From 8dce43b413e503e961be6c599a8e771504f17d4b Mon Sep 17 00:00:00 2001 From: TonyAdo <71679464+adohe@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:33:49 +0800 Subject: [PATCH] feat: update workspace type add secret store config (#724) --- pkg/apis/core/v1/workspace.go | 3 + pkg/workspace/validation.go | 95 +++++++++++++++ pkg/workspace/validation_test.go | 197 +++++++++++++++++++++++++++++++ 3 files changed, 295 insertions(+) diff --git a/pkg/apis/core/v1/workspace.go b/pkg/apis/core/v1/workspace.go index 9b86d917..58e8111b 100644 --- a/pkg/apis/core/v1/workspace.go +++ b/pkg/apis/core/v1/workspace.go @@ -35,6 +35,9 @@ type Workspace struct { // Backends are the configs of a set of backends. Backends *BackendConfigs `yaml:"backends,omitempty" json:"backends,omitempty"` + + // SecretStore represents a secure external location for storing secrets. + SecretStore *SecretStoreSpec `yaml:"secretStore,omitempty" json:"secretStore,omitempty"` } // ModuleConfigs is a set of multiple ModuleConfig, whose key is the module name. diff --git a/pkg/workspace/validation.go b/pkg/workspace/validation.go index 7d537028..81054726 100644 --- a/pkg/workspace/validation.go +++ b/pkg/workspace/validation.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "kusionstack.io/kusion/pkg/apis/core/v1" ) @@ -40,6 +42,15 @@ var ( ErrEmptyAccessKeySecret = errors.New("empty access key secret") ErrEmptyOssEndpoint = errors.New("empty oss endpoint") ErrEmptyS3Region = errors.New("empty s3 region") + + ErrMissingProvider = errors.New("invalid secret store spec, missing provider config") + ErrMultiSecretStoreProviders = errors.New("may not specify more than 1 secret store provider") + ErrEmptyAWSRegion = errors.New("region must be provided when using AWS Secrets Manager") + ErrEmptyVaultServer = errors.New("server address must be provided when using Hashicorp Vault") + ErrEmptyVaultURL = errors.New("vault url must be provided when using Azure KeyVault") + ErrEmptyTenantID = errors.New("azure tenant id must be provided when using Azure KeyVault") + ErrEmptyAlicloudRegion = errors.New("region must be provided when using Alicloud Secrets Manager") + ErrMissingProviderType = errors.New("must specify a provider type") ) // ValidateWorkspace is used to validate the workspace get or set in the storage, and does not validate the @@ -63,6 +74,11 @@ func ValidateWorkspace(ws *v1.Workspace) error { return err } } + if ws.SecretStore != nil { + if allErrs := ValidateSecretStoreConfig(ws.SecretStore); allErrs != nil { + return utilerrors.NewAggregate(allErrs) + } + } return nil } @@ -319,3 +335,82 @@ func validateWholeGenericObjectStorageConfig(config *v1.GenericObjectStorageConf } return nil } + +// ValidateSecretStoreConfig tests that the specified SecretStoreSpec has valid data. +func ValidateSecretStoreConfig(spec *v1.SecretStoreSpec) []error { + if spec.Provider == nil { + return []error{ErrMissingProvider} + } + + numProviders := 0 + var allErrs []error + if spec.Provider.AWS != nil { + numProviders++ + allErrs = append(allErrs, validateAWSSecretStore(spec.Provider.AWS)...) + } + if spec.Provider.Vault != nil { + if numProviders > 0 { + allErrs = append(allErrs, ErrMultiSecretStoreProviders) + } else { + numProviders++ + allErrs = append(allErrs, validateHashiVaultSecretStore(spec.Provider.Vault)...) + } + } + if spec.Provider.Azure != nil { + if numProviders > 0 { + allErrs = append(allErrs, ErrMultiSecretStoreProviders) + } else { + numProviders++ + allErrs = append(allErrs, validateAzureKeyVaultSecretStore(spec.Provider.Azure)...) + } + } + if spec.Provider.Alicloud != nil { + if numProviders > 0 { + allErrs = append(allErrs, ErrMultiSecretStoreProviders) + } else { + numProviders++ + allErrs = append(allErrs, validateAlicloudSecretStore(spec.Provider.Alicloud)...) + } + } + + if numProviders == 0 { + allErrs = append(allErrs, ErrMissingProviderType) + } + + return allErrs +} + +func validateAWSSecretStore(ss *v1.AWSProvider) []error { + var allErrs []error + if len(ss.Region) == 0 { + allErrs = append(allErrs, ErrEmptyAWSRegion) + } + return allErrs +} + +func validateHashiVaultSecretStore(vault *v1.VaultProvider) []error { + var allErrs []error + if len(vault.Server) == 0 { + allErrs = append(allErrs, ErrEmptyVaultServer) + } + return allErrs +} + +func validateAzureKeyVaultSecretStore(azureKv *v1.AzureKVProvider) []error { + var allErrs []error + if azureKv.VaultURL == nil || len(*azureKv.VaultURL) == 0 { + allErrs = append(allErrs, ErrEmptyVaultURL) + } + if azureKv.TenantID == nil || len(*azureKv.TenantID) == 0 { + allErrs = append(allErrs, ErrEmptyTenantID) + } + return allErrs +} + +func validateAlicloudSecretStore(ac *v1.AlicloudProvider) []error { + var allErrs []error + if len(ac.Region) == 0 { + allErrs = append(allErrs, ErrEmptyAlicloudRegion) + } + return allErrs +} diff --git a/pkg/workspace/validation_test.go b/pkg/workspace/validation_test.go index f8dae81a..65757fa2 100644 --- a/pkg/workspace/validation_test.go +++ b/pkg/workspace/validation_test.go @@ -665,3 +665,200 @@ func TestValidateWholeS3Config(t *testing.T) { }) } } + +func TestValidateAWSSecretStore(t *testing.T) { + type args struct { + ss *v1.AWSProvider + } + tests := []struct { + name string + args args + want []error + }{ + { + name: "valid AWS provider spec", + args: args{ + ss: &v1.AWSProvider{ + Region: "eu-west-2", + }, + }, + want: nil, + }, + { + name: "invalid AWS provider spec", + args: args{ + ss: &v1.AWSProvider{}, + }, + want: []error{ErrEmptyAWSRegion}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, validateAWSSecretStore(tt.args.ss), "validateAWSSecretStore(%v)", tt.args.ss) + }) + } +} + +func TestValidateHashiVaultSecretStore(t *testing.T) { + type args struct { + vault *v1.VaultProvider + } + tests := []struct { + name string + args args + want []error + }{ + { + name: "valid Hashi Vault provider spec", + args: args{ + vault: &v1.VaultProvider{ + Server: "https://vault.example.com:8200", + }, + }, + want: nil, + }, + { + name: "invalid Hashi Vault provider spec", + args: args{ + vault: &v1.VaultProvider{}, + }, + want: []error{ErrEmptyVaultServer}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, validateHashiVaultSecretStore(tt.args.vault), "validateHashiVaultSecretStore(%v)", tt.args.vault) + }) + } +} + +func TestValidateAzureKeyVaultSecretStore(t *testing.T) { + type args struct { + azureKv *v1.AzureKVProvider + } + vaultURL := "https://local.vault.url" + tenantID := "my-tenant-id" + tests := []struct { + name string + args args + want []error + }{ + { + name: "valid Azure KV provider spec", + args: args{ + azureKv: &v1.AzureKVProvider{ + VaultURL: &vaultURL, + TenantID: &tenantID, + }, + }, + want: nil, + }, + { + name: "invalid Azure KV provider spec", + args: args{ + azureKv: &v1.AzureKVProvider{}, + }, + want: []error{ErrEmptyVaultURL, ErrEmptyTenantID}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, validateAzureKeyVaultSecretStore(tt.args.azureKv), "validateAzureKeyVaultSecretStore(%v)", tt.args.azureKv) + }) + } +} + +func TestValidateAlicloudSecretStore(t *testing.T) { + type args struct { + ac *v1.AlicloudProvider + } + tests := []struct { + name string + args args + want []error + }{ + { + name: "valid Alicloud provider spec", + args: args{ + ac: &v1.AlicloudProvider{ + Region: "sh", + }, + }, + want: nil, + }, + { + name: "invalid Alicloud provider spec", + args: args{ + ac: &v1.AlicloudProvider{}, + }, + want: []error{ErrEmptyAlicloudRegion}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, validateAlicloudSecretStore(tt.args.ac), "validateAlicloudSecretStore(%v)", tt.args.ac) + }) + } +} + +func TestValidateSecretStoreConfig(t *testing.T) { + type args struct { + spec *v1.SecretStoreSpec + } + tests := []struct { + name string + args args + want []error + }{ + { + name: "missing provider spec", + args: args{ + spec: &v1.SecretStoreSpec{}, + }, + want: []error{ErrMissingProvider}, + }, + { + name: "missing provider type", + args: args{ + spec: &v1.SecretStoreSpec{ + Provider: &v1.ProviderSpec{}, + }, + }, + want: []error{ErrMissingProviderType}, + }, + { + name: "multi secret store providers", + args: args{ + spec: &v1.SecretStoreSpec{ + Provider: &v1.ProviderSpec{ + AWS: &v1.AWSProvider{ + Region: "us-east-1", + }, + Vault: &v1.VaultProvider{ + Server: "https://vault.example.com:8200", + }, + }, + }, + }, + want: []error{ErrMultiSecretStoreProviders}, + }, + { + name: "valid secret store spec", + args: args{ + spec: &v1.SecretStoreSpec{ + Provider: &v1.ProviderSpec{ + AWS: &v1.AWSProvider{ + Region: "us-east-1", + }, + }, + }, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, ValidateSecretStoreConfig(tt.args.spec), "validateAlicloudSecretStore(%v)", tt.args.spec) + }) + } +}