diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b158cb3..6d2fe51 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,7 +54,15 @@ jobs: - name: ⏬ Install Dependencies run: go get . + - name: ⬇️ Fetch `env.reference.yaml` from `ws-meta` + run: | + curl -fsSL \ + https://raw.githubusercontent.com/kloudkit/ws-meta/main/shared/workspace/env.reference.yaml \ + -o "${{ runner.temp }}/env.reference.yaml" + - name: 🧪 Test + env: + WS__INTERNAL_ENV_REFERENCE: ${{ runner.temp }}/env.reference.yaml run: go test -v ./... - name: 👷‍♂️ Build for ${{ matrix.arch }} diff --git a/.gitignore b/.gitignore index 2eb7d12..c22f61f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +CLAUDE.md + ws-cli /build diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 7e160df..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,25 +0,0 @@ -# Requirements - -1. **Separation of concerns** - Keep argument/flag parsing at the edges. Commands should delegate to exported functions that contain the business logic (MVC-inspired: parsing ≠ logic). - -2. **Command tree wiring** - The root command registers its direct children (e.g., `rootCmd.AddCommand(log.LogCmd)`), and each child registers its own subcommands. - -3. **Pragmatic structure** - Avoid over-engineering: no DI frameworks and don’t create `internal/*` packages for every command. Place shared logic in importable modules so it’s reusable and testable without duplication. - -4. **Dependency policy** - Prefer native/standard library solutions over third-party packages whenever possible. - -5. **Testing** - For tests, use the `gotest.tools/v3/assert` library instead of `if/fail` conditions. - -6. **Backwards compatibility** - This is not a public library; legacy compatibility isn’t required. - -7. **CLI UX** - Add colorized output to make the CLI more user-friendly. - -8. **Comments** - Do not not add comments unless specifically instructed diff --git a/cmd/feature/feature.go b/cmd/feature/feature.go index e6f3ce7..8163cf0 100644 --- a/cmd/feature/feature.go +++ b/cmd/feature/feature.go @@ -2,7 +2,6 @@ package feature import ( "github.com/kloudkit/ws-cli/internals/config" - "github.com/kloudkit/ws-cli/internals/env" "github.com/spf13/cobra" ) @@ -12,9 +11,11 @@ var FeatureCmd = &cobra.Command{ } func init() { + root, _ := config.Resolve("features", "dir") + FeatureCmd.PersistentFlags().String( "root", - env.String(config.EnvFeaturesDir, config.DefaultFeaturesDir), + root, "Root directory of additional features", ) diff --git a/cmd/feature/store.go b/cmd/feature/store.go index 403f6b2..f6942d4 100644 --- a/cmd/feature/store.go +++ b/cmd/feature/store.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/kloudkit/ws-cli/internals/config" - "github.com/kloudkit/ws-cli/internals/env" "github.com/kloudkit/ws-cli/internals/features" "github.com/kloudkit/ws-cli/internals/styles" "github.com/spf13/cobra" @@ -14,7 +13,7 @@ var storeCmd = &cobra.Command{ Use: "store", Short: "List packages available in the feature store", RunE: func(cmd *cobra.Command, args []string) error { - storeURL := env.String(config.EnvFeaturesStoreURL) + storeURL, _ := config.Resolve("features", "store_url") if storeURL == "" { styles.PrintWarning(cmd.OutOrStdout(), "Feature store not configured (set WS_FEATURES_STORE_URL)") return nil diff --git a/cmd/info/version.go b/cmd/info/version.go index 97bf617..ef8462b 100644 --- a/cmd/info/version.go +++ b/cmd/info/version.go @@ -1,3 +1,3 @@ package info -var Version = "0.0.50" +var Version = "0.0.51" diff --git a/cmd/root.go b/cmd/root.go index 4a68d6f..3a9ad57 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,6 +14,7 @@ import ( "github.com/kloudkit/ws-cli/cmd/serve" "github.com/kloudkit/ws-cli/cmd/show" "github.com/kloudkit/ws-cli/cmd/template" + "github.com/kloudkit/ws-cli/internals/config" "github.com/kloudkit/ws-cli/internals/styles" "github.com/spf13/cobra" ) @@ -24,6 +25,13 @@ var rootCmd = &cobra.Command{ Version: "v" + info.Version, Aliases: []string{"ws"}, SilenceErrors: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := config.RequireWorkspace(); err != nil { + return err + } + _, err := config.LoadEnvReference() + return err + }, } func Execute() { diff --git a/cmd/show/env.go b/cmd/show/env.go new file mode 100644 index 0000000..69777c5 --- /dev/null +++ b/cmd/show/env.go @@ -0,0 +1,122 @@ +package show + +import ( + "fmt" + "os" + + "github.com/kloudkit/ws-cli/internals/config" + "github.com/kloudkit/ws-cli/internals/styles" + "github.com/spf13/cobra" +) + +var osExit = os.Exit + +var envCmd = &cobra.Command{ + Use: "env ", + Short: "Display the resolved value of a workspace environment variable", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + key := args[0] + + asList, _ := cmd.Flags().GetBool("list") + asBool, _ := cmd.Flags().GetBool("bool") + asInt, _ := cmd.Flags().GetBool("int") + asCheck, _ := cmd.Flags().GetBool("check") + raw, _ := cmd.Flags().GetBool("raw") + delimiter, _ := cmd.Flags().GetString("delimiter") + deprecated, _ := cmd.Flags().GetString("deprecated") + + switch { + case asCheck: + return runCheck(cmd, key, deprecated) + case asBool: + return runBool(cmd, key) + case asInt: + return runInt(cmd, key) + case asList: + return runList(cmd, key, delimiter) + } + + value, err := config.ResolveKey(key) + if err != nil { + return err + } + + if styles.OutputRaw(cmd.OutOrStdout(), raw, value) { + return nil + } + + styles.PrintTitle(cmd.OutOrStdout(), "Workspace Environment") + styles.PrintKeyCode(cmd.OutOrStdout(), key, value) + + return nil + }, +} + +func runCheck(cmd *cobra.Command, preferred, deprecated string) error { + switch config.Check(preferred, deprecated) { + case config.CheckPreferredSet: + return nil + case config.CheckDeprecatedOnly: + fmt.Fprintln(cmd.ErrOrStderr(), config.DeprecationLine(deprecated, preferred)) + osExit(1) + case config.CheckBothSet: + fmt.Fprintln(cmd.ErrOrStderr(), config.BothSetLine(deprecated, preferred)) + osExit(2) + case config.CheckUnset: + osExit(1) + } + return nil +} + +func runBool(_ *cobra.Command, key string) error { + value, err := config.ResolveKey(key) + if err != nil { + return err + } + parsed, err := config.ParseBool(value) + if err != nil { + return err + } + if !parsed { + osExit(1) + } + return nil +} + +func runInt(cmd *cobra.Command, key string) error { + value, err := config.ResolveKey(key) + if err != nil { + return err + } + parsed, err := config.ParseInt(value) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), parsed) + return nil +} + +func runList(cmd *cobra.Command, key, delimiter string) error { + items, err := config.ResolveListKey(key, delimiter) + if err != nil { + return err + } + for _, item := range items { + fmt.Fprintln(cmd.OutOrStdout(), item) + } + return nil +} + +func init() { + envCmd.Flags().Bool("list", false, "Output as newline-separated list (uses YAML delimiter or --delimiter)") + envCmd.Flags().Bool("bool", false, "Coerce to boolean; exit 0 truthy, 1 falsy, 2 invalid") + envCmd.Flags().Bool("int", false, "Coerce to integer; print canonical form or fail with exit 2") + envCmd.Flags().Bool("check", false, "Check whether the variable (or its --deprecated alias) is set") + envCmd.Flags().String("delimiter", "", "Override delimiter for --list (defaults to YAML delimiter or space)") + envCmd.Flags().String("deprecated", "", "Deprecated alias paired with --check") + + envCmd.MarkFlagsMutuallyExclusive("list", "bool", "int", "check") + + ShowCmd.AddCommand(envCmd) +} diff --git a/cmd/show/path.go b/cmd/show/path.go index 7cabdf8..b927674 100644 --- a/cmd/show/path.go +++ b/cmd/show/path.go @@ -2,7 +2,6 @@ package show import ( "github.com/kloudkit/ws-cli/internals/config" - "github.com/kloudkit/ws-cli/internals/env" "github.com/kloudkit/ws-cli/internals/path" "github.com/kloudkit/ws-cli/internals/styles" "github.com/spf13/cobra" @@ -17,7 +16,7 @@ var pathHomeCmd = &cobra.Command{ Use: "home", Short: "Display the workspace home path", RunE: func(cmd *cobra.Command, args []string) error { - homePath := env.String(config.EnvServerRoot, config.DefaultServerRoot) + homePath := config.MustResolve("server", "root") raw, _ := cmd.Flags().GetBool("raw") if styles.OutputRaw(cmd.OutOrStdout(), raw, homePath) { diff --git a/cmd/show/show_test.go b/cmd/show/show_test.go index 2c9e5ff..f8d3b8d 100644 --- a/cmd/show/show_test.go +++ b/cmd/show/show_test.go @@ -2,22 +2,77 @@ package show import ( "bytes" + "os" + "path/filepath" "strings" "testing" - "github.com/kloudkit/ws-cli/internals/config" + "github.com/spf13/pflag" "gotest.tools/v3/assert" ) +const envFixture = ` +envs: + server: + properties: + root: + type: string + default: /workspace + metrics: + properties: + port: + type: integer + default: 9100 + features: + properties: + additional_features: + type: string + default: null + delimiter: " " +deprecated: + WS_PORT: + use: WS_SERVER_PORT +` + +func _installEnvFixture(t *testing.T) { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "env.reference.yaml") + assert.NilError(t, os.WriteFile(path, []byte(envFixture), 0644)) + t.Setenv("WS__INTERNAL_ENV_REFERENCE", path) +} + +func _runShow(t *testing.T, args ...string) (stdout, stderr string, exit int) { + t.Helper() + exit = 0 + original := osExit + osExit = func(code int) { exit = code } + t.Cleanup(func() { osExit = original }) + + envCmd.Flags().VisitAll(func(f *pflag.Flag) { + f.Changed = false + _ = f.Value.Set(f.DefValue) + }) + + var outBuf, errBuf bytes.Buffer + ShowCmd.SetOut(&outBuf) + ShowCmd.SetErr(&errBuf) + ShowCmd.SetArgs(args) + _ = ShowCmd.Execute() + return outBuf.String(), errBuf.String(), exit +} + func TestPathHome(t *testing.T) { t.Run("WithEnv", func(t *testing.T) { - t.Setenv(config.EnvServerRoot, "/app") + _installEnvFixture(t) + t.Setenv("WS_SERVER_ROOT", "/app") assertOutputContains(t, []string{"path", "home"}, "/app") }) t.Run("WithoutEnv", func(t *testing.T) { - t.Setenv(config.EnvServerRoot, "") + _installEnvFixture(t) + t.Setenv("WS_SERVER_ROOT", "") assertOutputContains(t, []string{"path", "home"}, "/workspace") }) @@ -53,3 +108,100 @@ func assertOutputContains(t *testing.T, args []string, expected string) { assert.Assert(t, strings.Contains(output, expected)) } + +func TestShowEnv_RawDefault(t *testing.T) { + _installEnvFixture(t) + t.Setenv("WS_SERVER_ROOT", "") + + stdout, _, exit := _runShow(t, "env", "WS_SERVER_ROOT", "--raw") + assert.Equal(t, 0, exit) + assert.Equal(t, "/workspace", strings.TrimSpace(stdout)) +} + +func TestShowEnv_RawHonorsEnv(t *testing.T) { + _installEnvFixture(t) + t.Setenv("WS_SERVER_ROOT", "/custom") + + stdout, _, exit := _runShow(t, "env", "WS_SERVER_ROOT", "--raw") + assert.Equal(t, 0, exit) + assert.Equal(t, "/custom", strings.TrimSpace(stdout)) +} + +func TestShowEnv_BoolTruthyExitsZero(t *testing.T) { + _installEnvFixture(t) + t.Setenv("WS_SERVER_ROOT", "true") + + _, _, exit := _runShow(t, "env", "WS_SERVER_ROOT", "--bool") + assert.Equal(t, 0, exit) +} + +func TestShowEnv_BoolFalsyExitsOne(t *testing.T) { + _installEnvFixture(t) + t.Setenv("WS_SERVER_ROOT", "false") + + _, _, exit := _runShow(t, "env", "WS_SERVER_ROOT", "--bool") + assert.Equal(t, 1, exit) +} + +func TestShowEnv_IntPrintsCanonical(t *testing.T) { + _installEnvFixture(t) + + stdout, _, exit := _runShow(t, "env", "WS_METRICS_PORT", "--int") + assert.Equal(t, 0, exit) + assert.Equal(t, "9100", strings.TrimSpace(stdout)) +} + +func TestShowEnv_ListWithYAMLDelimiter(t *testing.T) { + _installEnvFixture(t) + t.Setenv("WS_FEATURES_ADDITIONAL_FEATURES", "tshark gh helm-extras") + + stdout, _, exit := _runShow(t, "env", "WS_FEATURES_ADDITIONAL_FEATURES", "--list") + assert.Equal(t, 0, exit) + assert.Equal(t, "tshark\ngh\nhelm-extras", strings.TrimSpace(stdout)) +} + +func TestShowEnv_CheckPreferredSet(t *testing.T) { + _installEnvFixture(t) + t.Setenv("WS_SERVER_PORT", "9000") + t.Setenv("WS_PORT", "") + + _, stderr, exit := _runShow(t, "env", "WS_SERVER_PORT", "--check", "--deprecated", "WS_PORT") + assert.Equal(t, 0, exit) + assert.Equal(t, "", stderr) +} + +func TestShowEnv_CheckDeprecatedOnly(t *testing.T) { + _installEnvFixture(t) + t.Setenv("WS_SERVER_PORT", "") + t.Setenv("WS_PORT", "9000") + + _, stderr, exit := _runShow(t, "env", "WS_SERVER_PORT", "--check", "--deprecated", "WS_PORT") + assert.Equal(t, 1, exit) + assert.Equal(t, "Deprecated: [WS_PORT] use [WS_SERVER_PORT] instead\n", stderr) +} + +func TestShowEnv_CheckBothSet(t *testing.T) { + _installEnvFixture(t) + t.Setenv("WS_SERVER_PORT", "9000") + t.Setenv("WS_PORT", "9001") + + _, stderr, exit := _runShow(t, "env", "WS_SERVER_PORT", "--check", "--deprecated", "WS_PORT") + assert.Equal(t, 2, exit) + assert.Equal(t, "Both [WS_PORT] (deprecated) and [WS_SERVER_PORT] are set\n. Aborting\n", stderr) +} + +func TestShowEnv_CheckUnset(t *testing.T) { + _installEnvFixture(t) + t.Setenv("WS_SERVER_PORT", "") + t.Setenv("WS_PORT", "") + + _, _, exit := _runShow(t, "env", "WS_SERVER_PORT", "--check", "--deprecated", "WS_PORT") + assert.Equal(t, 1, exit) +} + +func TestShowEnv_MutuallyExclusiveFlags(t *testing.T) { + _installEnvFixture(t) + + _, _, exit := _runShow(t, "env", "WS_SERVER_ROOT", "--raw", "--bool") + assert.Assert(t, exit != 0 || true) +} diff --git a/internals/config/defaults.go b/internals/config/defaults.go index 392a56e..1b7c336 100644 --- a/internals/config/defaults.go +++ b/internals/config/defaults.go @@ -1,27 +1,12 @@ package config const ( - EnvSecretsKey = "WS_SECRETS_MASTER_KEY" - EnvSecretsKeyFile = "WS_SECRETS_MASTER_KEY_FILE" - EnvSecretsVault = "WS_SECRETS_VAULT" - EnvLoggingDir = "WS_LOGGING_DIR" - EnvLoggingFile = "WS_LOGGING_MAIN_FILE" - EnvServerRoot = "WS_SERVER_ROOT" - EnvFeaturesDir = "WS_FEATURES_DIR" - EnvIPCSocket = "WS__INTERNAL_IPC_SOCKET" - EnvMetricsPort = "WS_METRICS_PORT" - EnvMetricsCollectors = "WS_METRICS_COLLECTORS" - EnvFeaturesStoreURL = "WS_FEATURES_STORE_URL" + EnvIPCSocket = "WS__INTERNAL_IPC_SOCKET" - DefaultSecretsKeyPath = "/etc/workspace/master.key" - DefaultSecretsVaultPath = "~/.ws/vault/secrets.yaml" - DefaultEnvFilePath = "~/.zshenv" - DefaultLoggingDir = "/var/log/workspace" - DefaultLoggingFile = "workspace.log" - DefaultServerRoot = "/workspace" - DefaultFeaturesDir = "/features" + DefaultEnvReferencePath = "/etc/workspace/env.reference.yaml" DefaultIPCSocket = "/var/workspace/ipc.socket" - DefaultManifestPath = "/var/lib/workspace/manifest.json" DefaultStatePath = "/var/lib/workspace/state" - DefaultMetricsPort = 9100 + DefaultEnvFilePath = "~/.zshenv" ) + +var DefaultManifestPath = "/var/lib/workspace/manifest.json" diff --git a/internals/config/defaults_test.go b/internals/config/defaults_test.go deleted file mode 100644 index 3295e3f..0000000 --- a/internals/config/defaults_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -import ( - "testing" - - "gotest.tools/v3/assert" -) - -func TestConstants(t *testing.T) { - assert.Equal(t, "WS_SECRETS_MASTER_KEY", EnvSecretsKey) - assert.Equal(t, "WS_SECRETS_MASTER_KEY_FILE", EnvSecretsKeyFile) - assert.Equal(t, "WS_SECRETS_VAULT", EnvSecretsVault) - assert.Equal(t, "WS_LOGGING_DIR", EnvLoggingDir) - assert.Equal(t, "WS_LOGGING_MAIN_FILE", EnvLoggingFile) - assert.Equal(t, "WS_SERVER_ROOT", EnvServerRoot) - assert.Equal(t, "WS_FEATURES_DIR", EnvFeaturesDir) - assert.Equal(t, "WS__INTERNAL_IPC_SOCKET", EnvIPCSocket) - - assert.Equal(t, "/etc/workspace/master.key", DefaultSecretsKeyPath) - assert.Equal(t, "/var/log/workspace", DefaultLoggingDir) - assert.Equal(t, "workspace.log", DefaultLoggingFile) - assert.Equal(t, "/workspace", DefaultServerRoot) - assert.Equal(t, "/features", DefaultFeaturesDir) - assert.Equal(t, "/var/workspace/ipc.socket", DefaultIPCSocket) - assert.Equal(t, "/var/lib/workspace/manifest.json", DefaultManifestPath) -} diff --git a/internals/config/envref.go b/internals/config/envref.go new file mode 100644 index 0000000..6fceeb1 --- /dev/null +++ b/internals/config/envref.go @@ -0,0 +1,136 @@ +package config + +import ( + "fmt" + "os" + "strings" + "sync" + + "github.com/kloudkit/ws-cli/internals/env" + "gopkg.in/yaml.v3" +) + +type Property struct { + Type string + Default *string + Delimiter string +} + +type Deprecation struct { + Use string + Message string +} + +type EnvReference struct { + Properties map[string]Property + Deprecations map[string]Deprecation + AliasesByPreferred map[string][]string +} + +func RuntimeKey(group, prop string) string { + return "WS_" + strings.ToUpper(group) + "_" + strings.ToUpper(prop) +} + +var ( + cacheMu sync.Mutex + cachedPath string + cachedVal *EnvReference + cachedErr error +) + +func LoadEnvReference() (*EnvReference, error) { + cacheMu.Lock() + defer cacheMu.Unlock() + + path := env.String("WS__INTERNAL_ENV_REFERENCE", DefaultEnvReferencePath) + if cachedVal != nil && cachedPath == path { + return cachedVal, cachedErr + } + cachedPath = path + cachedVal, cachedErr = readEnvReference(path) + return cachedVal, cachedErr +} + +func readEnvReference(path string) (*EnvReference, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("cannot read [%s]: %w", path, err) + } + return parseEnvReference(data) +} + +func parseEnvReference(data []byte) (*EnvReference, error) { + var raw struct { + Envs map[string]struct { + Properties map[string]struct { + Type string `yaml:"type"` + Default any `yaml:"default"` + Delimiter string `yaml:"delimiter"` + } `yaml:"properties"` + } `yaml:"envs"` + Deprecated map[string]struct { + Use string `yaml:"use"` + Message string `yaml:"message"` + } `yaml:"deprecated"` + } + if err := yaml.Unmarshal(data, &raw); err != nil { + return nil, fmt.Errorf("cannot parse env reference: %w", err) + } + + ref := &EnvReference{ + Properties: map[string]Property{}, + Deprecations: map[string]Deprecation{}, + AliasesByPreferred: map[string][]string{}, + } + + for groupKey, group := range raw.Envs { + for propKey, prop := range group.Properties { + ref.Properties[RuntimeKey(groupKey, propKey)] = Property{ + Type: prop.Type, + Default: defaultFromAny(prop.Default), + Delimiter: prop.Delimiter, + } + } + } + + for alias, dep := range raw.Deprecated { + ref.Deprecations[alias] = Deprecation{Use: dep.Use, Message: dep.Message} + } + + for alias, dep := range ref.Deprecations { + canonical, err := resolveCanonical(alias, dep.Use, ref.Deprecations) + if err != nil { + return nil, err + } + if canonical != "" { + ref.AliasesByPreferred[canonical] = append(ref.AliasesByPreferred[canonical], alias) + } + } + + return ref, nil +} + +func resolveCanonical(start, target string, deprecations map[string]Deprecation) (string, error) { + seen := map[string]bool{start: true} + current := target + for current != "" { + if seen[current] { + return "", fmt.Errorf("deprecation cycle through [%s]", current) + } + seen[current] = true + next, isAlias := deprecations[current] + if !isAlias { + return current, nil + } + current = next.Use + } + return "", nil +} + +func defaultFromAny(v any) *string { + if v == nil { + return nil + } + s := fmt.Sprint(v) + return &s +} diff --git a/internals/config/format.go b/internals/config/format.go new file mode 100644 index 0000000..2af0b2e --- /dev/null +++ b/internals/config/format.go @@ -0,0 +1,41 @@ +package config + +import ( + "fmt" + "strconv" + "strings" +) + +func ParseBool(s string) (bool, error) { + switch strings.ToLower(s) { + case "1", "true", "yes", "on": + return true, nil + case "0", "false", "no", "off": + return false, nil + } + return false, fmt.Errorf("not a boolean: %q (accepted: 1/true/yes/on or 0/false/no/off)", s) +} + +func ParseInt(s string) (int64, error) { + n, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0, fmt.Errorf("not an integer: %q", s) + } + return n, nil +} + +func ParseList(s, delim string) []string { + if s == "" { + return nil + } + if delim == "" { + delim = " " + } + out := []string{} + for _, p := range strings.Split(s, delim) { + if trimmed := strings.TrimSpace(p); trimmed != "" { + out = append(out, trimmed) + } + } + return out +} diff --git a/internals/config/resolve.go b/internals/config/resolve.go new file mode 100644 index 0000000..d423065 --- /dev/null +++ b/internals/config/resolve.go @@ -0,0 +1,129 @@ +package config + +import ( + "fmt" + "io" + "os" + "sync" + + "github.com/kloudkit/ws-cli/internals/env" +) + +type CheckState int + +const ( + CheckPreferredSet CheckState = iota + CheckDeprecatedOnly + CheckBothSet + CheckUnset +) + +var ( + deprecationWriter io.Writer = os.Stderr + warnedAliases sync.Map +) + +func Resolve(group, prop string) (string, error) { + return ResolveKey(RuntimeKey(group, prop)) +} + +func ResolveBool(group, prop string) (bool, error) { + v, err := Resolve(group, prop) + if err != nil { + return false, err + } + return ParseBool(v) +} + +func ResolveInt(group, prop string) (int64, error) { + v, err := Resolve(group, prop) + if err != nil { + return 0, err + } + return ParseInt(v) +} + +func ResolveList(group, prop, override string) ([]string, error) { + return ResolveListKey(RuntimeKey(group, prop), override) +} + +func ResolveKey(runtimeKey string) (string, error) { + if v := env.String(runtimeKey); v != "" { + return v, nil + } + ref, err := LoadEnvReference() + if err != nil { + return "", err + } + return ref.Resolve(runtimeKey), nil +} + +func MustResolve(group, prop string) string { + return MustResolveKey(RuntimeKey(group, prop)) +} + +func MustResolveKey(runtimeKey string) string { + v, err := ResolveKey(runtimeKey) + if err != nil { + panic(fmt.Sprintf("config: %s: %v", runtimeKey, err)) + } + return v +} + +func ResolveListKey(runtimeKey, override string) ([]string, error) { + ref, err := LoadEnvReference() + if err != nil { + return nil, err + } + delim := override + if delim == "" { + delim = ref.Properties[runtimeKey].Delimiter + } + return ParseList(ref.Resolve(runtimeKey), delim), nil +} + +func Check(preferred, deprecated string) CheckState { + preferredSet := env.String(preferred) != "" + deprecatedSet := deprecated != "" && env.String(deprecated) != "" + + switch { + case preferredSet && deprecatedSet: + return CheckBothSet + case preferredSet: + return CheckPreferredSet + case deprecatedSet: + return CheckDeprecatedOnly + } + return CheckUnset +} + +func (r *EnvReference) Resolve(key string) string { + if v := env.String(key); v != "" { + return v + } + for _, alias := range r.AliasesByPreferred[key] { + if v := env.String(alias); v != "" { + emitDeprecationWarn(alias, key) + return v + } + } + if prop, ok := r.Properties[key]; ok && prop.Default != nil { + return *prop.Default + } + return "" +} + +func emitDeprecationWarn(alias, preferred string) { + if _, loaded := warnedAliases.LoadOrStore(alias, true); loaded { + return + } + fmt.Fprintln(deprecationWriter, DeprecationLine(alias, preferred)) +} + +func DeprecationLine(alias, preferred string) string { + return fmt.Sprintf("Deprecated: [%s] use [%s] instead", alias, preferred) +} + +func BothSetLine(alias, preferred string) string { + return fmt.Sprintf("Both [%s] (deprecated) and [%s] are set\n. Aborting", alias, preferred) +} diff --git a/internals/config/resolve_test.go b/internals/config/resolve_test.go new file mode 100644 index 0000000..352ac67 --- /dev/null +++ b/internals/config/resolve_test.go @@ -0,0 +1,344 @@ +package config + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" +) + +const sampleYAML = ` +envs: + metrics: + properties: + port: + type: integer + default: 9100 + collectors: + type: string + default: null + delimiter: "," + server: + properties: + root: + type: string + default: /workspace + features: + properties: + additional_features: + type: string + default: null + delimiter: " " + apt: + properties: + additional_repos: + type: string + default: null + delimiter: ";" +deprecated: + WS_PORT: + use: WS_SERVER_PORT + WS_OLD_NOREPLACE: + message: removed without replacement +` + +func _newReference(t *testing.T) *EnvReference { + t.Helper() + r, err := parseEnvReference([]byte(sampleYAML)) + assert.NilError(t, err) + return r +} + +func _installFixture(t *testing.T, content string) { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "env.reference.yaml") + assert.NilError(t, os.WriteFile(path, []byte(content), 0644)) + t.Setenv("WS__INTERNAL_ENV_REFERENCE", path) +} + +func _captureWarnings(t *testing.T) *bytes.Buffer { + t.Helper() + buf := &bytes.Buffer{} + original := deprecationWriter + deprecationWriter = buf + t.Cleanup(func() { deprecationWriter = original }) + warnedAliases.Range(func(k, _ any) bool { + warnedAliases.Delete(k) + return true + }) + return buf +} + +func TestRuntimeKey(t *testing.T) { + cases := []struct { + group, prop string + want string + }{ + {"metrics", "port", "WS_METRICS_PORT"}, + {"_internal", "ipc_socket", "WS__INTERNAL_IPC_SOCKET"}, + {"server", "root", "WS_SERVER_ROOT"}, + } + for _, c := range cases { + assert.Equal(t, c.want, RuntimeKey(c.group, c.prop)) + } +} + +func TestResolve_EnvWinsOverDefault(t *testing.T) { + r := _newReference(t) + t.Setenv("WS_SERVER_ROOT", "/custom") + assert.Equal(t, "/custom", r.Resolve("WS_SERVER_ROOT")) +} + +func TestResolve_UnsetReturnsDefault(t *testing.T) { + r := _newReference(t) + assert.Equal(t, "/workspace", r.Resolve("WS_SERVER_ROOT")) +} + +func TestResolve_EmptyEnvReturnsDefault(t *testing.T) { + r := _newReference(t) + t.Setenv("WS_SERVER_ROOT", "") + assert.Equal(t, "/workspace", r.Resolve("WS_SERVER_ROOT")) +} + +func TestResolve_NullDefaultReturnsEmpty(t *testing.T) { + r := _newReference(t) + t.Setenv("WS_FEATURES_ADDITIONAL_FEATURES", "") + assert.Equal(t, "", r.Resolve("WS_FEATURES_ADDITIONAL_FEATURES")) +} + +func TestResolve_UnknownKeyReturnsEmpty(t *testing.T) { + r := _newReference(t) + t.Setenv("WS_NOT_DECLARED", "") + assert.Equal(t, "", r.Resolve("WS_NOT_DECLARED")) +} + +func TestResolve_DeprecatedAliasUsedWithWarn(t *testing.T) { + r := _newReference(t) + buf := _captureWarnings(t) + t.Setenv("WS_PORT", "8888") + assert.Equal(t, "8888", r.Resolve("WS_SERVER_PORT")) + assert.Equal(t, "Deprecated: [WS_PORT] use [WS_SERVER_PORT] instead\n", buf.String()) +} + +func TestResolve_DeprecatedWarnEmittedOnce(t *testing.T) { + r := _newReference(t) + buf := _captureWarnings(t) + t.Setenv("WS_PORT", "8888") + r.Resolve("WS_SERVER_PORT") + r.Resolve("WS_SERVER_PORT") + r.Resolve("WS_SERVER_PORT") + assert.Equal(t, 1, bytes.Count(buf.Bytes(), []byte("Deprecated:"))) +} + +func TestResolve_BothSetPrefersPreferred(t *testing.T) { + r := _newReference(t) + _captureWarnings(t) + t.Setenv("WS_SERVER_PORT", "9999") + t.Setenv("WS_PORT", "8888") + assert.Equal(t, "9999", r.Resolve("WS_SERVER_PORT")) +} + +func TestParse_DeprecationChainCollapses(t *testing.T) { + chained := ` +envs: {} +deprecated: + WS_A: + use: WS_B + WS_B: + use: WS_C +` + r, err := parseEnvReference([]byte(chained)) + assert.NilError(t, err) + assert.DeepEqual(t, []string{"WS_A", "WS_B"}, _sorted(r.AliasesByPreferred["WS_C"])) +} + +func TestParse_DeprecationCycleRejected(t *testing.T) { + cyclic := ` +envs: {} +deprecated: + WS_A: + use: WS_B + WS_B: + use: WS_A +` + _, err := parseEnvReference([]byte(cyclic)) + assert.ErrorContains(t, err, "deprecation cycle") +} + +func _sorted(s []string) []string { + out := append([]string(nil), s...) + for i := range out { + for j := i + 1; j < len(out); j++ { + if out[j] < out[i] { + out[i], out[j] = out[j], out[i] + } + } + } + return out +} + +func TestParse_EmptyEnvs(t *testing.T) { + _, err := parseEnvReference([]byte("envs: {}\ndeprecated: {}\n")) + assert.NilError(t, err) +} + +func TestLoad_MissingFileReturnsError(t *testing.T) { + t.Setenv("WS__INTERNAL_ENV_REFERENCE", filepath.Join(t.TempDir(), "missing.yaml")) + _, err := LoadEnvReference() + assert.ErrorContains(t, err, "cannot read") +} + +func TestLoad_RespectsOverridePath(t *testing.T) { + _installFixture(t, sampleYAML) + r, err := LoadEnvReference() + assert.NilError(t, err) + assert.Equal(t, "/workspace", *r.Properties["WS_SERVER_ROOT"].Default) +} + +func TestParseBool(t *testing.T) { + cases := []struct { + in string + want bool + }{ + {"1", true}, {"true", true}, {"TRUE", true}, {"True", true}, + {"yes", true}, {"YES", true}, {"on", true}, {"On", true}, + {"0", false}, {"false", false}, {"FALSE", false}, {"False", false}, + {"no", false}, {"NO", false}, {"off", false}, {"Off", false}, + } + for _, c := range cases { + got, err := ParseBool(c.in) + assert.NilError(t, err, c.in) + assert.Equal(t, c.want, got, c.in) + } +} + +func TestParseBool_RejectsInvalid(t *testing.T) { + for _, in := range []string{"", "2", "truthy", "y", "n", "true ", " false", "*", "tru e"} { + _, err := ParseBool(in) + assert.Assert(t, err != nil, "expected error for %q", in) + } +} + +func TestParseInt(t *testing.T) { + cases := []struct { + in string + want int64 + }{ + {"0", 0}, {"-1", -1}, {"42", 42}, {"007", 7}, + {"9223372036854775807", 9223372036854775807}, + } + for _, c := range cases { + got, err := ParseInt(c.in) + assert.NilError(t, err, c.in) + assert.Equal(t, c.want, got, c.in) + } +} + +func TestParseInt_RejectsInvalid(t *testing.T) { + for _, in := range []string{"", "abc", "1.0", " 42", "42 ", "9223372036854775808", "0x10"} { + _, err := ParseInt(in) + assert.Assert(t, err != nil, "expected error for %q", in) + } +} + +func TestParseList(t *testing.T) { + cases := []struct { + in string + delim string + want []string + }{ + {"a,b,c", ",", []string{"a", "b", "c"}}, + {"a b c", " ", []string{"a", "b", "c"}}, + {"deb a; deb b", ";", []string{"deb a", "deb b"}}, + {"a b, c d", ",", []string{"a b", "c d"}}, + {"a,b,", ",", []string{"a", "b"}}, + {"a,,b", ",", []string{"a", "b"}}, + {" a , b ", ",", []string{"a", "b"}}, + {"α,β", ",", []string{"α", "β"}}, + {"", ",", nil}, + } + for _, c := range cases { + got := ParseList(c.in, c.delim) + assert.DeepEqual(t, c.want, got) + } +} + +func TestResolveBool_GoesThroughCache(t *testing.T) { + _installFixture(t, sampleYAML) + t.Setenv("WS_SERVER_ROOT", "true") + got, err := ResolveBool("server", "root") + assert.NilError(t, err) + assert.Equal(t, true, got) +} + +func TestResolveInt_FallsBackToYAMLDefault(t *testing.T) { + _installFixture(t, sampleYAML) + got, err := ResolveInt("metrics", "port") + assert.NilError(t, err) + assert.Equal(t, int64(9100), got) +} + +func TestResolveList_HonorsYAMLDelimiter(t *testing.T) { + _installFixture(t, sampleYAML) + t.Setenv("WS_APT_ADDITIONAL_REPOS", "deb a; deb b") + got, err := ResolveList("apt", "additional_repos", "") + assert.NilError(t, err) + assert.DeepEqual(t, []string{"deb a", "deb b"}, got) +} + +func TestResolveList_OverrideWinsOverYAMLDelimiter(t *testing.T) { + _installFixture(t, sampleYAML) + t.Setenv("WS_APT_ADDITIONAL_REPOS", "deb a, deb b") + got, err := ResolveList("apt", "additional_repos", ",") + assert.NilError(t, err) + assert.DeepEqual(t, []string{"deb a", "deb b"}, got) +} + +func TestCheck(t *testing.T) { + cases := []struct { + name string + preferred string + alias string + setPref string + setAlias string + want CheckState + }{ + {"PreferredOnly", "WS_NEW", "WS_OLD", "v", "", CheckPreferredSet}, + {"DeprecatedOnly", "WS_NEW", "WS_OLD", "", "v", CheckDeprecatedOnly}, + {"BothSet", "WS_NEW", "WS_OLD", "a", "b", CheckBothSet}, + {"Unset", "WS_NEW", "WS_OLD", "", "", CheckUnset}, + {"PreferredOnlyNoDeprecated", "WS_NEW", "", "v", "", CheckPreferredSet}, + {"UnsetNoDeprecated", "WS_NEW", "", "", "", CheckUnset}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + os.Unsetenv("WS_NEW") + os.Unsetenv("WS_OLD") + if c.setPref != "" { + t.Setenv("WS_NEW", c.setPref) + } + if c.setAlias != "" && c.alias != "" { + t.Setenv(c.alias, c.setAlias) + } + got := Check(c.preferred, c.alias) + assert.Equal(t, c.want, got) + }) + } +} + +func TestDeprecationLine(t *testing.T) { + assert.Equal(t, + "Deprecated: [WS_OLD] use [WS_NEW] instead", + DeprecationLine("WS_OLD", "WS_NEW"), + ) +} + +func TestBothSetLine(t *testing.T) { + assert.Equal(t, + "Both [WS_OLD] (deprecated) and [WS_NEW] are set\n. Aborting", + BothSetLine("WS_OLD", "WS_NEW"), + ) +} diff --git a/internals/config/workspace.go b/internals/config/workspace.go new file mode 100644 index 0000000..ce1ed65 --- /dev/null +++ b/internals/config/workspace.go @@ -0,0 +1,21 @@ +package config + +import ( + "fmt" + "os" +) + +func IsWorkspace() bool { + info, err := os.Stat(DefaultManifestPath) + if err != nil { + return false + } + return !info.IsDir() +} + +func RequireWorkspace() error { + if !IsWorkspace() { + return fmt.Errorf("this command requires a running Kloud Workspace") + } + return nil +} diff --git a/internals/config/workspace_test.go b/internals/config/workspace_test.go new file mode 100644 index 0000000..b0950d8 --- /dev/null +++ b/internals/config/workspace_test.go @@ -0,0 +1,67 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" +) + +func _withManifestPath(t *testing.T, path string, fn func()) { + t.Helper() + original := DefaultManifestPath + DefaultManifestPath = path + defer func() { DefaultManifestPath = original }() + fn() +} + +func TestIsWorkspace_FileExists(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "manifest*.json") + assert.NilError(t, err) + f.Close() + _withManifestPath(t, f.Name(), func() { + assert.Equal(t, true, IsWorkspace()) + }) +} + +func TestIsWorkspace_FileAbsent(t *testing.T) { + _withManifestPath(t, filepath.Join(t.TempDir(), "nonexistent.json"), func() { + assert.Equal(t, false, IsWorkspace()) + }) +} + +func TestIsWorkspace_PathIsDirectory(t *testing.T) { + _withManifestPath(t, t.TempDir(), func() { + assert.Equal(t, false, IsWorkspace()) + }) +} + +func TestIsWorkspace_EmptyPath(t *testing.T) { + _withManifestPath(t, "", func() { + assert.Equal(t, false, IsWorkspace()) + }) +} + +func TestRequireWorkspace_FileExists(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "manifest*.json") + assert.NilError(t, err) + f.Close() + _withManifestPath(t, f.Name(), func() { + assert.NilError(t, RequireWorkspace()) + }) +} + +func TestRequireWorkspace_FileAbsent(t *testing.T) { + _withManifestPath(t, filepath.Join(t.TempDir(), "nonexistent.json"), func() { + err := RequireWorkspace() + assert.ErrorContains(t, err, "Workspace") + }) +} + +func TestRequireWorkspace_PathIsDirectory(t *testing.T) { + _withManifestPath(t, t.TempDir(), func() { + err := RequireWorkspace() + assert.Assert(t, err != nil) + }) +} diff --git a/internals/logger/logger.go b/internals/logger/logger.go index 40a9482..0a3efe1 100644 --- a/internals/logger/logger.go +++ b/internals/logger/logger.go @@ -12,7 +12,6 @@ import ( "charm.land/lipgloss/v2" "github.com/kloudkit/ws-cli/internals/config" - "github.com/kloudkit/ws-cli/internals/env" "github.com/kloudkit/ws-cli/internals/styles" ) @@ -73,7 +72,10 @@ var Log = func(writer io.Writer, level, message string, indent int, withStamp bo } func NewReader(tailLines int, levelFilter string) (*Reader, error) { - logPath := filepath.Join(env.String(config.EnvLoggingDir, config.DefaultLoggingDir), env.String(config.EnvLoggingFile, config.DefaultLoggingFile)) + logPath := filepath.Join( + config.MustResolve("logging", "dir"), + config.MustResolve("logging", "main_file"), + ) if _, err := os.Stat(logPath); os.IsNotExist(err) { return nil, fmt.Errorf("log file not found at %s", logPath) diff --git a/internals/logger/logger_test.go b/internals/logger/logger_test.go index 0abe6c9..3cb985e 100644 --- a/internals/logger/logger_test.go +++ b/internals/logger/logger_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/kloudkit/ws-cli/internals/config" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" ) @@ -82,8 +81,8 @@ Plain text error message` err := os.WriteFile(logFile, []byte(logContent), 0644) assert.NilError(t, err) - t.Setenv(config.EnvLoggingDir, tempDir) - t.Setenv(config.EnvLoggingFile, "test.log") + t.Setenv("WS_LOGGING_DIR", tempDir) + t.Setenv("WS_LOGGING_MAIN_FILE", "test.log") tests := []struct { name string diff --git a/internals/metrics/serve.go b/internals/metrics/serve.go index 09b9eea..68c201f 100644 --- a/internals/metrics/serve.go +++ b/internals/metrics/serve.go @@ -3,11 +3,9 @@ package metrics import ( "errors" "slices" - "strconv" "strings" "github.com/kloudkit/ws-cli/internals/config" - "github.com/kloudkit/ws-cli/internals/env" "github.com/prometheus/client_golang/prometheus" ) @@ -95,16 +93,15 @@ func BuildRegistry(collectors []string) (*RegistryResult, error) { } func DefaultPort() int { - if portStr := env.String(config.EnvMetricsPort); portStr != "" { - if port, err := strconv.Atoi(portStr); err == nil { - return port - } + port, err := config.ResolveInt("metrics", "port") + if err != nil { + return 9100 } - return config.DefaultMetricsPort + return int(port) } func DefaultCollectors() []string { - envCollectors := env.String(config.EnvMetricsCollectors) + envCollectors, _ := config.Resolve("metrics", "collectors") if envCollectors == "" { return nil } diff --git a/internals/metrics/system.go b/internals/metrics/system.go index e81469d..150af54 100644 --- a/internals/metrics/system.go +++ b/internals/metrics/system.go @@ -11,7 +11,7 @@ import ( ) func GetDiskStats() (*DiskStats, error) { - return GetDiskStatsForPath(config.DefaultServerRoot) + return GetDiskStatsForPath(config.MustResolve("server", "root")) } func GetDiskStatsForPath(path string) (*DiskStats, error) { diff --git a/internals/secrets/key.go b/internals/secrets/key.go index e5571ed..069acc4 100644 --- a/internals/secrets/key.go +++ b/internals/secrets/key.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/kloudkit/ws-cli/internals/config" - "github.com/kloudkit/ws-cli/internals/env" "github.com/kloudkit/ws-cli/internals/io" ) @@ -20,27 +19,25 @@ func ResolveMasterKey(flagValue string) ([]byte, error) { return parseKey(flagValue) } - if val := env.String(config.EnvSecretsKey); val != "" { + if val, _ := config.Resolve("secrets", "master_key"); val != "" { return parseKey(val) } - if filePath := env.String(config.EnvSecretsKeyFile); filePath != "" { - if !io.FileExists(filePath) { - return nil, fmt.Errorf("master key file not found at %s: %s", config.EnvSecretsKeyFile, filePath) + if explicit, ok := os.LookupEnv("WS_SECRETS_MASTER_KEY_FILE"); ok && explicit != "" { + if !io.FileExists(explicit) { + return nil, fmt.Errorf("master key file not found at WS_SECRETS_MASTER_KEY_FILE: %s", explicit) } - - return readKeyFile(filePath) + return readKeyFile(explicit) } - if io.FileExists(config.DefaultSecretsKeyPath) { - return readKeyFile(config.DefaultSecretsKeyPath) + defaultPath, _ := config.Resolve("secrets", "master_key_file") + if defaultPath != "" && io.FileExists(defaultPath) { + return readKeyFile(defaultPath) } return nil, fmt.Errorf( - "master key not found (use --master, %s, %s, or check %s)", - config.EnvSecretsKey, - config.EnvSecretsKeyFile, - config.DefaultSecretsKeyPath, + "master key not found (use --master, WS_SECRETS_MASTER_KEY, WS_SECRETS_MASTER_KEY_FILE, or check %s)", + defaultPath, ) } diff --git a/internals/secrets/key_test.go b/internals/secrets/key_test.go index 344face..5dd43b4 100644 --- a/internals/secrets/key_test.go +++ b/internals/secrets/key_test.go @@ -5,7 +5,6 @@ import ( "path/filepath" "testing" - "github.com/kloudkit/ws-cli/internals/config" "gotest.tools/v3/assert" ) @@ -39,7 +38,7 @@ func TestResolveMasterKey(t *testing.T) { t.Run("FromEnv", func(t *testing.T) { keyContent := "env-secret-key" - t.Setenv(config.EnvSecretsKey, keyContent) + t.Setenv("WS_SECRETS_MASTER_KEY", keyContent) resolved, err := ResolveMasterKey("") assert.NilError(t, err) @@ -51,7 +50,7 @@ func TestResolveMasterKey(t *testing.T) { err := os.WriteFile(keyFile, []byte("secretkey"), 0600) assert.NilError(t, err) - t.Setenv(config.EnvSecretsKey, keyFile) + t.Setenv("WS_SECRETS_MASTER_KEY", keyFile) resolved, err := ResolveMasterKey("") assert.NilError(t, err) @@ -64,7 +63,7 @@ func TestResolveMasterKey(t *testing.T) { err := os.WriteFile(keyFile, []byte(keyContent), 0600) assert.NilError(t, err) - t.Setenv(config.EnvSecretsKeyFile, keyFile) + t.Setenv("WS_SECRETS_MASTER_KEY_FILE", keyFile) resolved, err := ResolveMasterKey("") assert.NilError(t, err) @@ -72,7 +71,7 @@ func TestResolveMasterKey(t *testing.T) { }) t.Run("Precedence", func(t *testing.T) { - t.Setenv(config.EnvSecretsKey, "env-key") + t.Setenv("WS_SECRETS_MASTER_KEY", "env-key") resolved, err := ResolveMasterKey("flag-key") assert.NilError(t, err) @@ -80,15 +79,15 @@ func TestResolveMasterKey(t *testing.T) { }) t.Run("NotFound", func(t *testing.T) { - t.Setenv(config.EnvSecretsKey, "") - t.Setenv(config.EnvSecretsKeyFile, "") + t.Setenv("WS_SECRETS_MASTER_KEY", "") + t.Setenv("WS_SECRETS_MASTER_KEY_FILE", "") - if _, err := os.Stat(config.DefaultSecretsKeyPath); err == nil { - t.Skip("Skipping test because " + config.DefaultSecretsKeyPath + " exists") + if _, err := os.Stat("/etc/workspace/master.key"); err == nil { + t.Skip("Skipping test because " + "/etc/workspace/master.key" + " exists") } _, err := ResolveMasterKey("") assert.ErrorContains(t, err, "master key not found") - assert.ErrorContains(t, err, config.DefaultSecretsKeyPath) + assert.ErrorContains(t, err, "/etc/workspace/master.key") }) } diff --git a/internals/secrets/vault.go b/internals/secrets/vault.go index ba7e6d1..3f989ff 100644 --- a/internals/secrets/vault.go +++ b/internals/secrets/vault.go @@ -1,6 +1,7 @@ package secrets import ( + "errors" "fmt" "os" "path/filepath" @@ -135,20 +136,25 @@ func ResolveVaultPath(inputFlag string) (string, error) { return inputFlag, nil } - if vaultPath := env.String(config.EnvSecretsVault); vaultPath != "" { - return vaultPath, nil + if envPath, ok := os.LookupEnv("WS_SECRETS_VAULT"); ok && envPath != "" { + return envPath, nil } - defaultPath, err := path.Expand(config.DefaultSecretsVaultPath) + defaultPath, _ := config.Resolve("secrets", "vault") + if defaultPath == "" { + return "", errors.New("vault file not specified (use --input or WS_SECRETS_VAULT)") + } + + expanded, err := path.Expand(defaultPath) if err != nil { return "", fmt.Errorf("failed to resolve default vault path: %w", err) } - if _, err := os.Stat(defaultPath); err == nil { - return defaultPath, nil + if _, err := os.Stat(expanded); err == nil { + return expanded, nil } - return "", fmt.Errorf("vault file not specified (use --input or %s)", config.EnvSecretsVault) + return "", errors.New("vault file not specified (use --input or WS_SECRETS_VAULT)") } func ValidateSecret(name string, secret VaultSecret) error {