Skip to content

Commit 450e0ec

Browse files
authored
feat: implement none secrets provider for Kubernetes environments (#677)
1 parent a2dbc8d commit 450e0ec

File tree

6 files changed

+214
-4
lines changed

6 files changed

+214
-4
lines changed

cmd/thv/app/common.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func IsOIDCEnabled(cmd *cobra.Command) bool {
3838

3939
// SetSecretsProvider sets the secrets provider type in the configuration.
4040
// It validates the input and updates the configuration.
41-
// Choices are `encrypted` and `1password`.
41+
// Choices are `encrypted`, `1password`, and `none`.
4242
func SetSecretsProvider(provider secrets.ProviderType) error {
4343

4444
// Validate input
@@ -51,9 +51,10 @@ func SetSecretsProvider(provider secrets.ProviderType) error {
5151
switch provider {
5252
case secrets.EncryptedType:
5353
case secrets.OnePasswordType:
54+
case secrets.NoneType:
5455
// Valid provider type
5556
default:
56-
return fmt.Errorf("invalid secrets provider type: %s (valid types: encrypted, 1password)", provider)
57+
return fmt.Errorf("invalid secrets provider type: %s (valid types: encrypted, 1password, none)", provider)
5758
}
5859

5960
// Update the secrets provider type

pkg/config/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ func validateProviderType(provider string) (secrets.ProviderType, error) {
4242
return secrets.EncryptedType, nil
4343
case string(secrets.OnePasswordType):
4444
return secrets.OnePasswordType, nil
45+
case string(secrets.NoneType):
46+
return secrets.NoneType, nil
4547
default:
46-
return "", fmt.Errorf("invalid secrets provider type: %s (valid types: encrypted, 1password)", provider)
48+
return "", fmt.Errorf("invalid secrets provider type: %s (valid types: encrypted, 1password, none)", provider)
4749
}
4850
}
4951

pkg/config/config_test.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,20 @@ func TestSecrets_GetProviderType_EnvironmentVariable(t *testing.T) {
262262
require.NoError(t, err)
263263
assert.Equal(t, secrets.OnePasswordType, got, "Should fallback to config value when env var is unset")
264264

265-
// Test 3: Invalid environment variable returns error
265+
// Test 3: None provider via environment variable
266+
os.Setenv(secrets.ProviderEnvVar, "none")
267+
got, err = s.GetProviderType()
268+
require.NoError(t, err)
269+
assert.Equal(t, secrets.NoneType, got, "Environment variable should support none provider")
270+
271+
// Test 4: None provider via config
272+
os.Unsetenv(secrets.ProviderEnvVar)
273+
s.ProviderType = "none"
274+
got, err = s.GetProviderType()
275+
require.NoError(t, err)
276+
assert.Equal(t, secrets.NoneType, got, "Config should support none provider")
277+
278+
// Test 5: Invalid environment variable returns error
266279
os.Setenv(secrets.ProviderEnvVar, "invalid")
267280
_, err = s.GetProviderType()
268281
assert.Error(t, err, "Should return error for invalid environment variable")

pkg/secrets/factory.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const (
3333

3434
// OnePasswordType represents the 1Password secret provider.
3535
OnePasswordType ProviderType = "1password"
36+
37+
// NoneType represents the none secret provider.
38+
NoneType ProviderType = "none"
3639
)
3740

3841
// ErrUnknownManagerType is returned when an invalid value for ProviderType is specified.
@@ -55,6 +58,8 @@ func CreateSecretProvider(managerType ProviderType) (Provider, error) {
5558
return NewEncryptedManager(secretsPath, key[:])
5659
case OnePasswordType:
5760
return NewOnePasswordManager()
61+
case NoneType:
62+
return NewNoneManager()
5863
default:
5964
return nil, ErrUnknownManagerType
6065
}

pkg/secrets/none.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package secrets
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
)
8+
9+
// NoneManager is a no-op secrets provider that doesn't store or retrieve secrets.
10+
// It's designed for use in Kubernetes environments where secrets are provided
11+
// as environment variables or file mounts, eliminating the need for interactive
12+
// password prompts.
13+
type NoneManager struct{}
14+
15+
// GetSecret always returns an error indicating that the none provider doesn't support secret retrieval.
16+
func (*NoneManager) GetSecret(_ context.Context, name string) (string, error) {
17+
if name == "" {
18+
return "", errors.New("secret name cannot be empty")
19+
}
20+
return "", fmt.Errorf("secret not found: %s (none provider doesn't store secrets)", name)
21+
}
22+
23+
// SetSecret always returns an error indicating that the none provider doesn't support secret storage.
24+
func (*NoneManager) SetSecret(_ context.Context, name, _ string) error {
25+
if name == "" {
26+
return errors.New("secret name cannot be empty")
27+
}
28+
return errors.New("none provider doesn't support storing secrets")
29+
}
30+
31+
// DeleteSecret always returns an error indicating that the none provider doesn't support secret deletion.
32+
func (*NoneManager) DeleteSecret(_ context.Context, name string) error {
33+
if name == "" {
34+
return errors.New("secret name cannot be empty")
35+
}
36+
return fmt.Errorf("cannot delete non-existent secret: %s (none provider doesn't store secrets)", name)
37+
}
38+
39+
// ListSecrets returns an empty list since the none provider doesn't store any secrets.
40+
func (*NoneManager) ListSecrets(_ context.Context) ([]SecretDescription, error) {
41+
return []SecretDescription{}, nil
42+
}
43+
44+
// Cleanup is a no-op for the none provider since there's nothing to clean up.
45+
func (*NoneManager) Cleanup() error {
46+
return nil
47+
}
48+
49+
// Capabilities returns the capabilities of the none provider.
50+
// The none provider is essentially read-only but doesn't actually read anything.
51+
func (*NoneManager) Capabilities() ProviderCapabilities {
52+
return ProviderCapabilities{
53+
CanRead: false,
54+
CanWrite: false,
55+
CanDelete: false,
56+
CanList: true,
57+
CanCleanup: true,
58+
}
59+
}
60+
61+
// NewNoneManager creates an instance of NoneManager.
62+
func NewNoneManager() (Provider, error) {
63+
return &NoneManager{}, nil
64+
}

pkg/secrets/none_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package secrets
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestNewNoneManager(t *testing.T) {
12+
t.Parallel()
13+
manager, err := NewNoneManager()
14+
require.NoError(t, err)
15+
assert.NotNil(t, manager)
16+
}
17+
18+
func TestNoneManager_GetSecret(t *testing.T) {
19+
t.Parallel()
20+
manager, err := NewNoneManager()
21+
require.NoError(t, err)
22+
23+
ctx := context.Background()
24+
25+
// Test with valid name
26+
secret, err := manager.GetSecret(ctx, "test-secret")
27+
assert.Error(t, err)
28+
assert.Empty(t, secret)
29+
assert.Contains(t, err.Error(), "secret not found: test-secret")
30+
assert.Contains(t, err.Error(), "none provider doesn't store secrets")
31+
32+
// Test with empty name
33+
secret, err = manager.GetSecret(ctx, "")
34+
assert.Error(t, err)
35+
assert.Empty(t, secret)
36+
assert.Contains(t, err.Error(), "secret name cannot be empty")
37+
}
38+
39+
func TestNoneManager_SetSecret(t *testing.T) {
40+
t.Parallel()
41+
manager, err := NewNoneManager()
42+
require.NoError(t, err)
43+
44+
ctx := context.Background()
45+
46+
// Test with valid name and value
47+
err = manager.SetSecret(ctx, "test-secret", "test-value")
48+
assert.Error(t, err)
49+
assert.Contains(t, err.Error(), "none provider doesn't support storing secrets")
50+
51+
// Test with empty name
52+
err = manager.SetSecret(ctx, "", "test-value")
53+
assert.Error(t, err)
54+
assert.Contains(t, err.Error(), "secret name cannot be empty")
55+
}
56+
57+
func TestNoneManager_DeleteSecret(t *testing.T) {
58+
t.Parallel()
59+
manager, err := NewNoneManager()
60+
require.NoError(t, err)
61+
62+
ctx := context.Background()
63+
64+
// Test with valid name
65+
err = manager.DeleteSecret(ctx, "test-secret")
66+
assert.Error(t, err)
67+
assert.Contains(t, err.Error(), "cannot delete non-existent secret: test-secret")
68+
assert.Contains(t, err.Error(), "none provider doesn't store secrets")
69+
70+
// Test with empty name
71+
err = manager.DeleteSecret(ctx, "")
72+
assert.Error(t, err)
73+
assert.Contains(t, err.Error(), "secret name cannot be empty")
74+
}
75+
76+
func TestNoneManager_ListSecrets(t *testing.T) {
77+
t.Parallel()
78+
manager, err := NewNoneManager()
79+
require.NoError(t, err)
80+
81+
ctx := context.Background()
82+
83+
secrets, err := manager.ListSecrets(ctx)
84+
assert.NoError(t, err)
85+
assert.Empty(t, secrets)
86+
assert.Equal(t, []SecretDescription{}, secrets)
87+
}
88+
89+
func TestNoneManager_Cleanup(t *testing.T) {
90+
t.Parallel()
91+
manager, err := NewNoneManager()
92+
require.NoError(t, err)
93+
94+
err = manager.Cleanup()
95+
assert.NoError(t, err)
96+
}
97+
98+
func TestNoneManager_Capabilities(t *testing.T) {
99+
t.Parallel()
100+
manager, err := NewNoneManager()
101+
require.NoError(t, err)
102+
103+
capabilities := manager.Capabilities()
104+
assert.False(t, capabilities.CanRead)
105+
assert.False(t, capabilities.CanWrite)
106+
assert.False(t, capabilities.CanDelete)
107+
assert.True(t, capabilities.CanList)
108+
assert.True(t, capabilities.CanCleanup)
109+
110+
// Test capability helper methods
111+
assert.False(t, capabilities.IsReadOnly())
112+
assert.False(t, capabilities.IsReadWrite())
113+
assert.Equal(t, "custom", capabilities.String())
114+
}
115+
116+
func TestCreateSecretProvider_None(t *testing.T) {
117+
t.Parallel()
118+
provider, err := CreateSecretProvider(NoneType)
119+
require.NoError(t, err)
120+
assert.NotNil(t, provider)
121+
122+
// Verify it's actually a NoneManager
123+
_, ok := provider.(*NoneManager)
124+
assert.True(t, ok)
125+
}

0 commit comments

Comments
 (0)