From 84931a0ca45e5f85062eadd5dd258d312c02f638 Mon Sep 17 00:00:00 2001 From: Ron Green <11993626+georgettica@users.noreply.github.com> Date: Thu, 22 Apr 2021 16:08:40 +0300 Subject: [PATCH] feat(servicelog): list current servicelogs - refactor `osdctl servicelog post` --- cmd/root.go | 3 +- cmd/servicelog.go | 19 ---- cmd/servicelog/cmd.go | 21 +++++ cmd/servicelog/common.go | 57 ++++++++++++ cmd/servicelog/list.go | 121 +++++++++++++++++++++++++ cmd/{ => servicelog}/post.go | 83 +++++------------ docs/command/osdctl_servicelog.md | 1 + docs/command/osdctl_servicelog_list.md | 42 +++++++++ internal/servicelog/reply.go | 10 ++ 9 files changed, 278 insertions(+), 79 deletions(-) delete mode 100644 cmd/servicelog.go create mode 100644 cmd/servicelog/cmd.go create mode 100644 cmd/servicelog/common.go create mode 100644 cmd/servicelog/list.go rename cmd/{ => servicelog}/post.go (81%) create mode 100644 docs/command/osdctl_servicelog_list.md diff --git a/cmd/root.go b/cmd/root.go index 87a26d12..ebfc3933 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,6 +19,7 @@ import ( "github.com/openshift/osdctl/cmd/cost" "github.com/openshift/osdctl/cmd/federatedrole" "github.com/openshift/osdctl/cmd/network" + "github.com/openshift/osdctl/cmd/servicelog" ) // GitCommit is the short git commit hash from the environment @@ -63,7 +64,7 @@ func NewCmdRoot(streams genericclioptions.IOStreams) *cobra.Command { rootCmd.AddCommand(federatedrole.NewCmdFederatedRole(streams, kubeFlags)) rootCmd.AddCommand(network.NewCmdNetwork(streams, kubeFlags)) rootCmd.AddCommand(newCmdMetrics(streams, kubeFlags)) - rootCmd.AddCommand(servicelogCmd) + rootCmd.AddCommand(servicelog.NewCmdServiceLog()) // add docs command rootCmd.AddCommand(newCmdDocs(streams)) diff --git a/cmd/servicelog.go b/cmd/servicelog.go deleted file mode 100644 index ab825095..00000000 --- a/cmd/servicelog.go +++ /dev/null @@ -1,19 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -// servicelogCmd represents the servicelog command -var servicelogCmd = &cobra.Command{ - Use: "servicelog", - Short: "OCM/Hive Service log", - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, -} - -func init() { - // Add subcommands - servicelogCmd.AddCommand(postCmd) // servicelog post -} diff --git a/cmd/servicelog/cmd.go b/cmd/servicelog/cmd.go new file mode 100644 index 00000000..4bf45d34 --- /dev/null +++ b/cmd/servicelog/cmd.go @@ -0,0 +1,21 @@ +package servicelog + +import ( + "github.com/spf13/cobra" +) + +func NewCmdServiceLog() *cobra.Command { + var servicelogCmd = &cobra.Command{ + Use: "servicelog", + Short: "OCM/Hive Service log", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + // Add subcommands + servicelogCmd.AddCommand(listCmd) // servicelog list + servicelogCmd.AddCommand(postCmd) // servicelog post + + return servicelogCmd +} diff --git a/cmd/servicelog/common.go b/cmd/servicelog/common.go new file mode 100644 index 00000000..5f25040f --- /dev/null +++ b/cmd/servicelog/common.go @@ -0,0 +1,57 @@ +package servicelog + +import ( + "strings" + + "github.com/openshift-online/ocm-cli/pkg/ocm" + sdk "github.com/openshift-online/ocm-sdk-go" + log "github.com/sirupsen/logrus" +) + +var ( + templateParams, userParameterNames, userParameterValues []string + isURL bool + HTMLBody []byte +) + +const ( + // in case you want to see the swagger code gen, you can look at + // https://api.openshift.com/?urls.primaryName=Service%20logs#/default/post_api_service_logs_v1_cluster_logs + targetAPIPath = "/api/service_logs/v1/cluster_logs" +) + +func createConnection() *sdk.Connection { + connection, err := ocm.NewConnection().Build() + if err != nil { + if strings.Contains(err.Error(), "Not logged in, run the") { + log.Fatalf("Failed to create OCM connection: Authetication error, run the 'ocm login' command first.") + } + log.Fatalf("Failed to create OCM connection: %v", err) + } + return connection +} + +func sendRequest(request *sdk.Request) *sdk.Response { + response, err := request.Send() + if err != nil { + log.Fatalf("Can't send request: %v", err) + } + return response +} + +func check(response *sdk.Response, dir string) { + status := response.Status() + + body := response.Bytes() + + if status < 400 { + validateGoodResponse(body) + log.Info("Message has been successfully sent") + + } else { + validateBadResponse(body) + cleanup(dir) + log.Fatalf("Failed to post message because of %q", BadReply.Reason) + + } +} diff --git a/cmd/servicelog/list.go b/cmd/servicelog/list.go new file mode 100644 index 00000000..24c4f674 --- /dev/null +++ b/cmd/servicelog/list.go @@ -0,0 +1,121 @@ +package servicelog + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/openshift-online/ocm-cli/pkg/arguments" + "github.com/openshift-online/ocm-cli/pkg/dump" + sdk "github.com/openshift-online/ocm-sdk-go" + "github.com/openshift/osdctl/internal/servicelog" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// listCmd represents the list command +var listCmd = &cobra.Command{ + Use: "list", + Short: "gets all servicelog messages for a given cluster", + // validate only clusterid is provided + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clusterId := args[0] + + // Create an OCM client to talk to the cluster API + // the user has to be logged in (e.g. 'ocm login') + ocmClient := createConnection() + defer func() { + if err := ocmClient.Close(); err != nil { + log.Errorf("Cannot close the ocmClient (possible memory leak): %q", err) + } + }() + + // Use the OCM client to create the POST request + request := createClusterRequest(ocmClient, clusterId) + response := sendRequest(request) + clusterExternalId, err := extractExternalIdFromResponse(response) + if err != nil { + return err + } + + // send it as logservice and validate the response + request = createListRequest(ocmClient, clusterExternalId, serviceLogListAllMessagesFlag) + response = sendRequest(request) + + err = dump.Pretty(os.Stdout, response.Bytes()) + if err != nil { + return err + } + + return nil + }, +} + +var serviceLogListAllMessagesFlag = false + +const listServiceLogAPIPath = "/api/service_logs/v1/clusters/%s/cluster_logs" + +func init() { + // define required flags + listCmd.Flags().BoolVarP(&serviceLogListAllMessagesFlag, "all-messages", "A", serviceLogListAllMessagesFlag, "Toggle if we should see all of the messages or only SRE-P specific ones") +} + +func extractExternalIdFromResponse(response *sdk.Response) (string, error) { + status := response.Status() + body := response.Bytes() + + if status >= 400 { + validateBadResponse(body) + log.Fatalf("Failed to list message because of %q", BadReply.Reason) + return "", nil + } + + validateGoodResponse(body) + clusterListGoodReply := servicelog.ClusterListGoodReply{} + err := json.Unmarshal(body, &clusterListGoodReply) + if err != nil { + err = fmt.Errorf("cannot parse good clusterlist response: %w", err) + return "", err + } + + if clusterListGoodReply.Total != 1 || len(clusterListGoodReply.Items) != 1 { + return "", fmt.Errorf("could not find an exact match for the clustername") + } + + return clusterListGoodReply.Items[0].ExternalID, nil +} + +func createClusterRequest(ocmClient *sdk.Connection, clusterId string) *sdk.Request { + + searchString := fmt.Sprintf(`search=display_name like '%[1]s' or name like '%[1]s' or id like '%[1]s' or external_id like '%[1]s'`, clusterId) + searchString = strings.TrimSpace(searchString) + request := ocmClient.Get() + err := arguments.ApplyPathArg(request, "/api/clusters_mgmt/v1/clusters/") + if err != nil { + log.Fatalf("Can't parse API path '%s': %v\n", targetAPIPath, err) + } + + arguments.ApplyParameterFlag(request, []string{searchString}) + + return request +} + +func createListRequest(ocmClient *sdk.Connection, clusterId string, allMessages bool) *sdk.Request { + // Create and populate the request: + request := ocmClient.Get() + err := arguments.ApplyPathArg(request, targetAPIPath) + if err != nil { + log.Fatalf("Can't parse API path '%s': %v\n", targetAPIPath, err) + } + var empty []string + + formatMessage := fmt.Sprintf(`search=cluster_uuid = '%s'`, clusterId) + if !allMessages { + formatMessage += ` and service_name = 'SREManualAction'` + } + arguments.ApplyParameterFlag(request, []string{formatMessage}) + arguments.ApplyHeaderFlag(request, empty) + return request +} diff --git a/cmd/post.go b/cmd/servicelog/post.go similarity index 81% rename from cmd/post.go rename to cmd/servicelog/post.go index 64982a06..ecd61473 100644 --- a/cmd/post.go +++ b/cmd/servicelog/post.go @@ -1,36 +1,32 @@ -package cmd +package servicelog import ( "encoding/json" "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "strings" + "github.com/openshift-online/ocm-cli/pkg/arguments" "github.com/openshift-online/ocm-cli/pkg/dump" - "github.com/openshift-online/ocm-cli/pkg/ocm" sdk "github.com/openshift-online/ocm-sdk-go" "github.com/openshift/osdctl/internal/servicelog" "github.com/openshift/osdctl/internal/utils" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "io/ioutil" - "net/url" - "os" - "path/filepath" - "strings" ) var ( - template string - isURL bool - HTMLBody []byte - Message servicelog.Message - GoodReply servicelog.GoodReply - BadReply servicelog.BadReply - templateParams, userParameterNames, userParameterValues []string + GoodReply servicelog.GoodReply + BadReply servicelog.BadReply + Message servicelog.Message + template string ) const ( defaultTemplate = "" - targetAPIPath = "/api/service_logs/v1/cluster_logs" // https://api.openshift.com/?urls.primaryName=Service%20logs#/default/post_api_service_logs_v1_cluster_logs modifiedJSON = "modified-template.json" ) @@ -68,9 +64,15 @@ var postCmd = &cobra.Command{ // Use the OCM client to create the POST request // send it as logservice and validate the response - request := createRequest(ocmClient, newData) - response := postRequest(request) + request := createPostRequest(ocmClient, newData) + response := sendRequest(request) + check(response, dir) + + err := dump.Pretty(os.Stdout, response.Bytes()) + if err != nil { + log.Errorf("cannot print post command: %q", err) + } }, } @@ -226,18 +228,7 @@ func modifyTemplate(dir string) (newData string) { return newData } -func createConnection() *sdk.Connection { - connection, err := ocm.NewConnection().Build() - if err != nil { - if strings.Contains(err.Error(), "Not logged in, run the") { - log.Fatalf("Failed to create OCM connection: Authetication error, run the 'ocm login' command first.") - } - log.Fatalf("Failed to create OCM connection: %v", err) - } - return connection -} - -func createRequest(ocmClient *sdk.Connection, newData string) *sdk.Request { +func createPostRequest(ocmClient *sdk.Connection, newData string) *sdk.Request { // Create and populate the request: request := ocmClient.Post() err := arguments.ApplyPathArg(request, targetAPIPath) @@ -254,31 +245,6 @@ func createRequest(ocmClient *sdk.Connection, newData string) *sdk.Request { return request } -func postRequest(request *sdk.Request) *sdk.Response { - response, err := request.Send() - if err != nil { - log.Fatalf("Can't send request: %v", err) - } - return response -} - -func check(response *sdk.Response, dir string) { - status := response.Status() - - body := response.Bytes() - - if status < 400 { - validateGoodResponse(body) - log.Info("Message has been successfully sent") - - } else { - validateBadResponse(body) - cleanup(dir) - log.Fatalf("Failed to post message because of %q", BadReply.Reason) - - } -} - func validateGoodResponse(body []byte) { if err := parseGoodReply(body); err != nil { log.Fatalf("Cannot not parse the JSON template.\nError: %q\n", err) @@ -304,15 +270,14 @@ func validateGoodResponse(body []byte) { if description != Message.Description { log.Fatalf("Message sent, but wrong description information was passed (wanted %q, got %q)", Message.Description, description) } - - if err := dump.Pretty(os.Stdout, body); err != nil { - log.Fatalf("Server returned invalid JSON reply %q", err) + if ok := json.Valid(body); !ok { + log.Fatalf("Server returned invalid JSON") } } func validateBadResponse(body []byte) { - if err := dump.Pretty(os.Stderr, body); err != nil { - log.Errorf("Server returned invalid JSON reply %q", err) + if ok := json.Valid(body); !ok { + log.Errorf("Server returned invalid JSON") } if err := parseBadReply(body); err != nil { diff --git a/docs/command/osdctl_servicelog.md b/docs/command/osdctl_servicelog.md index 8af7fa3c..dc7f74d8 100644 --- a/docs/command/osdctl_servicelog.md +++ b/docs/command/osdctl_servicelog.md @@ -38,5 +38,6 @@ osdctl servicelog [flags] ### SEE ALSO * [osdctl](osdctl.md) - OSD CLI +* [osdctl servicelog list](osdctl_servicelog_list.md) - gets all servicelog messages for a given cluster * [osdctl servicelog post](osdctl_servicelog_post.md) - Send a servicelog message to a given cluster diff --git a/docs/command/osdctl_servicelog_list.md b/docs/command/osdctl_servicelog_list.md new file mode 100644 index 00000000..8b869938 --- /dev/null +++ b/docs/command/osdctl_servicelog_list.md @@ -0,0 +1,42 @@ +## osdctl servicelog list + +gets all servicelog messages for a given cluster + +### Synopsis + +gets all servicelog messages for a given cluster + +``` +osdctl servicelog list [flags] +``` + +### Options + +``` + -A, --all-messages Toggle if we should see all of the messages or only SRE-P specific ones + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --alsologtostderr log to standard error as well as files + --as string Username to impersonate for the operation + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory + --logtostderr log to standard error instead of files + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + -v, --v Level log level for V logs + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO + +* [osdctl servicelog](osdctl_servicelog.md) - OCM/Hive Service log + diff --git a/internal/servicelog/reply.go b/internal/servicelog/reply.go index 85163f19..1bae9e67 100644 --- a/internal/servicelog/reply.go +++ b/internal/servicelog/reply.go @@ -16,6 +16,16 @@ type GoodReply struct { CreatedAt time.Time `json:"created_at"` } +type ClusterListGoodReply struct { + Kind string `json:"kind"` + Page int `json:"page"` + Size int `json:"size"` + Total int `json:"total"` + Items []struct { + ExternalID string `json:"external_id"` + } `json:"items"` +} + type BadReply struct { ID string `json:"id"` Kind string `json:"kind"`