Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 -- <your executable>
```

### Reading

```bash
Expand Down
15 changes: 13 additions & 2 deletions cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <service...> -- <command> [<arg...>]",
Expand Down Expand Up @@ -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
`,
}

Expand All @@ -68,6 +76,7 @@ only inject secrets for which there is a corresponding env var with value
<strict-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)
}

Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down
113 changes: 113 additions & 0 deletions cmd/exec_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
11 changes: 11 additions & 0 deletions store/secretsmanagerstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions store/ssmstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down