@@ -63,6 +63,37 @@ func (rc *ResolvedConfig) HasFlag(name string) bool {
return false
}
+// commandSuffix is the suffix keld recognises in .environ keys to indicate
+// that the value is a shell command whose stdout should be captured and
+// used as the actual variable value (with the suffix stripped).
+//
+// For example, RESTIC_REPOSITORY_COMMAND = "op read ..." resolves to
+// RESTIC_REPOSITORY at execution time.
+const commandSuffix = "_COMMAND"
+
+// HasEnvSource reports whether the given environment variable will have
+// a value when the restic process runs. It checks three sources in
+// order:
+//
+// 1. Direct key in [ResolvedConfig.Environ] (e.g. RESTIC_REPOSITORY)
+// 2. _COMMAND variant in Environ (e.g. RESTIC_REPOSITORY_COMMAND),
+// which keld resolves before exec
+// 3. Process environment via [os.Getenv]
+//
+// This centralises the _COMMAND convention so callers never need to
+// check for it themselves.
+func (rc *ResolvedConfig) HasEnvSource(key string) bool {
+ if rc.Environ != nil {
+ if _, ok := rc.Environ[key]; ok {
+ return true
+ }
+ if _, ok := rc.Environ[key+commandSuffix]; ok {
+ return true
+ }
+ }
+ return os.Getenv(key) != ""
+}
+
// rawConfig is the entire parsed TOML file as nested string-keyed maps.
type rawConfig map[string]any
@@ -0,0 +1,87 @@
+package config
+
+import (
+ "testing"
+)
+
+func TestHasEnvSource(t *testing.T) {
+ tests := []struct {
+ name string
+ environ map[string]string
+ envKey string
+ procEnv map[string]string // simulated process env
+ want bool
+ }{
+ {
+ name: "direct key present",
+ environ: map[string]string{"RESTIC_REPOSITORY": "s3:bucket"},
+ envKey: "RESTIC_REPOSITORY",
+ want: true,
+ },
+ {
+ name: "command variant present",
+ environ: map[string]string{"RESTIC_REPOSITORY_COMMAND": "op read ..."},
+ envKey: "RESTIC_REPOSITORY",
+ want: true,
+ },
+ {
+ name: "neither direct nor command",
+ environ: map[string]string{"OTHER_VAR": "value"},
+ envKey: "RESTIC_REPOSITORY",
+ want: false,
+ },
+ {
+ name: "nil environ",
+ environ: nil,
+ envKey: "RESTIC_REPOSITORY",
+ want: false,
+ },
+ {
+ name: "empty string value still counts as present",
+ environ: map[string]string{"RESTIC_REPOSITORY": ""},
+ envKey: "RESTIC_REPOSITORY",
+ want: true,
+ },
+ {
+ name: "command variant for non-restic key",
+ environ: map[string]string{"AWS_ACCESS_KEY_ID_COMMAND": "op read ..."},
+ envKey: "AWS_ACCESS_KEY_ID",
+ want: true,
+ },
+ {
+ name: "process env fallback",
+ environ: nil,
+ envKey: "KELD_TEST_HAS_ENV_SOURCE",
+ procEnv: map[string]string{"KELD_TEST_HAS_ENV_SOURCE": "from-process"},
+ want: true,
+ },
+ {
+ name: "process env empty when not set",
+ environ: nil,
+ envKey: "KELD_TEST_HAS_ENV_SOURCE",
+ want: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Tests using t.Setenv cannot run in parallel.
+ if len(tt.procEnv) == 0 {
+ t.Parallel()
+ }
+
+ for k, v := range tt.procEnv {
+ t.Setenv(k, v)
+ }
+
+ cfg := &ResolvedConfig{
+ Environ: tt.environ,
+ }
+
+ got := cfg.HasEnvSource(tt.envKey)
+ if got != tt.want {
+ t.Errorf("HasEnvSource(%q) = %v, want %v", tt.envKey, got, tt.want)
+ }
+ })
+ }
+}
@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"maps"
- "os"
"os/exec"
"sort"
@@ -51,8 +50,7 @@ var globalFlags = map[string]bool{
// hasRepoSource reports whether a repository location is available from
// any source: CLI flags in the resolved config, the config's environ
-// section, or the process environment (e.g. RESTIC_REPOSITORY already
-// set in the user's shell).
+// section (including _COMMAND variants), or the process environment.
func hasRepoSource(cfg *config.ResolvedConfig) bool {
for _, f := range cfg.Flags {
switch f.Name {
@@ -60,20 +58,8 @@ func hasRepoSource(cfg *config.ResolvedConfig) bool {
return true
}
}
- if cfg.Environ != nil {
- if _, ok := cfg.Environ["RESTIC_REPOSITORY"]; ok {
- return true
- }
- if _, ok := cfg.Environ["RESTIC_REPOSITORY_FILE"]; ok {
- return true
- }
- }
- // Check the process environment as a last resort — the user may
- // have RESTIC_REPOSITORY set in their shell outside of keld config.
- if os.Getenv("RESTIC_REPOSITORY") != "" || os.Getenv("RESTIC_REPOSITORY_FILE") != "" {
- return true
- }
- return false
+ return cfg.HasEnvSource("RESTIC_REPOSITORY") ||
+ cfg.HasEnvSource("RESTIC_REPOSITORY_FILE")
}
// buildSnapshotCmd constructs the argument vector for running