diff --git a/README.md b/README.md index 09b8fc8b..4596b5a7 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,14 @@ example, if you do `chamber exec app apptwo -- ...` and both apps have a secret named `api_key`, the `api_key` from `apptwo` will be the one set in your environment. +When loading secrets from multiple services, chamber will warn about environment +variables that get overwritten. Use the `--no-warn-conflicts` flag to suppress +these warnings when conflicts are expected: + +```bash +$ chamber exec --no-warn-conflicts app apptwo -- +``` + ### Reading ```bash diff --git a/cmd/exec.go b/cmd/exec.go index 55ac5fc4..1f2db6b6 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -24,6 +24,9 @@ var strictValue string // Default value to expect in strict mode const strictValueDefault = "chamberme" +// When true, suppress warnings about conflicting environment variables +var noWarnConflicts bool + // execCmd represents the exec command var execCmd = &cobra.Command{ Use: "exec -- []", @@ -58,6 +61,11 @@ Given a secret store like this: $ HOME=/tmp DB_USERNAME=chamberme DB_PASSWORD=chamberme chamber exec --strict --pristine service exec -- env DB_USERNAME=root DB_PASSWORD=hunter22 + +--no-warn-conflicts suppresses warnings when services overwrite environment variables + + $ chamber exec --no-warn-conflicts service1 service2 -- env + # No warnings about conflicting keys between service1 and service2 `, } @@ -68,6 +76,7 @@ only inject secrets for which there is a corresponding env var with value , and fail if there are any env vars with that value missing from secrets`) execCmd.Flags().StringVar(&strictValue, "strict-value", strictValueDefault, "value to expect in --strict mode") + execCmd.Flags().BoolVar(&noWarnConflicts, "no-warn-conflicts", false, "suppress warnings when services overwrite environment variables (useful when conflicts are expected)") RootCmd.AddCommand(execCmd) } @@ -123,8 +132,10 @@ func execRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("Failed to list store contents: %w", err) } - for _, c := range collisions { - fmt.Fprintf(os.Stderr, "warning: service %s overwriting environment variable %s\n", service, c) + if !noWarnConflicts { + for _, c := range collisions { + fmt.Fprintf(os.Stderr, "warning: service %s overwriting environment variable %s\n", service, c) + } } } } diff --git a/cmd/exec_test.go b/cmd/exec_test.go new file mode 100644 index 00000000..5c20a44d --- /dev/null +++ b/cmd/exec_test.go @@ -0,0 +1,113 @@ +package cmd + +import ( + "bytes" + "os" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestExecCommandFlags(t *testing.T) { + t.Run("no-warn-conflicts flag should be defined", func(t *testing.T) { + // Reset the flag to default state + noWarnConflicts = false + + // Create a new exec command to test flag parsing + cmd := &cobra.Command{} + cmd.Flags().BoolVar(&noWarnConflicts, "no-warn-conflicts", false, "suppress warnings when services overwrite environment variables") + + // Test that the flag can be set + err := cmd.Flags().Set("no-warn-conflicts", "true") + assert.NoError(t, err) + assert.True(t, noWarnConflicts) + + // Test that the flag can be unset + err = cmd.Flags().Set("no-warn-conflicts", "false") + assert.NoError(t, err) + assert.False(t, noWarnConflicts) + }) + + t.Run("no-warn-conflicts flag should have correct default value", func(t *testing.T) { + // Reset to default state + noWarnConflicts = false + assert.False(t, noWarnConflicts) + }) +} + +func TestWarningBehavior(t *testing.T) { + // Helper function to capture stderr output + captureStderr := func(fn func()) string { + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + fn() + + w.Close() + os.Stderr = oldStderr + + var buf bytes.Buffer + buf.ReadFrom(r) + return buf.String() + } + + t.Run("should emit warnings when noWarnConflicts is false", func(t *testing.T) { + // Reset to default state + noWarnConflicts = false + + // Simulate the warning logic from exec.go + collisions := []string{"DB_HOST", "API_KEY"} + service := "test-service" + + output := captureStderr(func() { + if !noWarnConflicts { + for _, c := range collisions { + os.Stderr.WriteString("warning: service " + service + " overwriting environment variable " + c + "\n") + } + } + }) + + assert.Contains(t, output, "warning: service test-service overwriting environment variable DB_HOST") + assert.Contains(t, output, "warning: service test-service overwriting environment variable API_KEY") + }) + + t.Run("should not emit warnings when noWarnConflicts is true", func(t *testing.T) { + // Set flag to suppress warnings + noWarnConflicts = true + + // Simulate the warning logic from exec.go + collisions := []string{"DB_HOST", "API_KEY"} + service := "test-service" + + output := captureStderr(func() { + if !noWarnConflicts { + for _, c := range collisions { + os.Stderr.WriteString("warning: service " + service + " overwriting environment variable " + c + "\n") + } + } + }) + + assert.Empty(t, output) + }) + + t.Run("should handle empty collisions list", func(t *testing.T) { + // Reset to default state + noWarnConflicts = false + + // Simulate the warning logic with no collisions + collisions := []string{} + service := "test-service" + + output := captureStderr(func() { + if !noWarnConflicts { + for _, c := range collisions { + os.Stderr.WriteString("warning: service " + service + " overwriting environment variable " + c + "\n") + } + } + }) + + assert.Empty(t, output) + }) +} diff --git a/store/secretsmanagerstore_test.go b/store/secretsmanagerstore_test.go index 47028ce0..62c973b3 100644 --- a/store/secretsmanagerstore_test.go +++ b/store/secretsmanagerstore_test.go @@ -222,6 +222,17 @@ func TestNewSecretsManagerStore(t *testing.T) { secretsmanagerClient := s.svc.(*secretsmanager.Client) assert.Nil(t, secretsmanagerClient.Options().BaseEndpoint) }) + + t.Run("Should return error when AWS_PROFILE points to invalid profile", func(t *testing.T) { + os.Setenv("AWS_PROFILE", "invalid-profile") + defer os.Unsetenv("AWS_PROFILE") + + s, err := NewSecretsManagerStore(context.Background(), 1) + assert.NotNil(t, err) + assert.Nil(t, s) + // Verify it's a profile not exist error + assert.Contains(t, err.Error(), "profile") + }) } func TestSecretsManagerWrite(t *testing.T) { diff --git a/store/ssmstore_test.go b/store/ssmstore_test.go index 14dc8fea..72ccbb31 100644 --- a/store/ssmstore_test.go +++ b/store/ssmstore_test.go @@ -395,6 +395,17 @@ func TestNewSSMStore(t *testing.T) { assert.Nil(t, err) assert.Equal(t, DefaultRetryMode, s.config.RetryMode) }) + + t.Run("Should return error when AWS_PROFILE points to invalid profile", func(t *testing.T) { + os.Setenv("AWS_PROFILE", "invalid-profile") + defer os.Unsetenv("AWS_PROFILE") + + s, err := NewSSMStore(context.Background(), 1) + assert.NotNil(t, err) + assert.Nil(t, s) + // Verify it's a profile not exist error + assert.Contains(t, err.Error(), "profile") + }) } func TestNewSSMStoreWithRetryMode(t *testing.T) {