mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
- project_variables table (secret entries encrypted at rest, masked in API)
- resolveServiceEnv resolves shared.* before service lookup
- GET|PUT /projects/{id}/variables; CLI 'variables shared list|set|unset';
MCP containr_shared_variables_{list,set}
- Settings tab → Shared Variables editor; env help text mentions shared refs
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
226 lines
6.3 KiB
Go
226 lines
6.3 KiB
Go
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 <service-id>",
|
|
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 <service-id> 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 <service-id> 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 <project-id>",
|
|
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 <project-id> 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 <project-id> 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)
|
|
}
|