diff --git a/README.md b/README.md index c4ffda27..4cc163b4 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,12 @@ CLUSTER_ID= # the unique cluster name, or internal, external id for a cluster TEMPLATE= # file or url in which the template exists in osdctl servicelog post ${CLUSTER_ID} --template=${TEMPLATE} --dry-run +# Post an internal-only service log message +osdctl servicelog post ${CLUSTER_ID} -i -p "MESSAGE=This is an internal message" --dry-run + +# Post a short external message +osdctl servicelog post ${CLUSTER_ID} -r "summary=External Message" -r "description=This is an external message" -r internal_only=False --dry-run + QUERIES_HERE= # queries that can be run on ocm's `clusters` resource TEMPLATE= # file or url in which the template exists in osdctl servicelog post --template=${TEMPLATE} --query=${QUERIES_HERE} --dry-run diff --git a/cmd/servicelog/common.go b/cmd/servicelog/common.go index f491f4e0..f3e47435 100644 --- a/cmd/servicelog/common.go +++ b/cmd/servicelog/common.go @@ -3,12 +3,13 @@ package servicelog import ( "encoding/json" "fmt" + "time" + sdk "github.com/openshift-online/ocm-sdk-go" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" v1 "github.com/openshift-online/ocm-sdk-go/servicelogs/v1" "github.com/openshift/osdctl/internal/servicelog" "github.com/openshift/osdctl/pkg/utils" - "time" ) var ( diff --git a/cmd/servicelog/post.go b/cmd/servicelog/post.go index 1f28ef11..f12866a3 100644 --- a/cmd/servicelog/post.go +++ b/cmd/servicelog/post.go @@ -2,12 +2,15 @@ package servicelog import ( "encoding/json" + "errors" "fmt" "net/url" "os" "os/signal" "path/filepath" + "reflect" "regexp" + "strconv" "strings" "time" @@ -32,6 +35,7 @@ type PostCmdOptions struct { ClustersFile servicelog.ClustersFile Template string TemplateParams []string + Overrides []string filterFiles []string // Path to filter file filtersFromFile string // Contents of filterFiles filterParams []string @@ -63,6 +67,12 @@ func newPostCmd() *cobra.Command { # Post a service log to a single cluster via a remote URL, providing a parameter osdctl servicelog post ${CLUSTER_ID} -t https://raw.githubusercontent.com/openshift/managed-notifications/master/osd/incident_resolved.json -p ALERT_NAME="alert" + # Post an internal-only service log message + osdctl servicelog post ${CLUSTER_ID} -i -p "MESSAGE=This is an internal message" + + # Post a short external message + osdctl servicelog post ${CLUSTER_ID} -r "summary=External Message" -r "description=This is an external message" -r internal_only=False + # Post a service log to a group of clusters, determined by an OCM query ocm list cluster -p search="cloud_provider.id is 'gcp' and managed='true' and state is 'ready'" osdctl servicelog post -q "cloud_provider.id is 'gcp' and managed='true' and state is 'ready'" -t file.json @@ -76,9 +86,10 @@ func newPostCmd() *cobra.Command { }, } - // define required flags + // define flags postCmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Message template file or URL") postCmd.Flags().StringArrayVarP(&opts.TemplateParams, "param", "p", opts.TemplateParams, "Specify a key-value pair (eg. -p FOO=BAR) to set/override a parameter value in the template.") + postCmd.Flags().StringArrayVarP(&opts.Overrides, "override", "r", opts.Overrides, "Specify a key-value pair (eg. -r FOO=BAR) to replace a JSON key in the document, only supports string fields") postCmd.Flags().BoolVarP(&opts.isDryRun, "dry-run", "d", false, "Dry-run - print the service log about to be sent but don't send it.") postCmd.Flags().StringArrayVarP(&opts.filterParams, "query", "q", []string{}, "Specify a search query (eg. -q \"name like foo\") for a bulk-post to matching clusters.") postCmd.Flags().BoolVarP(&opts.skipPrompts, "yes", "y", false, "Skips all prompts.") @@ -129,15 +140,28 @@ func (o *PostCmdOptions) Run() error { return err } - o.parseUserParameters() // parse all the '-p' user flags - o.readFilterFile() // parse the ocm filters in file provided via '-f' flag - o.readTemplate() // parse the given JSON template provided via '-t' flag + o.parseUserParameters() // parse all the '-p' user flags + overrideMap, err := o.parseOverrides() // parse all the '-o' flags + if err != nil { + log.Fatalf("Error parsing overrides: %s", err) + } + + o.readFilterFile() // parse the ocm filters in file provided via '-f' flag + o.readTemplate() // parse the given JSON template provided via '-t' flag // For every '-p' flag, replace its related placeholder in the template & filterFiles for k := range userParameterNames { o.replaceFlags(userParameterNames[k], userParameterValues[k]) } + // Replace any overrides + for overrideKey, overrideValue := range overrideMap { + err := o.overrideField(overrideKey, overrideValue) + if err != nil { + log.Fatalf("could not override '%s': %s", overrideKey, err) + } + } + // Check if there are any remaining placeholders in the template that are not replaced by a parameter, // excluding '${CLUSTER_UUID}' which will be replaced for each cluster later o.checkLeftovers([]string{"${CLUSTER_UUID}"}) @@ -337,6 +361,68 @@ func (o *PostCmdOptions) parseUserParameters() { } } +// parseOverides parses all the '-o FOO=BAR' overrides which replace items in the final JSON document +func (o *PostCmdOptions) parseOverrides() (map[string]string, error) { + usageMessageError := errors.New("invalid syntax. Usage: '-r FOO=BAR'") + overrideMap := make(map[string]string) + + for _, v := range o.Overrides { + if !strings.Contains(v, "=") { + return nil, usageMessageError + } + + param := strings.SplitN(v, "=", 2) + if param[0] == "" || param[1] == "" { + return nil, usageMessageError + } + + overrideMap[param[0]] = param[1] + } + + return overrideMap, nil +} + +func (o *PostCmdOptions) overrideField(overrideKey string, overrideValue string) (err error) { + // Get a pointer, then the value of that pointer so that we can edit the fields + rt := reflect.ValueOf(&o.Message).Elem() + + for i := 0; i < rt.NumField(); i++ { + // Get JSON field name + field := rt.Type().Field(i) + jsonName := strings.Split(field.Tag.Get("json"), ",")[0] + + if overrideKey == jsonName { + // This shouldn't happen, but if it does we should make a nice error + if !rt.Field(i).CanSet() { + return fmt.Errorf("field cannot be modified") + } + + kind := rt.Field(i).Kind() + + // Set the field to the overridden value, since we have a string + // we may have to parse it to get the right type + switch kind { + case reflect.String: + rt.Field(i).SetString(overrideValue) + + case reflect.Bool: + overrideBool, err := strconv.ParseBool(overrideValue) + if err != nil { + return fmt.Errorf("couldn't parse bool: %s", err) + } + rt.Field(i).SetBool(overrideBool) + + default: + return fmt.Errorf("overriding of type %s not implemented", kind) + } + + return nil + } + } + + return fmt.Errorf("field does not exist") +} + // accessFile returns the contents of a local file or url, and any errors encountered func (o *PostCmdOptions) accessFile(filePath string) ([]byte, error) { @@ -392,6 +478,21 @@ func (o *PostCmdOptions) readTemplate() { return } + // If neither `-i` or `-t` is specified, but `-r` is specified at least once then use a pre-canned template + if !o.InternalOnly && (o.Template == "") && (len(o.Overrides) != 0) { + messageTemplate := []byte(` + { + "severity": "Info", + "service_name": "SREManualAction", + "internal_only": true + } + `) + if err := o.parseTemplate(messageTemplate); err != nil { + log.Fatalf("Cannot not parse the default message template.\nError: %q\n", err) + } + return + } + if o.Template == "" { log.Fatalf("Template file is not provided. Use '-t' to fix this.") } diff --git a/cmd/servicelog/post_test.go b/cmd/servicelog/post_test.go new file mode 100644 index 00000000..8e491427 --- /dev/null +++ b/cmd/servicelog/post_test.go @@ -0,0 +1,110 @@ +package servicelog + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/openshift/osdctl/internal/servicelog" +) + +func TestSetup(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Setup Suite") +} + +var _ = Describe("Test posting service logs", func() { + var options *PostCmdOptions + + BeforeEach(func() { + options = &PostCmdOptions{ + Overrides: []string{ + "description=new description", + "summary=new summary", + }, + Message: servicelog.Message{ + Summary: "The original summary", + InternalOnly: false, + }, + } + }) + + Context("overriding a field", func() { + It("overrides string fields successfully", func() { + overrideString := "Overridden Summary" + err := options.overrideField("summary", overrideString) + + Expect(err).ShouldNot(HaveOccurred()) + Expect(options.Message.Summary).To(Equal(overrideString)) + }) + + It("overrides bool fields correctly", func() { + Expect(options.Message.InternalOnly).ToNot(Equal(true)) + + err := options.overrideField("internal_only", "true") + + Expect(err).ShouldNot(HaveOccurred()) + Expect(options.Message.InternalOnly).To(Equal(true)) + }) + + It("errors when overriding a field that does not exist", func() { + err := options.overrideField("does_not_exist", "") + + Expect(err).Should(HaveOccurred()) + }) + + It("errors when overriding a bool with an unparsable string", func() { + err := options.overrideField("internal_only", "ThisIsNotABool") + + Expect(err).Should(HaveOccurred()) + }) + + It("errors when overriding an unsupported data type", func() { + err := options.overrideField("doc_references", "DoesntMatter") + + Expect(err).Should(HaveOccurred()) + }) + }) + + Context("parsing overrides", func() { + It("parses correctly", func() { + overrideMap, err := options.parseOverrides() + + Expect(err).ShouldNot(HaveOccurred()) + Expect(overrideMap).To(HaveKey("description")) + Expect(overrideMap["description"]).To(Equal("new description")) + Expect(overrideMap).To(HaveKey("summary")) + Expect(overrideMap["summary"]).To(Equal("new summary")) + }) + + It("fails when an option contains no equals sign", func() { + options.Overrides = []string{ + "THISDOESNOTHAVEANEQUALS", + } + + _, err := options.parseOverrides() + + Expect(err).Should(HaveOccurred()) + }) + + It("fails when an option has no key", func() { + options.Overrides = []string{ + "=VALUE", + } + + _, err := options.parseOverrides() + + Expect(err).Should(HaveOccurred()) + }) + + It("fails when an option has no value", func() { + options.Overrides = []string{ + "KEY=", + } + + _, err := options.parseOverrides() + + Expect(err).Should(HaveOccurred()) + }) + }) +})