package cmd import ( "bytes" "encoding/json" "fmt" "net/url" "os" "strings" "github.com/cozy/cozy-stack/client/request" "github.com/cozy/cozy-stack/pkg/consts" "github.com/spf13/cobra" ) var flagWithSources bool var featureCmdGroup = &cobra.Command{ Use: "features ", Aliases: []string{"feature"}, Short: "Manage the feature flags", } var featureShowCmd = &cobra.Command{ Use: "show", Short: `Display the computed feature flags for an instance`, Long: ` cozy-stack feature show displays the feature flags that are shown by apps. `, Example: `$ cozy-stack feature show --domain cozy.localhost:8080`, RunE: func(cmd *cobra.Command, args []string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } c := newClient(flagDomain, consts.Settings) req := &request.Options{ Method: "GET", Path: "/settings/flags", } if flagWithSources { req.Queries = url.Values{"include": {"source"}} } res, err := c.Req(req) if err != nil { return err } defer res.Body.Close() var obj struct { Data struct { Attributes map[string]json.RawMessage `json:"attributes"` } `json:"data"` Included []struct { ID string `json:"id"` Attributes map[string]json.RawMessage `json:"attributes"` } `json:"included"` } if err = json.NewDecoder(res.Body).Decode(&obj); err != nil { return err } for k, v := range obj.Data.Attributes { fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v)) } if len(obj.Included) > 0 { fmt.Fprintf(os.Stdout, "\nSources:\n") for _, source := range obj.Included { fmt.Fprintf(os.Stdout, "- %s\n", source.ID) for k, v := range source.Attributes { fmt.Fprintf(os.Stdout, "\t- %s: %s\n", k, string(v)) } } } return nil }, } var featureFlagCmd = &cobra.Command{ Use: "flags", Aliases: []string{"flag"}, Short: `Display and update the feature flags for an instance`, Long: ` cozy-stack feature flags displays the feature flags that are specific to an instance. It can also take a list of flags to update. If you give a null value, the flag will be removed. `, Example: `$ cozy-stack feature flags --domain cozy.localhost:8080 '{"add_this_flag": true, "remove_this_flag": null}'`, RunE: func(cmd *cobra.Command, args []string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } ac := newAdminClient() req := request.Options{ Method: "GET", Path: fmt.Sprintf("/instances/%s/feature/flags", flagDomain), } if len(args) > 0 { req.Method = "PATCH" req.Body = strings.NewReader(args[0]) } res, err := ac.Req(&req) if err != nil { return err } defer res.Body.Close() var obj map[string]json.RawMessage if err = json.NewDecoder(res.Body).Decode(&obj); err != nil { return err } for k, v := range obj { fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v)) } return nil }, } var featureSetCmd = &cobra.Command{ Use: "sets", Short: `Display and update the feature sets for an instance`, Long: ` cozy-stack feature sets displays the feature sets coming from the manager. It can also take a space-separated list of sets that will replace the previous list (no merge). All the sets can be removed by setting an empty list (''). `, Example: `$ cozy-stack feature sets --domain cozy.localhost:8080 'set1 set2'`, RunE: func(cmd *cobra.Command, args []string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } ac := newAdminClient() req := request.Options{ Method: "GET", Path: fmt.Sprintf("/instances/%s/feature/sets", flagDomain), } if len(args) > 0 { list := args if len(args) == 1 { list = strings.Fields(args[0]) } buf, err := json.Marshal(list) if err != nil { return err } req.Method = "PUT" req.Body = bytes.NewReader(buf) } res, err := ac.Req(&req) if err != nil { return err } defer res.Body.Close() var sets []string if err = json.NewDecoder(res.Body).Decode(&sets); err != nil { return err } for _, set := range sets { fmt.Fprintf(os.Stdout, "- %v\n", set) } return nil }, } var featureRatioCmd = &cobra.Command{ Use: "ratio ", Aliases: []string{"context"}, Short: `Display and update the feature flags for a context`, Long: ` cozy-stack feature ratio displays the feature flags for a context. It can also create, update, or remove flags (with a ratio and value). To remove a flag, set it to an empty array (or null). `, Example: `$ cozy-stack feature ratio --context beta '{"set_this_flag": [{"ratio": 0.1, "value": 1}, {"ratio": 0.9, "value": 2}] }'`, RunE: func(cmd *cobra.Command, args []string) error { if flagContext == "" { return cmd.Usage() } ac := newAdminClient() req := request.Options{ Method: "GET", Path: fmt.Sprintf("/instances/feature/contexts/%s", flagContext), } if len(args) > 0 { req.Method = "PATCH" req.Body = strings.NewReader(args[0]) } res, err := ac.Req(&req) if err != nil { return err } defer res.Body.Close() var obj map[string]json.RawMessage if err = json.NewDecoder(res.Body).Decode(&obj); err != nil { return err } for k, v := range obj { fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v)) } return nil }, } var featureConfigCmd = &cobra.Command{ Use: "config ", Short: `Display the feature flags from configuration for a context`, Long: ` cozy-stack feature config displays the feature flags from configuration for a context. These flags are read only and can only be updated by changing configuration and restarting the stack. `, Example: `$ cozy-stack feature config --context beta`, RunE: func(cmd *cobra.Command, args []string) error { if flagContext == "" { return cmd.Usage() } ac := newAdminClient() req := request.Options{ Method: "GET", Path: fmt.Sprintf("/instances/feature/config/%s", flagContext), } res, err := ac.Req(&req) if err != nil { return err } defer res.Body.Close() var obj map[string]json.RawMessage if err = json.NewDecoder(res.Body).Decode(&obj); err != nil { return err } for k, v := range obj { fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v)) } return nil }, } var featureDefaultCmd = &cobra.Command{ Use: "defaults", Short: `Display and update the default values for feature flags`, Long: ` cozy-stack feature defaults displays the default values for feature flags. It can also take a list of flags to update. If you give a null value, the flag will be removed. `, Example: `$ cozy-stack feature defaults '{"add_this_flag": true, "remove_this_flag": null}'`, RunE: func(cmd *cobra.Command, args []string) error { ac := newAdminClient() req := request.Options{ Method: "GET", Path: "/instances/feature/defaults", } if len(args) > 0 { req.Method = "PATCH" req.Body = strings.NewReader(args[0]) } res, err := ac.Req(&req) if err != nil { return err } defer res.Body.Close() var obj map[string]json.RawMessage if err = json.NewDecoder(res.Body).Decode(&obj); err != nil { return err } for k, v := range obj { fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v)) } return nil }, } func init() { featureShowCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance") featureShowCmd.Flags().BoolVar(&flagWithSources, "source", false, "Show the sources of the feature flags") featureFlagCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance") featureSetCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance") featureRatioCmd.Flags().StringVar(&flagContext, "context", "", "The context for the feature flags") featureConfigCmd.Flags().StringVar(&flagContext, "context", "", "The context for the feature flags") featureCmdGroup.AddCommand(featureShowCmd) featureCmdGroup.AddCommand(featureFlagCmd) featureCmdGroup.AddCommand(featureSetCmd) featureCmdGroup.AddCommand(featureRatioCmd) featureCmdGroup.AddCommand(featureConfigCmd) featureCmdGroup.AddCommand(featureDefaultCmd) RootCmd.AddCommand(featureCmdGroup) }