-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add alicloud secrets manager based secret store provider (#665)
* feat: add alicloud secrets manager based secret store provider * test: add unit tests for alicloud secrets manager provider
- Loading branch information
Showing
7 changed files
with
434 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
pkg/secrets/providers/alicloud/secretsmanager/fake/fake.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package fake | ||
|
||
import ( | ||
"github.com/aliyun/aliyun-secretsmanager-client-go/sdk/models" | ||
) | ||
|
||
type ( | ||
GetSecretInfoFn func(secretName string) (*models.SecretInfo, error) | ||
SecretsManagerClient struct { | ||
GetSecretInfoFn GetSecretInfoFn | ||
} | ||
) | ||
|
||
func NewGetSecretInfoFn(secretValue interface{}, secretDataType string, err error) GetSecretInfoFn { | ||
return func(secretName string) (*models.SecretInfo, error) { | ||
if secretValue == nil { | ||
return nil, err | ||
} | ||
if secretDataType == "text" { | ||
secretString := secretValue.(string) | ||
return &models.SecretInfo{ | ||
SecretValue: secretString, | ||
}, err | ||
} | ||
if secretDataType == "binary" { | ||
secretBinary := secretValue.([]byte) | ||
return &models.SecretInfo{ | ||
SecretValueByteBuffer: secretBinary, | ||
}, err | ||
} | ||
|
||
return nil, err | ||
} | ||
} | ||
|
||
func (sc *SecretsManagerClient) GetSecretInfo(secretName string) (*models.SecretInfo, error) { | ||
return sc.GetSecretInfoFn(secretName) | ||
} |
10 changes: 10 additions & 0 deletions
10
pkg/secrets/providers/alicloud/secretsmanager/interface.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package secretsmanager | ||
|
||
import ( | ||
"github.com/aliyun/aliyun-secretsmanager-client-go/sdk/models" | ||
) | ||
|
||
// Client is a testable interface for making operations call for Alicloud Secrets Manager. | ||
type Client interface { | ||
GetSecretInfo(secretName string) (*models.SecretInfo, error) | ||
} |
121 changes: 121 additions & 0 deletions
121
pkg/secrets/providers/alicloud/secretsmanager/secretsmanager.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package secretsmanager | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"strings" | ||
|
||
secretsapi "kusionstack.io/kusion/pkg/apis/secrets" | ||
"kusionstack.io/kusion/pkg/secrets" | ||
|
||
"github.com/aliyun/aliyun-secretsmanager-client-go/sdk" | ||
"github.com/aliyun/aliyun-secretsmanager-client-go/sdk/models" | ||
"github.com/aliyun/aliyun-secretsmanager-client-go/sdk/service" | ||
"github.com/tidwall/gjson" | ||
) | ||
|
||
const ( | ||
errMissingProviderSpec = "store spec is missing provider" | ||
errMissingAlicloudProvider = "invalid provider spec. Missing Alicloud field in store provider spec" | ||
errFailedToCreateClient = "failed to create Alicloud Secrets Manager client: %w" | ||
) | ||
|
||
var ( | ||
accessKeyID = os.Getenv("credentials_access_key_id") | ||
accessKeySecret = os.Getenv("credentials_access_secret") | ||
) | ||
|
||
// DefaultFactory should implement the secrets.SecretStoreFactory interface. | ||
var _ secrets.SecretStoreFactory = &DefaultFactory{} | ||
|
||
// smSecretStore should implement the secrets.SecretStore interface. | ||
var _ secrets.SecretStore = &smSecretStore{} | ||
|
||
// DefaultFactory implements the secrets.SecretStoreFactory interface. | ||
type DefaultFactory struct{} | ||
|
||
// smSecretStore implements the secrets.SecretStore interface. | ||
type smSecretStore struct { | ||
client Client | ||
} | ||
|
||
// NewSecretStore constructs a Vault based secret store with specific secret store spec. | ||
func (p *DefaultFactory) NewSecretStore(spec secretsapi.SecretStoreSpec) (secrets.SecretStore, error) { | ||
providerSpec := spec.Provider | ||
if providerSpec == nil { | ||
return nil, fmt.Errorf(errMissingProviderSpec) | ||
} | ||
if providerSpec.Alicloud == nil { | ||
return nil, fmt.Errorf(errMissingAlicloudProvider) | ||
} | ||
|
||
client, err := getAlicloudClient(providerSpec.Alicloud.Region) | ||
if err != nil { | ||
return nil, fmt.Errorf(errFailedToCreateClient, err) | ||
} | ||
|
||
return &smSecretStore{ | ||
client: client, | ||
}, nil | ||
} | ||
|
||
// getAlicloudClient returns an Alicloud Secrets Manager client with the specified region. | ||
// Ref: https://github.com/aliyun/aliyun-secretsmanager-client-go/blob/v1.1.4/README.md | ||
func getAlicloudClient(region string) (*sdk.SecretManagerCacheClient, error) { | ||
return sdk.NewSecretCacheClientBuilder( | ||
service.NewDefaultSecretManagerClientBuilder().Standard().WithAccessKey( | ||
accessKeyID, accessKeySecret, | ||
).WithRegion(region).Build()).Build() | ||
} | ||
|
||
// GetSecret retrieves ref secret value from Alicloud Secrets Manager. | ||
func (s *smSecretStore) GetSecret(ctx context.Context, ref secretsapi.ExternalSecretRef) ([]byte, error) { | ||
secretInfo, err := s.client.GetSecretInfo(ref.Name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if ref.Property == "" { | ||
if secretInfo.SecretValue != "" { | ||
return []byte(secretInfo.SecretValue), nil | ||
} | ||
if secretInfo.SecretValueByteBuffer != nil { | ||
return secretInfo.SecretValueByteBuffer, nil | ||
} | ||
return nil, fmt.Errorf("invalid secret data. no secret value string nor binary for key: %s", ref.Name) | ||
} | ||
val := s.convertSecretToGjson(secretInfo, ref.Property) | ||
if !val.Exists() { | ||
return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Name) | ||
} | ||
return []byte(val.String()), nil | ||
} | ||
|
||
func (s *smSecretStore) convertSecretToGjson(secretInfo *models.SecretInfo, refProperty string) gjson.Result { | ||
var payload string | ||
if secretInfo.SecretValue != "" { | ||
payload = secretInfo.SecretValue | ||
} | ||
if secretInfo.SecretValueByteBuffer != nil { | ||
payload = string(secretInfo.SecretValueByteBuffer) | ||
} | ||
|
||
// We need to search if a given key with a . exists before using gjson operations. | ||
idx := strings.Index(refProperty, ".") | ||
currentRefProperty := refProperty | ||
if idx > -1 { | ||
currentRefProperty = strings.ReplaceAll(refProperty, ".", "\\.") | ||
val := gjson.Get(payload, currentRefProperty) | ||
if !val.Exists() { | ||
currentRefProperty = refProperty | ||
} | ||
} | ||
|
||
return gjson.Get(payload, currentRefProperty) | ||
} | ||
|
||
func init() { | ||
secrets.SecretStoreProviders.Register(&DefaultFactory{}, &secretsapi.ProviderSpec{ | ||
Alicloud: &secretsapi.AlicloudProvider{}, | ||
}) | ||
} |
167 changes: 167 additions & 0 deletions
167
pkg/secrets/providers/alicloud/secretsmanager/secretsmanager_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package secretsmanager | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
|
||
secretsapi "kusionstack.io/kusion/pkg/apis/secrets" | ||
"kusionstack.io/kusion/pkg/secrets/providers/alicloud/secretsmanager/fake" | ||
) | ||
|
||
func TestGetSecret(t *testing.T) { | ||
testCases := map[string]struct { | ||
client Client | ||
name string | ||
property string | ||
expected []byte | ||
expectedErr error | ||
}{ | ||
"GetSecret": { | ||
client: &fake.SecretsManagerClient{ | ||
GetSecretInfoFn: fake.NewGetSecretInfoFn("t0p-Secret", "text", nil), | ||
}, | ||
name: "/beep", | ||
expected: []byte(`t0p-Secret`), | ||
expectedErr: nil, | ||
}, | ||
"GetSecret_With_Property": { | ||
client: &fake.SecretsManagerClient{ | ||
GetSecretInfoFn: fake.NewGetSecretInfoFn(`{"bar": "bang"}`, "text", nil), | ||
}, | ||
name: "/beep", | ||
property: "bar", | ||
expected: []byte(`bang`), | ||
expectedErr: nil, | ||
}, | ||
"GetSecret_With_NestedProperty": { | ||
client: &fake.SecretsManagerClient{ | ||
GetSecretInfoFn: fake.NewGetSecretInfoFn(`{"foobar":{"bar":"bang"}}`, "text", nil), | ||
}, | ||
name: "/beep", | ||
property: "foobar.bar", | ||
expected: []byte(`bang`), | ||
expectedErr: nil, | ||
}, | ||
"GetSecret_With_Binary": { | ||
client: &fake.SecretsManagerClient{ | ||
GetSecretInfoFn: fake.NewGetSecretInfoFn([]byte(`t0p-Secret`), "binary", nil), | ||
}, | ||
name: "/beep", | ||
expected: []byte(`t0p-Secret`), | ||
expectedErr: nil, | ||
}, | ||
"GetSecret_With_Property_Binary": { | ||
client: &fake.SecretsManagerClient{ | ||
GetSecretInfoFn: fake.NewGetSecretInfoFn([]byte(`{"bar":"bang"}`), "binary", nil), | ||
}, | ||
name: "/beep", | ||
property: "bar", | ||
expected: []byte(`bang`), | ||
expectedErr: nil, | ||
}, | ||
"GetSecret_With_NestedProperty_Binary": { | ||
client: &fake.SecretsManagerClient{ | ||
GetSecretInfoFn: fake.NewGetSecretInfoFn([]byte(`{"foobar":{"bar":"bang"}}`), "binary", nil), | ||
}, | ||
name: "/beep", | ||
property: "foobar.bar", | ||
expected: []byte(`bang`), | ||
expectedErr: nil, | ||
}, | ||
"GetSecret_With_Error": { | ||
client: &fake.SecretsManagerClient{ | ||
GetSecretInfoFn: fake.NewGetSecretInfoFn([]byte(`{"foobar":{"bar":"bang"}}`), "binary", errors.New("internal error")), | ||
}, | ||
name: "/beep", | ||
expected: nil, | ||
expectedErr: errors.New("internal error"), | ||
}, | ||
"GetSecret_Property_NotFound": { | ||
client: &fake.SecretsManagerClient{ | ||
GetSecretInfoFn: fake.NewGetSecretInfoFn([]byte(`{"foobar":{"bar":"bang"}}`), "binary", nil), | ||
}, | ||
name: "/beep", | ||
property: "foobar.baz", | ||
expected: nil, | ||
expectedErr: fmt.Errorf("key foobar.baz does not exist in secret /beep"), | ||
}, | ||
} | ||
|
||
for name, tc := range testCases { | ||
store := &smSecretStore{client: tc.client} | ||
ref := secretsapi.ExternalSecretRef{ | ||
Name: tc.name, | ||
Property: tc.property, | ||
} | ||
if name != "GetSecret_Property_NotFound" { | ||
continue | ||
} | ||
actual, err := store.GetSecret(context.TODO(), ref) | ||
if diff := cmp.Diff(err, tc.expectedErr, EquateErrors()); diff != "" { | ||
t.Errorf("\n%s\ngot unexpected error:\n%s", name, diff) | ||
} | ||
if diff := cmp.Diff(string(actual), string(tc.expected)); diff != "" { | ||
fmt.Println(diff) | ||
t.Errorf("\n%s\nget unexpected data: \n%s", name, diff) | ||
} | ||
} | ||
} | ||
|
||
func TestNewSecretStore(t *testing.T) { | ||
testCases := map[string]struct { | ||
spec secretsapi.SecretStoreSpec | ||
expectedErr error | ||
}{ | ||
"InvalidSecretStoreSpec": { | ||
spec: secretsapi.SecretStoreSpec{}, | ||
expectedErr: errors.New(errMissingProviderSpec), | ||
}, | ||
"InvalidProviderSpec": { | ||
spec: secretsapi.SecretStoreSpec{ | ||
Provider: &secretsapi.ProviderSpec{}, | ||
}, | ||
expectedErr: errors.New(errMissingAlicloudProvider), | ||
}, | ||
"ValidVaultProviderSpec": { | ||
spec: secretsapi.SecretStoreSpec{ | ||
Provider: &secretsapi.ProviderSpec{ | ||
Alicloud: &secretsapi.AlicloudProvider{ | ||
Region: "cn-beijing", | ||
}, | ||
}, | ||
}, | ||
expectedErr: nil, | ||
}, | ||
} | ||
|
||
factory := DefaultFactory{} | ||
for name, tc := range testCases { | ||
_, err := factory.NewSecretStore(tc.spec) | ||
if diff := cmp.Diff(err, tc.expectedErr, EquateErrors()); diff != "" { | ||
t.Errorf("\n%s\ngot unexpected error: \n%s", name, diff) | ||
} | ||
} | ||
} | ||
|
||
// EquateErrors returns true if the supplied errors are of the same type and | ||
// produce same error message. | ||
func EquateErrors() cmp.Option { | ||
return cmp.Comparer(func(a, b error) bool { | ||
if a == nil || b == nil { | ||
return a == nil && b == nil | ||
} | ||
|
||
av := reflect.ValueOf(a) | ||
bv := reflect.ValueOf(b) | ||
if av.Type() != bv.Type() { | ||
return false | ||
} | ||
|
||
return a.Error() == b.Error() | ||
}) | ||
} |