mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
Rewrite the CLI as a thin client over the authenticated API so agents and scripts reach every resource the UI does (ROADMAP2 phase A2). - shared Client with bearer auth, error-envelope decoding, and stable exit codes: 0 ok / 1 error / 2 auth / 3 not-found / 4 validation / 5 unreachable - named profiles in ~/.containr.yaml (current_profile + profiles.*); resolution order --flag > env > profile > localhost default; CONTAINR_TOKEN / CONTAINR_API_URL / CONTAINR_PROFILE env support - --json prints raw API responses; --yes skips confirmations; destructive commands refuse non-interactively without it - auth login verifies the token against /user/profile before saving, interactive prompt via term.ReadPassword - commands for every resource: tokens, projects, services, deploy, deployments (logs/rollback), logs --follow (SSE), exec, variables (set/unset merge with secret masking preserved), databases (backup/restore/download), cron, templates, nodes + agent tokens, scaling, ha, security, gateway, admin, notifications - containr up: git remote + branch → create-or-reuse service → deploy Verified live: login/status/tokens/projects/services, up creating a service + deployment, non-TTY confirmation refusal, scope-capped admin rejection (exit 2), revoke. Authored By: TDvorak <[email protected]>
139 lines
3.6 KiB
Go
139 lines
3.6 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")
|
|
return mutateVars(args[0], 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
|
|
}
|
|
return mutateVars(args[0], 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 fetchVars(c *Client, serviceID string) ([]map[string]interface{}, error) {
|
|
data, err := c.Do("GET", "/services/"+serviceID+"/variables", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return unwrapList(data, "variables")
|
|
}
|
|
|
|
func mutateVars(serviceID string, mutate func([]map[string]interface{}) []map[string]interface{}) error {
|
|
c, err := client()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
vars, err := fetchVars(c, serviceID)
|
|
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,
|
|
})
|
|
}
|
|
data, err := c.Do("PUT", "/services/"+serviceID+"/variables", map[string]interface{}{"variables": payload})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if JSONMode() {
|
|
PrintRaw(data)
|
|
return nil
|
|
}
|
|
fmt.Printf("Variables updated on %s — redeploy to apply to running containers\n", serviceID)
|
|
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")
|
|
VariablesCmd.AddCommand(varsListCmd, varsSetCmd, varsUnsetCmd)
|
|
}
|