From c21b0cc5a6910e06b9061cfebb0bba6edc0ef6a1 Mon Sep 17 00:00:00 2001 From: Siggi Skulason Date: Thu, 28 Oct 2021 17:18:11 +0100 Subject: [PATCH] feat: add V2 support-notifications This commit adds support for support-notification endpoints: - notification [add/cleanup/list/rm] - subscription [add/get/list/rm] - transmission [get/list/rm] Fix #392 Fix #389 Signed-off-by: Siggi Skulason --- internal/cmd/common.go | 8 + internal/cmd/notification.go | 286 +++++++++++++++++++++++++++++++ internal/cmd/subscription.go | 315 +++++++++++++++++++++++++++++++++++ internal/cmd/transmission.go | 237 ++++++++++++++++++++++++++ internal/service/service.go | 10 ++ 5 files changed, 856 insertions(+) create mode 100644 internal/cmd/notification.go create mode 100644 internal/cmd/subscription.go create mode 100644 internal/cmd/transmission.go diff --git a/internal/cmd/common.go b/internal/cmd/common.go index aa61158..c44efb9 100644 --- a/internal/cmd/common.go +++ b/internal/cmd/common.go @@ -134,3 +134,11 @@ func getRFC822Time(t int64) string { return time.Unix(0, t*int64(time.Millisecond)).Format(time.RFC822) } } + +func getMillisTimestampFromRFC822Time(t string) (int64, error) { + result, err := time.Parse(time.RFC822, t) + if err != nil { + return 0, err + } + return result.UnixNano() / int64(time.Millisecond), nil +} diff --git a/internal/cmd/notification.go b/internal/cmd/notification.go new file mode 100644 index 0000000..0d438e9 --- /dev/null +++ b/internal/cmd/notification.go @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2021 Canonical Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + * SPDX-License-Identifier: Apache-2.0' + */ + +package cmd + +import ( + "context" + jsonpkg "encoding/json" + "errors" + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v2/models" + "github.com/spf13/cobra" +) + +func init() { + var cmd = &cobra.Command{ + Use: "notification", + Short: "Add, remove and list notifications [Support Notifications]", + Long: "Add, remove and list notifications [Support Notifications]", + SilenceUsage: true, + } + rootCmd.AddCommand(cmd) + initAddNotificationCommand(cmd) + initListNotificationCommand(cmd) + initRmNotificationCommand(cmd) + initCleanupNotificationCommand(cmd) + +} + +var notificationCategory, notificationContent, notificationContentType, notificationDescription string +var notificationSender, notificationSeverity, notificationStatus string +var notificationLabel, notificationStart, notificationEnd, notificationId string + +// initCleanupNotificationCommand implements DELETE /cleanup +// "Deletes all notifications and the corresponding transmissions."" +func initCleanupNotificationCommand(cmd *cobra.Command) { + var cleanup = &cobra.Command{ + Use: "cleanup", + Short: "Delete all notifications and corresponding transmissions", + Long: "Delete all notifications and corresponding transmissions", + RunE: handleCleanupNotifications, + SilenceUsage: true, + } + cmd.AddCommand(cleanup) +} + +// initRmDeviceCommand implements the DELETE /notification/id/{id} +// "Deletes a notification by ID and all of its associated transmissions."" +func initRmNotificationCommand(cmd *cobra.Command) { + var rm = &cobra.Command{ + Use: "rm", + Short: "Delete a notification and all of its associated transmissions", + Long: "Delete a notification and all of its associated transmissions", + RunE: handleRmNotifications, + SilenceUsage: true, + } + rm.Flags().StringVarP(¬ificationId, "id", "i", "", "The ID that identifies the notification") + rm.MarkFlagRequired("id") + cmd.AddCommand(rm) +} + +// initAddNotificationCommand implements the POST /notification endpoint +// "Adds one or more notifications to be sent." +func initAddNotificationCommand(cmd *cobra.Command) { + var add = &cobra.Command{ + Use: "add", + Short: "Add a notification to be sent", + Long: "Add a notification to be sent", + Example: ` edgex-cli notification add -s "sender01" -c "content" --category "category01"`, + RunE: handleAddNotifications, + SilenceUsage: true, + } + add.Flags().StringVarP(¬ificationCategory, "category", "", "", "Categorizes the notification") + add.Flags().StringVarP(¬ificationContent, "content", "c", "", "The content to be sent as the body of the notification") + add.Flags().StringVarP(¬ificationContentType, "content-type", "t", "", "Indicates the MIME type/Content-type of the notification's content") + add.Flags().StringVarP(¬ificationDescription, "description", "d", "", "An optional description of the notification's intent") + add.Flags().StringVarP(¬ificationSender, "sender", "s", "", "Identifies the sender of a notification, usually the name of sender") + add.Flags().StringVarP(¬ificationSeverity, "severity", "", "NORMAL", "Indicates the level of severity for the notification. Current accepted values include: MINOR, NORMAL, CRITICAL") + add.Flags().StringVarP(¬ificationStatus, "status", "", "", "A status indicating the current processing status of the notification. Accepted values are: NEW, PROCESSED, ESCALATED") + addLabelsFlag(add) + add.MarkFlagRequired("sender") + add.MarkFlagRequired("content") + add.MarkFlagRequired("category") + cmd.AddCommand(add) +} + +// initListNotificationCommand implements a number of endpoints: +// GET /notification/category/{category} +// "Returns a paginated list of notifications associated with the given category." +// GET /notification/label/{label} +// "Returns a paginated list of notifications associated with the given label." +// GET /notification/start/{start}/end/{end} +// "Allows querying of notifications by their creation timestamp within a given time range, sorted in descending order. Results are paginated."" +// GET /notification/status/{status} +// "Returns a paginated list of notifications with the specified status." +func initListNotificationCommand(cmd *cobra.Command) { + var listCmd = &cobra.Command{ + Use: "list", + Short: "List notifications", + Long: "List notifications associated with a given label, category or time range", + Example: ` edgex-cli notification list --start "01 jan 20 00:00 GMT" --end "01 dec 21 00:00 GMT" + edgex-cli notification list --category "category01" + edgex-cli notification list --label "l01"`, + RunE: handleListNotifications, + SilenceUsage: true, + } + listCmd.Flags().StringVarP(¬ificationCategory, "category", "c", "", "List notifications belonging to this category") + listCmd.Flags().StringVarP(¬ificationLabel, "label", "", "", "List notifications with this label") + listCmd.Flags().StringVarP(¬ificationStart, "start", "s", "", "List notifications from after this (RFC822) timestamp") + listCmd.Flags().StringVarP(¬ificationEnd, "end", "e", "", "List notifications from before this (RFC822) timestamp") + listCmd.Flags().StringVarP(¬ificationStatus, "status", "", "", "List notifications with this status") + + addFormatFlags(listCmd) + addVerboseFlag(listCmd) + addLimitOffsetFlags(listCmd) + cmd.AddCommand(listCmd) +} + +func handleCleanupNotifications(cmd *cobra.Command, args []string) error { + client := getSupportNotificationsService().GetNotificationClient() + + response, err := client.CleanupNotifications(context.Background()) + if err != nil { + fmt.Println(response.Message) + } + return err +} + +func handleRmNotifications(cmd *cobra.Command, args []string) error { + client := getSupportNotificationsService().GetNotificationClient() + response, err := client.DeleteNotificationById(context.Background(), notificationId) + if err == nil { + fmt.Println(response.Message) + } + return err +} + +func handleAddNotifications(cmd *cobra.Command, args []string) error { + + if notificationStatus != "" { + notificationStatus = strings.ToUpper(notificationStatus) + if !(notificationStatus == models.New || notificationStatus == models.Processed || notificationStatus == models.Escalated) { + return fmt.Errorf("status should be %s, %s or %s", models.New, models.Processed, models.Escalated) + } + } + + if notificationSeverity != "" { + notificationSeverity = strings.ToUpper(notificationSeverity) + if !(notificationSeverity == models.Minor || notificationSeverity == models.Normal || notificationSeverity == models.Critical) { + return fmt.Errorf("severity should be %s, %s or %s", models.Minor, models.Normal, models.Critical) + } + } + + client := getSupportNotificationsService().GetNotificationClient() + + var req = requests.NewAddNotificationRequest(dtos.Notification{ + Category: notificationCategory, + Content: notificationContent, + ContentType: notificationContentType, + Description: notificationDescription, + Sender: notificationSender, + Severity: notificationSeverity, + Status: notificationStatus, + Labels: getLabels(), + }) + + response, err := client.SendNotification(context.Background(), []requests.AddNotificationRequest{req}) + + if err != nil { + return err + } + if response != nil { + fmt.Println(response[0]) + } + return err +} + +func handleListNotifications(cmd *cobra.Command, args []string) error { + client := getSupportNotificationsService().GetNotificationClient() + var response responses.MultiNotificationsResponse + var err error + + if notificationCategory != "" { + response, err = client.NotificationsByCategory(context.Background(), notificationCategory, offset, limit) + } else if notificationLabel != "" { + response, err = client.NotificationsByLabel(context.Background(), notificationLabel, offset, limit) + } else if notificationStatus != "" { + notificationStatus = strings.ToUpper(notificationStatus) + if !(notificationStatus == models.New || notificationStatus == models.Processed || notificationStatus == models.Escalated) { + return fmt.Errorf("status should be %s, %s or %s", models.New, models.Processed, models.Escalated) + } + response, err = client.NotificationsByStatus(context.Background(), notificationStatus, offset, limit) + } else if notificationStart != "" && notificationEnd != "" { + start, err := getMillisTimestampFromRFC822Time(notificationStart) + if err != nil { + return err + } + end, err := getMillisTimestampFromRFC822Time(notificationEnd) + if err != nil { + return err + } + response, err = client.NotificationsByTimeRange(context.Background(), int(start), int(end), offset, limit) + } else { + return errors.New("category, label, status or a timerange must be specified") + } + + if err != nil { + return err + } + + if json { + result, err := jsonpkg.Marshal(response) + if err != nil { + return err + } + fmt.Print(string(result)) + } else { + + if len(response.Notifications) == 0 { + fmt.Println("No notifications available") + return nil + } + w := tabwriter.NewWriter(os.Stdout, 1, 1, 2, ' ', 0) + printNotificationTableHeader(w) + for _, n := range response.Notifications { + printNotification(w, &n) + } + w.Flush() + } + return nil +} + +func printNotificationTableHeader(w *tabwriter.Writer) { + if verbose { + fmt.Fprintln(w, "Id\tCategory\tContent\tContentType\tCreated\tDescription\tLabels\tModified\tSender\tSeverity\tStatus") + } else { + fmt.Fprintln(w, "Category\tContent\tDescription\tLabels\tSender\tSeverity\tStatus") + } + +} + +func printNotification(w *tabwriter.Writer, n *dtos.Notification) { + if verbose { + fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", + n.Id, + n.Category, + n.Content, + n.ContentType, + getRFC822Time(n.Created), + n.Description, + n.Labels, + getRFC822Time(n.Modified), + n.Sender, + n.Severity, + n.Status) + } else { + fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\n", + n.Category, + n.Content, + n.Description, + n.Labels, + n.Sender, + n.Severity, + n.Status) + } +} diff --git a/internal/cmd/subscription.go b/internal/cmd/subscription.go new file mode 100644 index 0000000..e9534d1 --- /dev/null +++ b/internal/cmd/subscription.go @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2021 Canonical Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + * SPDX-License-Identifier: Apache-2.0' + */ + +package cmd + +import ( + "context" + jsonpkg "encoding/json" + "errors" + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/spf13/cobra" +) + +func init() { + var cmd = &cobra.Command{ + Use: "subscription", + Short: "Add, remove and list subscriptions [Support Notificationss]", + Long: "Add, remove and list subscriptions [Support Notifications]", + SilenceUsage: true, + } + rootCmd.AddCommand(cmd) + initRmSubscriptionCommand(cmd) + initAddSubscriptionCommand(cmd) + initListSubscriptionCommand(cmd) + initGetSubscriptionByNameCommand(cmd) +} + +// initRmSubscriptionCommand implements the DELETE /subscription/name/{name} endpoint +// "Deletes a subscription according to the given name." +func initRmSubscriptionCommand(cmd *cobra.Command) { + var rm = &cobra.Command{ + Use: "rm", + Short: "Delete the named subscription", + Long: "Delete the named subscription", + RunE: handleRmSubscription, + SilenceUsage: true, + } + rm.Flags().StringVarP(&subscriptionName, "name", "n", "", "Name of subscription to remove") + rm.MarkFlagRequired("name") + cmd.AddCommand(rm) +} + +var subscriptionName, subscriptionChannels, subscriptionReceiver string +var subscriptionCategories, subscriptionDescription, subscriptionResendInterval string +var subscriptionResendLimit int +var subscriptionAdminState, subscriptionSelectedCategory, subscriptionSelectedLabel, subscriptionSelectedReceiver string + +func getSubscriptionChannels() (channels []dtos.Address, err error) { + if subscriptionChannels != "" { + err = jsonpkg.Unmarshal([]byte(subscriptionChannels), &channels) + if err != nil { + err = fmt.Errorf("channels JSON object array invalid (%v)", err) + } + } + return +} + +func getSubscriptionCategories() []string { + var a []string + if len(subscriptionCategories) > 0 { + a = strings.Split(subscriptionCategories, ",") + } + return a + +} + +// initAddSubscriptionCommand implements the POST /notification endpoint +// "Adds one or more notifications to be sent." +func initAddSubscriptionCommand(cmd *cobra.Command) { + var add = &cobra.Command{ + Use: "add", + Short: "Add a new subscription", + Long: "Add a new subscription", + Example: ` edgex-cli subscription add -n "name01" --receiver "receiver01" -c "[{\"type\": \"REST\", \"host\": \"localhost\", \"port\": 7770, \"httpMethod\": \"POST\"}]"`, + RunE: handleAddSubscription, + SilenceUsage: true, + } + + add.Flags().StringVarP(&subscriptionName, "name", "n", "", "A meaningful identifier for the subscription") + add.Flags().StringVarP(&subscriptionChannels, "channels", "c", "", "A JSON object array indicating how this subscription is capable of receiving notifications") + add.Flags().StringVarP(&subscriptionCategories, "categories", "", "", "A comma-delimited list of categories") + add.Flags().StringVarP(&subscriptionReceiver, "receiver", "", "", "The name of the party interested in the notification") + add.Flags().StringVarP(&subscriptionDescription, "description", "", "", "An optional description of the subscription's intent.") + add.Flags().IntVarP(&subscriptionResendLimit, "resend-limit", "", 0, "The retry limit for attempts to send notifications") + add.Flags().StringVarP(&subscriptionResendInterval, "resend-interval", "", "1h", "The interval in ISO 8691 format of resending the notification") + add.Flags().StringVarP(&subscriptionAdminState, "admin-state", "a", "UNLOCKED", "Admin state [LOCKED | UNLOCKED]") + + addLabelsFlag(add) + add.MarkFlagRequired("name") + add.MarkFlagRequired("receiver") + add.MarkFlagRequired("channels") + //admin-state" + //channels + cmd.AddCommand(add) +} + +// initListSubscriptionCommand implements a number of endpoints: +// GET /subscription/all +// "Allows paginated retrieval of subscriptions, sorted by created timestamp descending." +// GET /subscription/category/{category} +// "Returns a paginated list of subscriptions associated with the specified category." +// GET /subscription/label/{label} +// "Returns a paginated list of subscriptions associated with the specified label." +// GET /subscription/receiver/{receiver} +// "Returns a paginated list of subscriptions associated with the specified receiver." +func initListSubscriptionCommand(cmd *cobra.Command) { + var listCmd = &cobra.Command{ + Use: "list", + Short: "List subscriptions", + Long: "List all subscriptions, optionally filtered by a given category, label or receiver", + Example: ` edgex-cli subscription list + edgex-cli subscription list --category "my-category" + edgex-cli subscription list --label "my-label"`, + RunE: handleListSubscription, + SilenceUsage: true, + } + listCmd.Flags().StringVarP(&subscriptionSelectedCategory, "category", "c", "", "List subscriptions associated with this category") + listCmd.Flags().StringVarP(&subscriptionSelectedLabel, "label", "", "", "List subscriptions associated with this label") + listCmd.Flags().StringVarP(&subscriptionSelectedReceiver, "receiver", "r", "", "List subscriptions associated with this receiver") + addFormatFlags(listCmd) + addVerboseFlag(listCmd) + addLimitOffsetFlags(listCmd) + cmd.AddCommand(listCmd) +} + +// initGetSubscriptionByNameCommand implements the GET ​/subscription/name endpoint +// "Returns a subscription by its unique name."" +func initGetSubscriptionByNameCommand(cmd *cobra.Command) { + var nameCmd = &cobra.Command{ + Use: "name", + Short: "Return a subscription by its unique name", + Long: `Return a subscription by its unique name`, + RunE: handleGetSubscriptionByName, + SilenceUsage: true, + } + nameCmd.Flags().StringVarP(&subscriptionName, "name", "n", "", "Subscription name") + nameCmd.MarkFlagRequired("name") + addFormatFlags(nameCmd) + addVerboseFlag(nameCmd) + cmd.AddCommand(nameCmd) + +} + +func handleRmSubscription(cmd *cobra.Command, args []string) error { + client := getSupportNotificationsService().GetSubscriptionClient() + response, err := client.DeleteSubscriptionByName(context.Background(), subscriptionName) + if err == nil { + fmt.Println(response.Message) + } + return err +} + +func handleAddSubscription(cmd *cobra.Command, args []string) error { + + client := getSupportNotificationsService().GetSubscriptionClient() + + err := validateAdminState(subscriptionAdminState) + if err != nil { + return err + } + + channels, err := getSubscriptionChannels() + if err != nil { + return err + } + + l := getLabels() + c := getSubscriptionCategories() + + if l == nil && c == nil { + return errors.New("either labels or categories must be specified") + + } + + var req = requests.NewAddSubscriptionRequest(dtos.Subscription{ + Name: subscriptionName, + Channels: channels, + Receiver: subscriptionReceiver, + Categories: c, + Labels: l, + Description: subscriptionDescription, + ResendLimit: subscriptionResendLimit, + ResendInterval: subscriptionResendInterval, + AdminState: subscriptionAdminState, + }) + + response, err := client.Add(context.Background(), []requests.AddSubscriptionRequest{req}) + + if err != nil { + return err + } + if response != nil { + fmt.Println(response[0]) + } + return err +} + +func handleListSubscription(cmd *cobra.Command, args []string) error { + + client := getSupportNotificationsService().GetSubscriptionClient() + + var response responses.MultiSubscriptionsResponse + var err error + + if subscriptionSelectedCategory != "" { + response, err = client.SubscriptionsByCategory(context.Background(), subscriptionSelectedCategory, offset, limit) + } else if subscriptionSelectedLabel != "" { + response, err = client.SubscriptionsByLabel(context.Background(), subscriptionSelectedLabel, offset, limit) + } else if subscriptionSelectedReceiver != "" { + response, err = client.SubscriptionsByReceiver(context.Background(), subscriptionSelectedReceiver, offset, limit) + } else { + response, err = client.AllSubscriptions(context.Background(), offset, limit) + } + + if err != nil { + return err + } + + if json { + result, err := jsonpkg.Marshal(response) + if err != nil { + return err + } + fmt.Print(string(result)) + } else { + + if len(response.Subscriptions) == 0 { + fmt.Println("No subscriptions available") + return nil + } + w := tabwriter.NewWriter(os.Stdout, 1, 1, 2, ' ', 0) + printSubscriptionTableHeader(w) + for _, n := range response.Subscriptions { + printSubscription(w, &n) + } + w.Flush() + } + return nil +} + +func handleGetSubscriptionByName(cmd *cobra.Command, args []string) error { + client := getSupportNotificationsService().GetSubscriptionClient() + + response, err := client.SubscriptionByName(context.Background(), subscriptionName) + if err != nil { + return err + } + + if json { + result, err := jsonpkg.Marshal(response) + if err != nil { + return err + } + + fmt.Println(string(result)) + } else { + w := tabwriter.NewWriter(os.Stdout, 1, 1, 2, ' ', 0) + printSubscriptionTableHeader(w) + printSubscription(w, &response.Subscription) + w.Flush() + } + return nil +} + +func printSubscriptionTableHeader(w *tabwriter.Writer) { + if verbose { + fmt.Fprintln(w, "Id\tName\tDescription\tChannels\tReceiver\tCategories\tLabels\tResendLimit\tResendInterval\tAdminState") + } else { + fmt.Fprintln(w, "Namet\tDescription\tChannels\tReceiver\tCategories\tLabels") + } +} + +func printSubscription(w *tabwriter.Writer, n *dtos.Subscription) { + if verbose { + fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", + n.Id, + n.Name, + n.Description, + n.Channels, + n.Receiver, + n.Categories, + n.Labels, + n.ResendLimit, + n.ResendInterval, + n.AdminState) + } else { + fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n", + n.Name, + n.Description, + n.Channels, + n.Receiver, + n.Categories, + n.Labels) + } +} diff --git a/internal/cmd/transmission.go b/internal/cmd/transmission.go new file mode 100644 index 0000000..0f61cd7 --- /dev/null +++ b/internal/cmd/transmission.go @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2021 Canonical Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + * SPDX-License-Identifier: Apache-2.0' + */ + +package cmd + +import ( + "context" + jsonpkg "encoding/json" + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v2/models" + "github.com/spf13/cobra" +) + +var transmissionId string +var transmissionAge int +var transmissionSubscriptionName, transmissionStart, transmissionEnd, transmissionStatus string + +func init() { + var cmd = &cobra.Command{ + Use: "transmission", + Short: "Remove and list transmissions [Support Notifications]", + Long: "Remove and list transmissions [Support Notifications]", + SilenceUsage: true, + } + rootCmd.AddCommand(cmd) + initRmTransmissionCommand(cmd) + initListTransmissionCommand(cmd) + initGetTransmissionByIdCommand(cmd) +} + +// initRmTransmissionCommand implements the DELETE /transmission/age/{age} +// "Deletes the processed transmissions if the current timestamp minus +// their created timestamp is less than the age parameter." +func initRmTransmissionCommand(cmd *cobra.Command) { + var rm = &cobra.Command{ + Use: "rm", + Short: "Delete processed transmissions", + Long: "Delete processed transmissions older than the specificed age (in milliseconds)", + RunE: handleRmTransmission, + SilenceUsage: true, + } + rm.Flags().IntVarP(&transmissionAge, "age", "a", 0, "The minimum age of transmissions to deleted (in milliseconds)") + rm.MarkFlagRequired("age") + cmd.AddCommand(rm) +} + +// initListTransmissionCommand implements a number of endpoints: +// - GET /transmission/all +// "Given the entire range of transmissions sorted in descending order of created time, +// returns a portion of that range according to the offset and limit parameters." +// - GET /transmission/subscription/name/{name} +// "Returns a paginated list of transmissions that originated with the specified subscription." +// - GET /transmission/start/{start}/end/{end} +// "Allows querying of transmissions by their creation timestamp within a +// given time range, sorted in descending order. Results are paginated. +// - GET /transmission/status/{status} +// "Allows retrieval of the transmissions associated with the specified status. +// Ordered by create timestamp descending."" +func initListTransmissionCommand(cmd *cobra.Command) { + var listCmd = &cobra.Command{ + Use: "list", + Short: "List transmissions", + Long: "Get paginated list of transmissions, optionally filtered by a subscription name, status or time range", + Example: ` edgex-cli transmission list + edgex-cli transmission list --name "name01" + edgex-cli transmission list --status "SENT" + edgex-cli transmission list --start "01 jan 20 00:00 GMT" --end "01 dec 21 00:00 GMT"`, + RunE: handleListTransmission, + SilenceUsage: true, + } + listCmd.Flags().StringVarP(&transmissionSubscriptionName, "name", "n", "", "List transmissions that originated with the specified subscription") + listCmd.Flags().StringVarP(&transmissionStart, "start", "s", "", "List transmissions from after this (RFC822) timestamp") + listCmd.Flags().StringVarP(&transmissionEnd, "end", "e", "", "List transmissions from before this (RFC822) timestamp") + listCmd.Flags().StringVarP(&transmissionStatus, "status", "", "", "List transmissions with this status [ACKNOWLEDGED, FAILED, SENT, RESENDING, ESCALATED]") + addFormatFlags(listCmd) + addVerboseFlag(listCmd) + addLimitOffsetFlags(listCmd) + cmd.AddCommand(listCmd) +} + +// initGetTransmissionByIdCommand implements the GET ​/transmission/id endpoint +// "Returns a transmission by ID." +func initGetTransmissionByIdCommand(cmd *cobra.Command) { + var nameCmd = &cobra.Command{ + Use: "id", + Short: "Return a transmission by ID", + Long: `Return a transmission by ID`, + RunE: handleGetTransmissionById, + SilenceUsage: true, + } + nameCmd.Flags().StringVarP(&transmissionId, "id", "i", "", "The ID that identifies the transmission") + nameCmd.MarkFlagRequired("id") + addFormatFlags(nameCmd) + addVerboseFlag(nameCmd) + cmd.AddCommand(nameCmd) + +} + +func handleRmTransmission(cmd *cobra.Command, args []string) error { + client := getSupportNotificationsService().GetTransmissionClient() + response, err := client.DeleteProcessedTransmissionsByAge(context.Background(), transmissionAge) + if err == nil { + fmt.Println(response.Message) + } + return err +} + +func handleListTransmission(cmd *cobra.Command, args []string) error { + + var transmissionSubscriptionName, transmissionStart, transmissionEnd, transmissionStatus string + + client := getSupportNotificationsService().GetTransmissionClient() + var response responses.MultiTransmissionsResponse + var err error + + if transmissionSubscriptionName != "" { + response, err = client.TransmissionsBySubscriptionName(context.Background(), transmissionSubscriptionName, offset, limit) + } else if transmissionStatus != "" { + transmissionStatus = strings.ToUpper(transmissionStatus) + if !(transmissionStatus == models.Acknowledged || notificationStatus == models.Failed || notificationStatus == models.Sent || + notificationStatus == models.RESENDING || notificationStatus == models.Escalated) { + return fmt.Errorf("status should be one of: %s, %s, %s, %s, %s", models.Acknowledged, models.Failed, models.Sent, + models.RESENDING, models.Escalated) + } + response, err = client.TransmissionsByStatus(context.Background(), transmissionStatus, offset, limit) + } else if transmissionStart != "" && transmissionEnd != "" { + start, err := getMillisTimestampFromRFC822Time(transmissionStart) + if err != nil { + return err + } + end, err := getMillisTimestampFromRFC822Time(transmissionEnd) + if err != nil { + return err + } + response, err = client.TransmissionsByTimeRange(context.Background(), int(start), int(end), offset, limit) + } else { + response, err = client.AllTransmissions(context.Background(), offset, limit) + + } + + if err != nil { + return err + } + + if json { + result, err := jsonpkg.Marshal(response) + if err != nil { + return err + } + fmt.Print(string(result)) + } else { + + if len(response.Transmissions) == 0 { + fmt.Println("No transmissions available") + return nil + } + w := tabwriter.NewWriter(os.Stdout, 1, 1, 2, ' ', 0) + printTransmissionTableHeader(w) + for _, n := range response.Transmissions { + printTransmission(w, &n) + } + w.Flush() + } + return nil +} + +func handleGetTransmissionById(cmd *cobra.Command, args []string) error { + client := getSupportNotificationsService().GetTransmissionClient() + + response, err := client.TransmissionById(context.Background(), transmissionId) + if err != nil { + return err + } + + if json { + result, err := jsonpkg.Marshal(response) + if err != nil { + return err + } + + fmt.Println(string(result)) + } else { + w := tabwriter.NewWriter(os.Stdout, 1, 1, 2, ' ', 0) + printTransmissionTableHeader(w) + printTransmission(w, &response.Transmission) + w.Flush() + } + return nil +} + +func printTransmissionTableHeader(w *tabwriter.Writer) { + if verbose { + fmt.Fprintln(w, "Id\tChannel\tCreated\tNotificationId\tSubscriptionName\tRecords\tResendCount\tStatus") + } else { + fmt.Fprintln(w, "SubscriptionName\tResendCount\tStatus") + } + +} + +func printTransmission(w *tabwriter.Writer, t *dtos.Transmission) { + if verbose { + fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", + t.Id, + t.Channel, + getRFC822Time(t.Created), + t.NotificationId, + t.SubscriptionName, + t.Records, + t.ResendCount, + t.Status) + } else { + fmt.Fprintf(w, "%v\t%v\t%v\n", + t.SubscriptionName, + t.ResendCount, + t.Status) + + } +} diff --git a/internal/service/service.go b/internal/service/service.go index 2ad7978..f70eace 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -76,3 +76,13 @@ func (c Service) GetNotificationClient() interfaces.NotificationClient { url := fmt.Sprintf("http://%s:%v", c.Host, c.Port) return http.NewNotificationClient(url) } + +func (c Service) GetSubscriptionClient() interfaces.SubscriptionClient { + url := fmt.Sprintf("http://%s:%v", c.Host, c.Port) + return http.NewSubscriptionClient(url) +} + +func (c Service) GetTransmissionClient() interfaces.TransmissionClient { + url := fmt.Sprintf("http://%s:%v", c.Host, c.Port) + return http.NewTransmissionClient(url) +}