| index 749f32fc7..5d6aa0ec2 100644 |
| --- a/appconfig_cli.go |
| +++ b/appconfig_cli.go |
| @@ -26,6 +26,7 @@ commands: |
| list -app <a> [-environment <name>] list an app's config vars |
| environments -app <a> list an app's named environment bundles |
| exec-delivery -app <a> file|off opt an app's TCP-addr exec routes into config delivery (issue #18) |
| + sync -app <a> request immediate config convergence (issue #44) |
| |
| -environment names a bundle at apps/<acct>/<app>/environments/<name>.json, |
| layered on top of the default bundle (config.json) at materialization time — |
| @@ -40,6 +41,17 @@ exactly as it already does for socket-exec routes, but those routes are |
| NEVER bounced: carlos does not own the process, so the app's own supervisor |
| owns the restart. "off" clears the field; a plain "env list" mentions it |
| when set. |
| + |
| +sync touches apps/<acct>/<app>/config-gen.json — the agent's fast poll loop |
| +(host package) notices within ~3s and re-renders exec-backed instances; |
| +unit-backed routes still wait for the next root adopt pass. It never |
| +restarts a box's shared edge service, only the one app's own delivery. |
| + |
| +Every command above also works through a logged-in console |
| +(carlos auth login) when CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR are |
| +unset — [-console <origin>] picks which logged-in console when more than one |
| +is configured; the store env vars win outright when set (direct-bucket |
| +mode, unchanged). |
| ` |
| |
| // RunEnv is the `carlos env` verb. |
| @@ -59,6 +71,8 @@ func RunEnv(args []string) { |
| runEnvEnvironments(args[1:]) |
| case "exec-delivery": |
| runEnvExecDelivery(args[1:]) |
| + case "sync": |
| + runEnvSync(args[1:]) |
| default: |
| fmt.Fprint(os.Stderr, envUsage) |
| os.Exit(1) |
| @@ -177,16 +191,36 @@ func runEnvSet(args []string) { |
| fs := flag.NewFlagSet("env set", flag.ExitOnError) |
| app := fs.String("app", "", "app name") |
| environment := fs.String("environment", "", "named environment bundle to set in (omit for the default bundle)") |
| - _, acctOf := accountFlags(fs) |
| + acctRaw, acctOf := accountFlags(fs) |
| + consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| if *app == "" { |
| - log.Fatal("usage: carlos env set -app <a> [-environment <name>] KEY=value [KEY2=value2 ...]") |
| + log.Fatal("usage: carlos env set -app <a> [-environment <name>] [-console <origin>] KEY=value [KEY2=value2 ...]") |
| } |
| checkEnvironmentFlag(*environment) |
| kv, err := parseKV(fs.Args()) |
| if err != nil { |
| log.Fatalf("env set: %v", err) |
| } |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess != nil { |
| + fmt.Fprintf(os.Stdout, "using %s as %s\n", sess.console, sess.address) |
| + set := make(map[string]appconfig.Var, len(kv)) |
| + for k, v := range kv { |
| + set[k] = appconfig.Var{Kind: "env", Value: v} |
| + } |
| + if _, err := sess.configApply(context.Background(), *acctRaw, *app, |
| + apiConfigApply{Environment: *environment, Set: set}); err != nil { |
| + log.Fatal(err) |
| + } |
| + fmt.Printf("set %d env var(s) for %s (%s)\n", len(kv), *app, bundleLabel(*environment)) |
| + return |
| + } |
| + |
| if err := saveEnvVars(context.Background(), openStore(), acctOf(), *app, *environment, kv); err != nil { |
| log.Fatal(err) |
| } |
| @@ -197,12 +231,29 @@ func runEnvUnset(args []string) { |
| fs := flag.NewFlagSet("env unset", flag.ExitOnError) |
| app := fs.String("app", "", "app name") |
| environment := fs.String("environment", "", "named environment bundle to remove from (omit for the default bundle)") |
| - _, acctOf := accountFlags(fs) |
| + acctRaw, acctOf := accountFlags(fs) |
| + consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| if *app == "" || fs.NArg() == 0 { |
| - log.Fatal("usage: carlos env unset -app <a> [-environment <name>] KEY [KEY2 ...]") |
| + log.Fatal("usage: carlos env unset -app <a> [-environment <name>] [-console <origin>] KEY [KEY2 ...]") |
| } |
| checkEnvironmentFlag(*environment) |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess != nil { |
| + fmt.Fprintf(os.Stdout, "using %s as %s\n", sess.console, sess.address) |
| + removed, err := sess.configApply(context.Background(), *acctRaw, *app, |
| + apiConfigApply{Environment: *environment, Unset: fs.Args()}) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + fmt.Printf("unset %d of %d var(s) for %s (%s)\n", removed, fs.NArg(), *app, bundleLabel(*environment)) |
| + return |
| + } |
| + |
| removed, err := unsetVars(context.Background(), openStore(), acctOf(), *app, *environment, fs.Args()) |
| if err != nil { |
| log.Fatal(err) |
| @@ -214,36 +265,78 @@ func runEnvList(args []string) { |
| fs := flag.NewFlagSet("env list", flag.ExitOnError) |
| app := fs.String("app", "", "app name") |
| environment := fs.String("environment", "", "named environment bundle to list (omit for the default bundle)") |
| - _, acctOf := accountFlags(fs) |
| + acctRaw, acctOf := accountFlags(fs) |
| + consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| if *app == "" { |
| - log.Fatal("usage: carlos env list -app <a> [-environment <name>]") |
| + log.Fatal("usage: carlos env list -app <a> [-environment <name>] [-console <origin>]") |
| } |
| checkEnvironmentFlag(*environment) |
| - m, err := loadBundle(context.Background(), openStore(), acctOf(), *app, *environment) |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| if err != nil { |
| log.Fatal(err) |
| } |
| - fmt.Printf("%s (%s):\n", *app, bundleLabel(*environment)) |
| - // exec_delivery is app-level only (read from the default bundle |
| - // alone — see appconfig.Manifest.ExecDelivery) and never printed for |
| - // a named environment's listing, matching where it can ever actually |
| - // be SET (runEnvExecDelivery refuses -environment before it reaches |
| - // appconfig at all). |
| - if *environment == "" && m.ExecDelivery != "" { |
| - fmt.Printf("exec-delivery: %s\n", m.ExecDelivery) |
| + if sess != nil { |
| + fmt.Fprintf(os.Stdout, "using %s as %s\n", sess.console, sess.address) |
| + cfg, err := sess.configGet(context.Background(), *acctRaw, *app, *environment) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + printEnvList(*app, *environment, cfg.ExecDelivery, envVarsFromAPI(cfg.Vars)) |
| + return |
| + } |
| + |
| + m, err := loadBundle(context.Background(), openStore(), acctOf(), *app, *environment) |
| + if err != nil { |
| + log.Fatal(err) |
| } |
| - var keys []string |
| + envVars := map[string]string{} |
| for k, v := range m.Vars { |
| if v.Kind == "env" { |
| - keys = append(keys, k) |
| + envVars[k] = v.Value |
| } |
| } |
| + printEnvList(*app, *environment, m.ExecDelivery, envVars) |
| +} |
| + |
| +// envVarsFromAPI narrows an apiConfig's Vars to the kind=="env" rows, keyed |
| +// by name — configGet's response shape for the API branch of runEnvList, |
| +// matching the direct branch's own map[key]value shape so printEnvList |
| +// prints identically regardless of mode. |
| +func envVarsFromAPI(vars []apiConfigVar) map[string]string { |
| + out := map[string]string{} |
| + for _, v := range vars { |
| + if v.Kind == "env" { |
| + out[v.Key] = v.Value |
| + } |
| + } |
| + return out |
| +} |
| + |
| +// printEnvList renders `env list`'s output — header line, an exec-delivery |
| +// line when set and only for the default bundle (app-level only — see |
| +// appconfig.Manifest.ExecDelivery), then a sorted KEY/VALUE tabwriter — |
| +// byte-identical between the direct and API branches of runEnvList, which |
| +// differ only in HOW they fetch execDelivery/vars. |
| +func printEnvList(app, environment, execDelivery string, vars map[string]string) { |
| + fmt.Printf("%s (%s):\n", app, bundleLabel(environment)) |
| + // exec_delivery is app-level only and never printed for a named |
| + // environment's listing, matching where it can ever actually be SET |
| + // (runEnvExecDelivery refuses -environment before it reaches appconfig |
| + // at all). |
| + if environment == "" && execDelivery != "" { |
| + fmt.Printf("exec-delivery: %s\n", execDelivery) |
| + } |
| + var keys []string |
| + for k := range vars { |
| + keys = append(keys, k) |
| + } |
| sort.Strings(keys) |
| w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) |
| fmt.Fprintln(w, "KEY\tVALUE") |
| for _, k := range keys { |
| - fmt.Fprintf(w, "%s\t%s\n", k, m.Vars[k].Value) |
| + fmt.Fprintf(w, "%s\t%s\n", k, vars[k]) |
| } |
| w.Flush() |
| } |
| @@ -264,10 +357,11 @@ func runEnvExecDelivery(args []string) { |
| // below. There is no environment-scoped variant to route it to: |
| // appconfig.SetExecDelivery has no environment parameter at all. |
| environment := fs.String("environment", "", "refused: exec_delivery is app-level only (default bundle), never a named environment") |
| - _, acctOf := accountFlags(fs) |
| + acctRaw, acctOf := accountFlags(fs) |
| + consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| if *app == "" || fs.NArg() != 1 { |
| - log.Fatal("usage: carlos env exec-delivery -app <a> file|off") |
| + log.Fatal("usage: carlos env exec-delivery -app <a> [-console <origin>] file|off") |
| } |
| if *environment != "" { |
| log.Fatal(`carlos env exec-delivery: -environment is refused — exec_delivery is APP-LEVEL ONLY, read from the ` + |
| @@ -286,7 +380,26 @@ func runEnvExecDelivery(args []string) { |
| default: |
| log.Fatalf("carlos env exec-delivery: %q is not a valid value — want %q or \"off\"", fs.Arg(0), appconfig.ExecDeliveryFile) |
| } |
| - if err := appconfig.SetExecDelivery(context.Background(), openStore(), acctOf(), *app, value); err != nil { |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess != nil { |
| + fmt.Fprintf(os.Stdout, "using %s as %s\n", sess.console, sess.address) |
| + // The wire vocabulary is "file"/"off", never "" — value is "" for |
| + // the local SetExecDelivery convention (clearing the field), but |
| + // the console's PUT decodes ExecDelivery as a pointer where nil |
| + // means "no change", so clearing it must still be spelled "off". |
| + wire := "off" |
| + if value != "" { |
| + wire = value |
| + } |
| + if _, err := sess.configApply(context.Background(), *acctRaw, *app, |
| + apiConfigApply{ExecDelivery: &wire}); err != nil { |
| + log.Fatal(err) |
| + } |
| + } else if err := appconfig.SetExecDelivery(context.Background(), openStore(), acctOf(), *app, value); err != nil { |
| log.Fatal(err) |
| } |
| if value == "" { |
| @@ -299,11 +412,29 @@ func runEnvExecDelivery(args []string) { |
| func runEnvEnvironments(args []string) { |
| fs := flag.NewFlagSet("env environments", flag.ExitOnError) |
| app := fs.String("app", "", "app name") |
| - _, acctOf := accountFlags(fs) |
| + acctRaw, acctOf := accountFlags(fs) |
| + consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| if *app == "" { |
| - log.Fatal("usage: carlos env environments -app <a>") |
| + log.Fatal("usage: carlos env environments -app <a> [-console <origin>]") |
| } |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess != nil { |
| + fmt.Fprintf(os.Stdout, "using %s as %s\n", sess.console, sess.address) |
| + cfg, err := sess.configGet(context.Background(), *acctRaw, *app, "") |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + for _, n := range cfg.Environments { |
| + fmt.Println(n) |
| + } |
| + return |
| + } |
| + |
| names, err := appconfig.ListEnvironments(context.Background(), openStore(), acctOf(), *app) |
| if err != nil { |
| log.Fatal(err) |
| @@ -313,6 +444,41 @@ func runEnvEnvironments(args []string) { |
| } |
| } |
| |
| +// runEnvSync is `carlos env sync` (issue #44) — the escape hatch for a |
| +// config write that needs to reach a running instance faster than |
| +// configSyncLoop's hourly floor: it touches apps/<acct>/<app>/config-gen.json |
| +// (appconfig.GenKey), which the agent's configGenLoop polls every ~3s and |
| +// treats a moved etag as "run SyncExec now". It never restarts a box's |
| +// shared edge service and never targets a bundle directly — the generation |
| +// object covers every bundle an app has (default plus every environment) in |
| +// one touch, matching TouchGeneration's own "app granularity, not |
| +// per-bundle" design (generation.go). |
| +func runEnvSync(args []string) { |
| + fs := flag.NewFlagSet("env sync", flag.ExitOnError) |
| + app := fs.String("app", "", "app name") |
| + acctRaw, acctOf := accountFlags(fs) |
| + consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Parse(args) |
| + if *app == "" { |
| + log.Fatal("usage: carlos env sync -app <a> [-account <sqid-or-name>] [-console <origin>]") |
| + } |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess != nil { |
| + fmt.Fprintf(os.Stdout, "using %s as %s\n", sess.console, sess.address) |
| + if err := sess.configSync(context.Background(), *acctRaw, *app); err != nil { |
| + log.Fatal(err) |
| + } |
| + } else if err := appconfig.TouchGeneration(context.Background(), openStore(), acctOf(), *app); err != nil { |
| + log.Fatal(err) |
| + } |
| + fmt.Printf("config sync requested for %s — exec-backed instances converge within seconds; "+ |
| + "unit-backed routes on the next root adopt pass\n", *app) |
| +} |
| + |
| const secretsUsage = `usage: carlos secrets <command> |
| |
| commands: |
| @@ -325,6 +491,16 @@ commands: |
| see "carlos env" for the full explanation; the same bundle backs both env |
| vars and secrets, sealed or not. "carlos env environments -app <a>" lists |
| an app's bundle names. |
| + |
| +set/unset/list also work through a logged-in console (carlos auth login) |
| +when CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR are unset — |
| +[-console <origin>] picks which logged-in console when more than one is |
| +configured; the store env vars win outright when set (direct-bucket mode, |
| +unchanged). In that mode "secrets set" seals with CARLOS_SECRETS_PUBKEY if |
| +it's set in this terminal's own environment, otherwise the console's own |
| +served public key (Config.SecretsPub) if it has one — CARLOS_SECRETS_KEY |
| +(the PRIVATE half) never leaves the box holding it either way. genkey is |
| +always local, in both modes. |
| ` |
| |
| // RunSecrets is the `carlos secrets` verb. |
| @@ -358,31 +534,91 @@ func secretsPubKey() *ecdh.PublicKey { |
| return k |
| } |
| |
| +// secretsPubKeyAPI resolves the sealing key for API-mode `secrets set`: |
| +// CARLOS_SECRETS_PUBKEY in THIS terminal's own environment wins — matching |
| +// secretsPubKey's direct-mode rule exactly, so the same env var works |
| +// whichever mode a script happens to run in — otherwise the console's own |
| +// served public key (Config.SecretsPub, GET config's "secrets_pubkey" |
| +// field). That fallback is what makes `carlos auth login` alone sufficient |
| +// for `secrets set` (spec decision 2): public material by design, it only |
| +// encrypts. Neither present: a fatal naming both fixes — CARLOS_SECRETS_KEY |
| +// (the PRIVATE half) never leaves the box holding it, so there is no third |
| +// way to get a key here. |
| +func secretsPubKeyAPI(sess *apiSession, account, app string) *ecdh.PublicKey { |
| + if raw := os.Getenv("CARLOS_SECRETS_PUBKEY"); raw != "" { |
| + k, err := appconfig.ParsePublicKey(raw, "CARLOS_SECRETS_PUBKEY") |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + return k |
| + } |
| + cfg, err := sess.configGet(context.Background(), account, app, "") |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if cfg.SecretsPubkey == "" { |
| + log.Fatal("no CARLOS_SECRETS_PUBKEY in env and this console does not serve one — " + |
| + "set the env var, or set CARLOS_SECRETS_PUBKEY in the console's environment") |
| + } |
| + k, err := appconfig.ParsePublicKey(cfg.SecretsPubkey, "console secrets_pubkey") |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + return k |
| +} |
| + |
| func runSecretsSet(args []string) { |
| fs := flag.NewFlagSet("secrets set", flag.ExitOnError) |
| app := fs.String("app", "", "app name") |
| environment := fs.String("environment", "", "named environment bundle to set in (omit for the default bundle)") |
| - _, acctOf := accountFlags(fs) |
| + acctRaw, acctOf := accountFlags(fs) |
| + consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| if *app == "" { |
| - log.Fatal("usage: carlos secrets set -app <a> [-environment <name>] KEY=value [KEY2=value2 ...]") |
| + log.Fatal("usage: carlos secrets set -app <a> [-environment <name>] [-console <origin>] KEY=value [KEY2=value2 ...]") |
| } |
| checkEnvironmentFlag(*environment) |
| kv, err := parseKV(fs.Args()) |
| if err != nil { |
| log.Fatalf("secrets set: %v", err) |
| } |
| - pub := secretsPubKey() |
| - sealed := make(map[string]string, len(kv)) |
| + // Validate every value while the plaintext is still in hand, before |
| + // either branch below seals anything — after Seal the value is opaque |
| + // forever (`secrets list` never shows values), so a bad one would only |
| + // surface as a render failure in the hourly adopt log, stalling the |
| + // app's entire config with nothing for the operator to inspect. |
| for k, v := range kv { |
| - // Validate here, while the plaintext is still in hand — after Seal |
| - // the value is opaque forever (`secrets list` never shows values), so |
| - // a bad one would only surface as a render failure in the hourly |
| - // adopt log, stalling the app's entire config with nothing for the |
| - // operator to inspect. |
| if err := appconfig.ValidVarValue(v); err != nil { |
| log.Fatalf("secrets set: %s: %v", k, err) |
| } |
| + } |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess != nil { |
| + fmt.Fprintf(os.Stdout, "using %s as %s\n", sess.console, sess.address) |
| + pub := secretsPubKeyAPI(sess, *acctRaw, *app) |
| + set := make(map[string]appconfig.Var, len(kv)) |
| + for k, v := range kv { |
| + env, err := appconfig.Seal(pub, []byte(v)) |
| + if err != nil { |
| + log.Fatalf("seal %s: %v", k, err) |
| + } |
| + set[k] = appconfig.Var{Kind: "secret", Envelope: env} |
| + } |
| + if _, err := sess.configApply(context.Background(), *acctRaw, *app, |
| + apiConfigApply{Environment: *environment, Set: set}); err != nil { |
| + log.Fatal(err) |
| + } |
| + fmt.Printf("set %d secret(s) for %s (%s)\n", len(kv), *app, bundleLabel(*environment)) |
| + return |
| + } |
| + |
| + pub := secretsPubKey() |
| + sealed := make(map[string]string, len(kv)) |
| + for k, v := range kv { |
| env, err := appconfig.Seal(pub, []byte(v)) |
| if err != nil { |
| log.Fatalf("seal %s: %v", k, err) |
| @@ -399,12 +635,29 @@ func runSecretsUnset(args []string) { |
| fs := flag.NewFlagSet("secrets unset", flag.ExitOnError) |
| app := fs.String("app", "", "app name") |
| environment := fs.String("environment", "", "named environment bundle to remove from (omit for the default bundle)") |
| - _, acctOf := accountFlags(fs) |
| + acctRaw, acctOf := accountFlags(fs) |
| + consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| if *app == "" || fs.NArg() == 0 { |
| - log.Fatal("usage: carlos secrets unset -app <a> [-environment <name>] KEY [KEY2 ...]") |
| + log.Fatal("usage: carlos secrets unset -app <a> [-environment <name>] [-console <origin>] KEY [KEY2 ...]") |
| } |
| checkEnvironmentFlag(*environment) |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess != nil { |
| + fmt.Fprintf(os.Stdout, "using %s as %s\n", sess.console, sess.address) |
| + removed, err := sess.configApply(context.Background(), *acctRaw, *app, |
| + apiConfigApply{Environment: *environment, Unset: fs.Args()}) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + fmt.Printf("unset %d of %d secret(s) for %s (%s)\n", removed, fs.NArg(), *app, bundleLabel(*environment)) |
| + return |
| + } |
| + |
| removed, err := unsetVars(context.Background(), openStore(), acctOf(), *app, *environment, fs.Args()) |
| if err != nil { |
| log.Fatal(err) |
| @@ -412,31 +665,61 @@ func runSecretsUnset(args []string) { |
| fmt.Printf("unset %d of %d secret(s) for %s (%s)\n", removed, fs.NArg(), *app, bundleLabel(*environment)) |
| } |
| |
| +// printSecretsList renders `secrets list`'s output — header line, then |
| +// sorted key names only, never values — byte-identical between the direct |
| +// and API branches of runSecretsList, which differ only in HOW they fetch |
| +// the key set. |
| +func printSecretsList(app, environment string, keys []string) { |
| + fmt.Printf("%s (%s):\n", app, bundleLabel(environment)) |
| + sort.Strings(keys) |
| + for _, k := range keys { |
| + fmt.Println(k) |
| + } |
| +} |
| + |
| func runSecretsList(args []string) { |
| fs := flag.NewFlagSet("secrets list", flag.ExitOnError) |
| app := fs.String("app", "", "app name") |
| environment := fs.String("environment", "", "named environment bundle to list (omit for the default bundle)") |
| - _, acctOf := accountFlags(fs) |
| + acctRaw, acctOf := accountFlags(fs) |
| + consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| if *app == "" { |
| - log.Fatal("usage: carlos secrets list -app <a> [-environment <name>]") |
| + log.Fatal("usage: carlos secrets list -app <a> [-environment <name>] [-console <origin>]") |
| } |
| checkEnvironmentFlag(*environment) |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess != nil { |
| + fmt.Fprintf(os.Stdout, "using %s as %s\n", sess.console, sess.address) |
| + cfg, err := sess.configGet(context.Background(), *acctRaw, *app, *environment) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + var keys []string |
| + for _, v := range cfg.Vars { |
| + if v.Kind == "secret" { |
| + keys = append(keys, v.Key) |
| + } |
| + } |
| + printSecretsList(*app, *environment, keys) |
| + return |
| + } |
| + |
| m, err := loadBundle(context.Background(), openStore(), acctOf(), *app, *environment) |
| if err != nil { |
| log.Fatal(err) |
| } |
| - fmt.Printf("%s (%s):\n", *app, bundleLabel(*environment)) |
| var keys []string |
| for k, v := range m.Vars { |
| if v.Kind == "secret" { |
| keys = append(keys, k) |
| } |
| } |
| - sort.Strings(keys) |
| - for _, k := range keys { |
| - fmt.Println(k) |
| - } |
| + printSecretsList(*app, *environment, keys) |
| } |
| |
| func runSecretsGenkey(args []string) { |