Skip to content

Commit 68dff64

Browse files
committed
🐛 Fix env force bug
1 parent 2719cb4 commit 68dff64

4 files changed

Lines changed: 136 additions & 5 deletions

File tree

cmd/info/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package info
22

3-
var Version = "0.0.41"
3+
var Version = "0.0.42"

cmd/secrets/vault.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package secrets
22

33
import (
44
"fmt"
5+
"slices"
56
"strings"
67

78
internalSecrets "github.com/kloudkit/ws-cli/internals/secrets"
@@ -71,16 +72,27 @@ func init() {
7172
vaultCmd.Flags().Bool("stdout", false, "Output decrypted values to stdout")
7273
}
7374

75+
func sortedKeys(m map[string]string) []string {
76+
keys := make([]string, 0, len(m))
77+
for k := range m {
78+
keys = append(keys, k)
79+
}
80+
slices.Sort(keys)
81+
return keys
82+
}
83+
7484
func printStdoutResults(cmd *cobra.Command, results map[string]string, raw bool) {
75-
for key, value := range results {
85+
for _, key := range sortedKeys(results) {
86+
value := results[key]
7687
output := internalSecrets.FormatSecretForStdout(key, value, raw)
7788
fmt.Fprint(cmd.OutOrStdout(), output)
7889
}
7990
}
8091

8192
func printVaultSuccess(cmd *cobra.Command, results map[string]string) {
8293
fmt.Fprintln(cmd.OutOrStdout(), styles.Success().Render("✓ Vault processed successfully"))
83-
for key, dest := range results {
94+
for _, key := range sortedKeys(results) {
95+
dest := results[key]
8496
displayDest := dest
8597
if after, ok := strings.CutPrefix(dest, "env:"); ok {
8698
displayDest = fmt.Sprintf("env:%s", after)

internals/secrets/vault.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ func GetSecretKeys(vault *Vault, requestedKeys []string) []string {
183183
for key := range vault.Secrets {
184184
keys = append(keys, key)
185185
}
186+
slices.Sort(keys)
186187

187188
return keys
188189
}
@@ -210,6 +211,8 @@ func ProcessVault(vault *Vault, opts ProcessOptions) (map[string]string, error)
210211
return nil, err
211212
}
212213

214+
effectiveForce := opts.Force || secret.Force
215+
213216
encryptedValue := NormalizeEncrypted(secret.Encrypted)
214217

215218
decrypted, err := Decrypt(encryptedValue, opts.MasterKey)
@@ -228,12 +231,12 @@ func ProcessVault(vault *Vault, opts ProcessOptions) (map[string]string, error)
228231
}
229232

230233
if secret.Type == TypeEnv {
231-
if err := ProcessEnvSecret(secret.Destination, decrypted, opts.Force); err != nil {
234+
if err := ProcessEnvSecret(secret.Destination, decrypted, effectiveForce); err != nil {
232235
return nil, fmt.Errorf("failed to process env secret %q: %w", key, err)
233236
}
234237
results[key] = fmt.Sprintf("env:%s", secret.Destination)
235238
} else {
236-
if err := internalIO.WriteSecureFile(secret.Destination, decrypted, mode, opts.Force); err != nil {
239+
if err := internalIO.WriteSecureFile(secret.Destination, decrypted, mode, effectiveForce); err != nil {
237240
return nil, fmt.Errorf("failed to write secret %q: %w", key, err)
238241
}
239242
results[key] = secret.Destination

internals/secrets/vault_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ func TestGetSecretKeys(t *testing.T) {
253253
t.Run("AllKeys", func(t *testing.T) {
254254
keys := GetSecretKeys(vault, []string{})
255255
assert.Equal(t, 3, len(keys))
256+
for i := 1; i < len(keys); i++ {
257+
assert.Assert(t, keys[i-1] < keys[i], "keys should be sorted alphabetically")
258+
}
256259
})
257260

258261
t.Run("SpecificKeys", func(t *testing.T) {
@@ -503,3 +506,116 @@ func TestProcessEnvSecret(t *testing.T) {
503506
assert.Equal(t, 1, len(lines))
504507
})
505508
}
509+
510+
func TestDeterministicOrdering(t *testing.T) {
511+
t.Run("GetSecretKeysReturnsSorted", func(t *testing.T) {
512+
vault := &Vault{
513+
Secrets: map[string]VaultSecret{
514+
"zebra": {},
515+
"alpha": {},
516+
"charlie": {},
517+
"bravo": {},
518+
},
519+
}
520+
521+
keys := GetSecretKeys(vault, []string{})
522+
assert.Equal(t, 4, len(keys))
523+
assert.Equal(t, "alpha", keys[0])
524+
assert.Equal(t, "bravo", keys[1])
525+
assert.Equal(t, "charlie", keys[2])
526+
assert.Equal(t, "zebra", keys[3])
527+
})
528+
529+
t.Run("MultipleRunsProduceSameOrder", func(t *testing.T) {
530+
vault := &Vault{
531+
Secrets: map[string]VaultSecret{
532+
"secret3": {},
533+
"secret1": {},
534+
"secret2": {},
535+
},
536+
}
537+
538+
firstRun := GetSecretKeys(vault, []string{})
539+
secondRun := GetSecretKeys(vault, []string{})
540+
thirdRun := GetSecretKeys(vault, []string{})
541+
542+
assert.DeepEqual(t, firstRun, secondRun)
543+
assert.DeepEqual(t, secondRun, thirdRun)
544+
})
545+
546+
t.Run("RequestedKeysPreserveOrder", func(t *testing.T) {
547+
vault := &Vault{
548+
Secrets: map[string]VaultSecret{
549+
"zebra": {},
550+
"alpha": {},
551+
"charlie": {},
552+
},
553+
}
554+
555+
requested := []string{"zebra", "alpha"}
556+
keys := GetSecretKeys(vault, requested)
557+
assert.DeepEqual(t, requested, keys)
558+
})
559+
}
560+
561+
func TestPerSecretForce(t *testing.T) {
562+
t.Run("SecretForceOverwritesEnv", func(t *testing.T) {
563+
tmpDir := t.TempDir()
564+
envFile := filepath.Join(tmpDir, ".zshenv")
565+
t.Setenv("HOME", tmpDir)
566+
567+
existingContent := `export TEST_VAR="old_value"
568+
`
569+
err := os.WriteFile(envFile, []byte(existingContent), 0644)
570+
assert.NilError(t, err)
571+
572+
err = ProcessEnvSecret("TEST_VAR", []byte("new_value"), true)
573+
assert.NilError(t, err)
574+
575+
content, err := os.ReadFile(envFile)
576+
assert.NilError(t, err)
577+
578+
contentStr := string(content)
579+
assert.Assert(t, strings.Contains(contentStr, `export TEST_VAR="new_value"`))
580+
assert.Assert(t, !strings.Contains(contentStr, "old_value"))
581+
})
582+
583+
t.Run("SecretWithoutForceFailsOnExisting", func(t *testing.T) {
584+
tmpDir := t.TempDir()
585+
envFile := filepath.Join(tmpDir, ".zshenv")
586+
t.Setenv("HOME", tmpDir)
587+
588+
existingContent := `export TEST_VAR="old_value"
589+
`
590+
err := os.WriteFile(envFile, []byte(existingContent), 0644)
591+
assert.NilError(t, err)
592+
593+
err = ProcessEnvSecret("TEST_VAR", []byte("new_value"), false)
594+
assert.ErrorContains(t, err, `environment variable "TEST_VAR" already exists, use --force to overwrite`)
595+
})
596+
597+
t.Run("MixedForceInVault", func(t *testing.T) {
598+
tmpDir := t.TempDir()
599+
envFile := filepath.Join(tmpDir, ".zshenv")
600+
t.Setenv("HOME", tmpDir)
601+
602+
existingContent := `export VAR1="old1"
603+
export VAR2="old2"
604+
`
605+
err := os.WriteFile(envFile, []byte(existingContent), 0644)
606+
assert.NilError(t, err)
607+
608+
err = ProcessEnvSecret("VAR1", []byte("new1"), true)
609+
assert.NilError(t, err)
610+
611+
err = ProcessEnvSecret("VAR2", []byte("new2"), false)
612+
assert.ErrorContains(t, err, `environment variable "VAR2" already exists, use --force to overwrite`)
613+
614+
content, err := os.ReadFile(envFile)
615+
assert.NilError(t, err)
616+
617+
contentStr := string(content)
618+
assert.Assert(t, strings.Contains(contentStr, `export VAR1="new1"`))
619+
assert.Assert(t, strings.Contains(contentStr, `export VAR2="old2"`))
620+
})
621+
}

0 commit comments

Comments
 (0)