package commands import ( "fmt" "strings" "github.com/spf13/cobra" ) // VariablesCmd manages service environment variables. var VariablesCmd = &cobra.Command{ Use: "variables", Aliases: []string{"vars", "env"}, Short: "Manage service environment variables", } var varsListCmd = &cobra.Command{ Use: "list ", Short: "List variables (secrets shown masked)", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { c, err := client() if err != nil { return err } data, err := c.Do("GET", "/services/"+args[0]+"/variables", nil) if err != nil { return err } items, err := unwrapList(data, "variables") if err != nil { return err } rows := make([][]string, 0, len(items)) for _, v := range items { rows = append(rows, []string{str(v, "key"), str(v, "value"), str(v, "is_secret")}) } printRows(data, []string{"KEY", "VALUE", "SECRET"}, rows) return nil }, } // Variables are replaced wholesale server-side — set/unset fetch the // current list, mutate it, and PUT it back. Masked secret placeholders are // preserved by the server. var varsSetCmd = &cobra.Command{ Use: "set KEY=VALUE [KEY=VALUE...]", Short: "Set one or more variables", Args: cobra.MinimumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { secret, _ := cmd.Flags().GetBool("secret") redeploy, _ := cmd.Flags().GetBool("redeploy") return mutateVars(serviceVarsPath(args[0]), args[0], redeploy, func(vars []map[string]interface{}) []map[string]interface{} { for _, pair := range args[1:] { k, v, found := strings.Cut(pair, "=") if !found || k == "" { continue } vars = upsertVar(vars, k, v, secret) } return vars }) }, } var varsUnsetCmd = &cobra.Command{ Use: "unset KEY [KEY...]", Short: "Remove variables", Args: cobra.MinimumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { drop := map[string]bool{} for _, k := range args[1:] { drop[k] = true } redeploy, _ := cmd.Flags().GetBool("redeploy") return mutateVars(serviceVarsPath(args[0]), args[0], redeploy, func(vars []map[string]interface{}) []map[string]interface{} { out := vars[:0] for _, v := range vars { if !drop[str(v, "key")] { out = append(out, v) } } return out }) }, } // Shared variables — project scope, referenced as ${{shared.KEY}}. var varsSharedCmd = &cobra.Command{ Use: "shared", Short: "Manage project-level shared variables (${{shared.KEY}})", } var sharedVarsListCmd = &cobra.Command{ Use: "list ", Short: "List shared project variables (secrets shown masked)", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { c, err := client() if err != nil { return err } data, err := c.Do("GET", projectVarsPath(args[0]), nil) if err != nil { return err } items, err := unwrapList(data, "variables") if err != nil { return err } rows := make([][]string, 0, len(items)) for _, v := range items { rows = append(rows, []string{str(v, "key"), str(v, "value"), str(v, "is_secret")}) } printRows(data, []string{"KEY", "VALUE", "SECRET"}, rows) return nil }, } var sharedVarsSetCmd = &cobra.Command{ Use: "set KEY=VALUE [KEY=VALUE...]", Short: "Set shared project variables", Args: cobra.MinimumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { secret, _ := cmd.Flags().GetBool("secret") return mutateVars(projectVarsPath(args[0]), args[0], false, func(vars []map[string]interface{}) []map[string]interface{} { for _, pair := range args[1:] { k, v, found := strings.Cut(pair, "=") if !found || k == "" { continue } vars = upsertVar(vars, k, v, secret) } return vars }) }, } var sharedVarsUnsetCmd = &cobra.Command{ Use: "unset KEY [KEY...]", Short: "Remove shared project variables", Args: cobra.MinimumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { drop := map[string]bool{} for _, k := range args[1:] { drop[k] = true } return mutateVars(projectVarsPath(args[0]), args[0], false, func(vars []map[string]interface{}) []map[string]interface{} { out := vars[:0] for _, v := range vars { if !drop[str(v, "key")] { out = append(out, v) } } return out }) }, } func serviceVarsPath(serviceID string) string { return "/services/" + serviceID + "/variables" } func projectVarsPath(projectID string) string { return "/projects/" + projectID + "/variables" } func mutateVars(path, label string, redeploy bool, mutate func([]map[string]interface{}) []map[string]interface{}) error { c, err := client() if err != nil { return err } data, err := c.Do("GET", path, nil) if err != nil { return err } vars, err := unwrapList(data, "variables") if err != nil { return err } vars = mutate(vars) payload := make([]map[string]interface{}, 0, len(vars)) for _, v := range vars { payload = append(payload, map[string]interface{}{ "key": str(v, "key"), "value": str(v, "value"), "is_secret": v["is_secret"] == true, }) } body := map[string]interface{}{"variables": payload} if redeploy { body["redeploy"] = true } data, err = c.Do("PUT", path, body) if err != nil { return err } if JSONMode() { PrintRaw(data) return nil } obj, _ := unwrapObject(data) if obj["redeploy_queued"] == true { fmt.Printf("Variables updated on %s — redeploy queued\n", label) } else { fmt.Printf("Variables updated on %s — redeploy to apply to running containers\n", label) } return nil } func upsertVar(vars []map[string]interface{}, key, value string, secret bool) []map[string]interface{} { for _, v := range vars { if str(v, "key") == key { v["value"] = value v["is_secret"] = secret return vars } } return append(vars, map[string]interface{}{"key": key, "value": value, "is_secret": secret}) } func init() { varsSetCmd.Flags().Bool("secret", false, "mark the variable(s) as secret") varsSetCmd.Flags().Bool("redeploy", false, "redeploy the service after saving") varsUnsetCmd.Flags().Bool("redeploy", false, "redeploy the service after saving") sharedVarsSetCmd.Flags().Bool("secret", false, "mark the variable(s) as secret") varsSharedCmd.AddCommand(sharedVarsListCmd, sharedVarsSetCmd, sharedVarsUnsetCmd) VariablesCmd.AddCommand(varsListCmd, varsSetCmd, varsUnsetCmd, varsSharedCmd) }