github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/cmd/feature.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/url"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/cozy/cozy-stack/client/request"
    12  	"github.com/cozy/cozy-stack/pkg/consts"
    13  	"github.com/spf13/cobra"
    14  )
    15  
    16  var flagWithSources bool
    17  
    18  var featureCmdGroup = &cobra.Command{
    19  	Use:     "features <command>",
    20  	Aliases: []string{"feature"},
    21  	Short:   "Manage the feature flags",
    22  }
    23  
    24  var featureShowCmd = &cobra.Command{
    25  	Use:   "show",
    26  	Short: `Display the computed feature flags for an instance`,
    27  	Long: `
    28  cozy-stack feature show displays the feature flags that are shown by apps.
    29  `,
    30  	Example: `$ cozy-stack feature show --domain cozy.localhost:8080`,
    31  	RunE: func(cmd *cobra.Command, args []string) error {
    32  		if flagDomain == "" {
    33  			errPrintfln("%s", errMissingDomain)
    34  			return cmd.Usage()
    35  		}
    36  		c := newClient(flagDomain, consts.Settings)
    37  		req := &request.Options{
    38  			Method: "GET",
    39  			Path:   "/settings/flags",
    40  		}
    41  		if flagWithSources {
    42  			req.Queries = url.Values{"include": {"source"}}
    43  		}
    44  		res, err := c.Req(req)
    45  		if err != nil {
    46  			return err
    47  		}
    48  		defer res.Body.Close()
    49  		var obj struct {
    50  			Data struct {
    51  				Attributes map[string]json.RawMessage `json:"attributes"`
    52  			} `json:"data"`
    53  			Included []struct {
    54  				ID         string                     `json:"id"`
    55  				Attributes map[string]json.RawMessage `json:"attributes"`
    56  			} `json:"included"`
    57  		}
    58  		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
    59  			return err
    60  		}
    61  		for k, v := range obj.Data.Attributes {
    62  			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
    63  		}
    64  		if len(obj.Included) > 0 {
    65  			fmt.Fprintf(os.Stdout, "\nSources:\n")
    66  			for _, source := range obj.Included {
    67  				fmt.Fprintf(os.Stdout, "- %s\n", source.ID)
    68  				for k, v := range source.Attributes {
    69  					fmt.Fprintf(os.Stdout, "\t- %s: %s\n", k, string(v))
    70  				}
    71  			}
    72  		}
    73  		return nil
    74  	},
    75  }
    76  
    77  var featureFlagCmd = &cobra.Command{
    78  	Use:     "flags",
    79  	Aliases: []string{"flag"},
    80  	Short:   `Display and update the feature flags for an instance`,
    81  	Long: `
    82  cozy-stack feature flags displays the feature flags that are specific to an instance.
    83  
    84  It can also take a list of flags to update.
    85  
    86  If you give a null value, the flag will be removed.
    87  `,
    88  	Example: `$ cozy-stack feature flags --domain cozy.localhost:8080 '{"add_this_flag": true, "remove_this_flag": null}'`,
    89  	RunE: func(cmd *cobra.Command, args []string) error {
    90  		if flagDomain == "" {
    91  			errPrintfln("%s", errMissingDomain)
    92  			return cmd.Usage()
    93  		}
    94  		ac := newAdminClient()
    95  		req := request.Options{
    96  			Method: "GET",
    97  			Path:   fmt.Sprintf("/instances/%s/feature/flags", flagDomain),
    98  		}
    99  		if len(args) > 0 {
   100  			req.Method = "PATCH"
   101  			req.Body = strings.NewReader(args[0])
   102  		}
   103  		res, err := ac.Req(&req)
   104  		if err != nil {
   105  			return err
   106  		}
   107  		defer res.Body.Close()
   108  		var obj map[string]json.RawMessage
   109  		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
   110  			return err
   111  		}
   112  		for k, v := range obj {
   113  			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
   114  		}
   115  		return nil
   116  	},
   117  }
   118  
   119  var featureSetCmd = &cobra.Command{
   120  	Use:   "sets",
   121  	Short: `Display and update the feature sets for an instance`,
   122  	Long: `
   123  cozy-stack feature sets displays the feature sets coming from the manager.
   124  
   125  It can also take a space-separated list of sets that will replace the previous
   126  list (no merge).
   127  
   128  All the sets can be removed by setting an empty list ('').
   129  `,
   130  	Example: `$ cozy-stack feature sets --domain cozy.localhost:8080 'set1 set2'`,
   131  	RunE: func(cmd *cobra.Command, args []string) error {
   132  		if flagDomain == "" {
   133  			errPrintfln("%s", errMissingDomain)
   134  			return cmd.Usage()
   135  		}
   136  		ac := newAdminClient()
   137  		req := request.Options{
   138  			Method: "GET",
   139  			Path:   fmt.Sprintf("/instances/%s/feature/sets", flagDomain),
   140  		}
   141  		if len(args) > 0 {
   142  			list := args
   143  			if len(args) == 1 {
   144  				list = strings.Fields(args[0])
   145  			}
   146  			buf, err := json.Marshal(list)
   147  			if err != nil {
   148  				return err
   149  			}
   150  			req.Method = "PUT"
   151  			req.Body = bytes.NewReader(buf)
   152  		}
   153  		res, err := ac.Req(&req)
   154  		if err != nil {
   155  			return err
   156  		}
   157  		defer res.Body.Close()
   158  		var sets []string
   159  		if err = json.NewDecoder(res.Body).Decode(&sets); err != nil {
   160  			return err
   161  		}
   162  		for _, set := range sets {
   163  			fmt.Fprintf(os.Stdout, "- %v\n", set)
   164  		}
   165  		return nil
   166  	},
   167  }
   168  
   169  var featureRatioCmd = &cobra.Command{
   170  	Use:     "ratio <context-name>",
   171  	Aliases: []string{"context"},
   172  	Short:   `Display and update the feature flags for a context`,
   173  	Long: `
   174  cozy-stack feature ratio displays the feature flags for a context.
   175  
   176  It can also create, update, or remove flags (with a ratio and value).
   177  
   178  To remove a flag, set it to an empty array (or null).
   179  `,
   180  	Example: `$ cozy-stack feature ratio --context beta '{"set_this_flag": [{"ratio": 0.1, "value": 1}, {"ratio": 0.9, "value": 2}] }'`,
   181  	RunE: func(cmd *cobra.Command, args []string) error {
   182  		if flagContext == "" {
   183  			return cmd.Usage()
   184  		}
   185  		ac := newAdminClient()
   186  		req := request.Options{
   187  			Method: "GET",
   188  			Path:   fmt.Sprintf("/instances/feature/contexts/%s", flagContext),
   189  		}
   190  		if len(args) > 0 {
   191  			req.Method = "PATCH"
   192  			req.Body = strings.NewReader(args[0])
   193  		}
   194  		res, err := ac.Req(&req)
   195  		if err != nil {
   196  			return err
   197  		}
   198  		defer res.Body.Close()
   199  		var obj map[string]json.RawMessage
   200  		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
   201  			return err
   202  		}
   203  		for k, v := range obj {
   204  			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
   205  		}
   206  		return nil
   207  	},
   208  }
   209  
   210  var featureConfigCmd = &cobra.Command{
   211  	Use:   "config <context-name>",
   212  	Short: `Display the feature flags from configuration for a context`,
   213  	Long: `
   214  cozy-stack feature config displays the feature flags from configuration for a context.
   215  
   216  These flags are read only and can only be updated by changing configuration and restarting the stack.
   217  `,
   218  	Example: `$ cozy-stack feature config --context beta`,
   219  	RunE: func(cmd *cobra.Command, args []string) error {
   220  		if flagContext == "" {
   221  			return cmd.Usage()
   222  		}
   223  		ac := newAdminClient()
   224  		req := request.Options{
   225  			Method: "GET",
   226  			Path:   fmt.Sprintf("/instances/feature/config/%s", flagContext),
   227  		}
   228  		res, err := ac.Req(&req)
   229  		if err != nil {
   230  			return err
   231  		}
   232  		defer res.Body.Close()
   233  		var obj map[string]json.RawMessage
   234  		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
   235  			return err
   236  		}
   237  		for k, v := range obj {
   238  			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
   239  		}
   240  		return nil
   241  	},
   242  }
   243  
   244  var featureDefaultCmd = &cobra.Command{
   245  	Use:   "defaults",
   246  	Short: `Display and update the default values for feature flags`,
   247  	Long: `
   248  cozy-stack feature defaults displays the default values for feature flags.
   249  
   250  It can also take a list of flags to update.
   251  
   252  If you give a null value, the flag will be removed.
   253  `,
   254  	Example: `$ cozy-stack feature defaults '{"add_this_flag": true, "remove_this_flag": null}'`,
   255  	RunE: func(cmd *cobra.Command, args []string) error {
   256  		ac := newAdminClient()
   257  		req := request.Options{
   258  			Method: "GET",
   259  			Path:   "/instances/feature/defaults",
   260  		}
   261  		if len(args) > 0 {
   262  			req.Method = "PATCH"
   263  			req.Body = strings.NewReader(args[0])
   264  		}
   265  		res, err := ac.Req(&req)
   266  		if err != nil {
   267  			return err
   268  		}
   269  		defer res.Body.Close()
   270  		var obj map[string]json.RawMessage
   271  		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
   272  			return err
   273  		}
   274  		for k, v := range obj {
   275  			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
   276  		}
   277  		return nil
   278  	},
   279  }
   280  
   281  func init() {
   282  	featureShowCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance")
   283  	featureShowCmd.Flags().BoolVar(&flagWithSources, "source", false, "Show the sources of the feature flags")
   284  	featureFlagCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance")
   285  	featureSetCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance")
   286  	featureRatioCmd.Flags().StringVar(&flagContext, "context", "", "The context for the feature flags")
   287  	featureConfigCmd.Flags().StringVar(&flagContext, "context", "", "The context for the feature flags")
   288  
   289  	featureCmdGroup.AddCommand(featureShowCmd)
   290  	featureCmdGroup.AddCommand(featureFlagCmd)
   291  	featureCmdGroup.AddCommand(featureSetCmd)
   292  	featureCmdGroup.AddCommand(featureRatioCmd)
   293  	featureCmdGroup.AddCommand(featureConfigCmd)
   294  	featureCmdGroup.AddCommand(featureDefaultCmd)
   295  	RootCmd.AddCommand(featureCmdGroup)
   296  }