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 }