diff --git a/pkg/modules/generators/app_configurations_generator.go b/pkg/modules/generators/app_configurations_generator.go index 2dbd90e6..685907c0 100644 --- a/pkg/modules/generators/app_configurations_generator.go +++ b/pkg/modules/generators/app_configurations_generator.go @@ -95,6 +95,7 @@ func (g *appConfigurationGenerator) Generate(i *apiv1.Intent) error { Namespace: namespaceName, ModuleInputs: modulesConfig, TerraformConfig: terraformConfig, + SecretStore: g.ws.SecretStore, } // Generate resources diff --git a/pkg/modules/generators/workload/secret/secret_generator.go b/pkg/modules/generators/workload/secret/secret_generator.go index b29d137e..92e146ee 100644 --- a/pkg/modules/generators/workload/secret/secret_generator.go +++ b/pkg/modules/generators/workload/secret/secret_generator.go @@ -1,21 +1,28 @@ package secret import ( + "context" + "errors" "fmt" + "net/url" + "strings" "golang.org/x/exp/maps" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" "kusionstack.io/kusion/pkg/modules" "kusionstack.io/kusion/pkg/modules/inputs/workload" + "kusionstack.io/kusion/pkg/secrets" ) type secretGenerator struct { - project *apiv1.Project - secrets map[string]workload.Secret - namespace string + project *apiv1.Project + namespace string + secrets map[string]workload.Secret + secretStore *apiv1.SecretStoreSpec } func NewSecretGenerator(ctx modules.GeneratorContext) (modules.Generator, error) { @@ -23,17 +30,18 @@ func NewSecretGenerator(ctx modules.GeneratorContext) (modules.Generator, error) return nil, fmt.Errorf("project name must not be empty") } - var secrets map[string]workload.Secret + var secretMap map[string]workload.Secret if ctx.Application.Workload.Service != nil { - secrets = ctx.Application.Workload.Service.Secrets + secretMap = ctx.Application.Workload.Service.Secrets } else { - secrets = ctx.Application.Workload.Job.Secrets + secretMap = ctx.Application.Workload.Job.Secrets } return &secretGenerator{ - project: ctx.Project, - secrets: secrets, - namespace: ctx.Namespace, + project: ctx.Project, + secrets: secretMap, + namespace: ctx.Namespace, + secretStore: ctx.SecretStore, }, nil } @@ -49,7 +57,7 @@ func (g *secretGenerator) Generate(spec *apiv1.Intent) error { } for secretName, secretRef := range g.secrets { - secret, err := generateSecret(g.namespace, secretName, secretRef) + secret, err := g.generateSecret(secretName, secretRef) if err != nil { return err } @@ -72,16 +80,18 @@ func (g *secretGenerator) Generate(spec *apiv1.Intent) error { // generateSecret generates target secret based on secret type. Most of these secret types are just semantic wrapper // of native Kubernetes secret types:https://kubernetes.io/docs/concepts/configuration/secret/#secret-types, and more // detailed usage info can be found in public documentation. -func generateSecret(namespace, secretName string, secretRef workload.Secret) (*v1.Secret, error) { +func (g *secretGenerator) generateSecret(secretName string, secretRef workload.Secret) (*v1.Secret, error) { switch secretRef.Type { case "basic": - return generateBasic(namespace, secretName, secretRef) + return g.generateBasic(secretName, secretRef) case "token": - return generateToken(namespace, secretName, secretRef) + return g.generateToken(secretName, secretRef) case "opaque": - return generateOpaque(namespace, secretName, secretRef) + return g.generateOpaque(secretName, secretRef) case "certificate": - return generateCertificate(namespace, secretName, secretRef) + return g.generateCertificate(secretName, secretRef) + case "external": + return g.generateSecretWithExternalProvider(secretName, secretRef) default: return nil, fmt.Errorf("unrecognized secret type %s", secretRef.Type) } @@ -89,20 +99,9 @@ func generateSecret(namespace, secretName string, secretRef workload.Secret) (*v // generateBasic generates secret used for basic authentication. The basic secret type // is used for username / password pairs. -func generateBasic(namespace, secretName string, secretRef workload.Secret) (*v1.Secret, error) { - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1.SchemeGroupVersion.String(), - Kind: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - }, - Data: grabData(secretRef.Data, v1.BasicAuthUsernameKey, v1.BasicAuthPasswordKey), - Immutable: &secretRef.Immutable, - Type: v1.SecretTypeBasicAuth, - } +func (g *secretGenerator) generateBasic(secretName string, secretRef workload.Secret) (*v1.Secret, error) { + secret := initBasicSecret(g.namespace, secretName, v1.SecretTypeBasicAuth, secretRef.Immutable) + secret.Data = grabData(secretRef.Data, v1.BasicAuthUsernameKey, v1.BasicAuthPasswordKey) for _, key := range []string{v1.BasicAuthUsernameKey, v1.BasicAuthPasswordKey} { if len(secret.Data[key]) == 0 { @@ -116,20 +115,9 @@ func generateBasic(namespace, secretName string, secretRef workload.Secret) (*v1 // generateToken generates secret used for password. Token secrets are useful for generating // a password or secure string used for passwords when the user is already known or not required. -func generateToken(namespace, secretName string, secretRef workload.Secret) (*v1.Secret, error) { - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1.SchemeGroupVersion.String(), - Kind: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - }, - Data: grabData(secretRef.Data, "token"), - Immutable: &secretRef.Immutable, - Type: v1.SecretTypeOpaque, - } +func (g *secretGenerator) generateToken(secretName string, secretRef workload.Secret) (*v1.Secret, error) { + secret := initBasicSecret(g.namespace, secretName, v1.SecretTypeOpaque, secretRef.Immutable) + secret.Data = grabData(secretRef.Data, "token") if len(secret.Data["token"]) == 0 { v := GenerateRandomString(54) @@ -140,40 +128,58 @@ func generateToken(namespace, secretName string, secretRef workload.Secret) (*v1 } // generateOpaque generates secret used for arbitrary user-defined data. -func generateOpaque(namespace, secretName string, secretRef workload.Secret) (*v1.Secret, error) { - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1.SchemeGroupVersion.String(), - Kind: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - }, - Data: grabData(secretRef.Data, maps.Keys(secretRef.Data)...), - Immutable: &secretRef.Immutable, - Type: v1.SecretTypeOpaque, - } - +func (g *secretGenerator) generateOpaque(secretName string, secretRef workload.Secret) (*v1.Secret, error) { + secret := initBasicSecret(g.namespace, secretName, v1.SecretTypeOpaque, secretRef.Immutable) + secret.Data = grabData(secretRef.Data, maps.Keys(secretRef.Data)...) return secret, nil } // generateCertificate generates secret used for storing a certificate and its associated key. // One common use for TLS Secrets is to configure encryption in transit for an Ingress, but // you can also use it with other resources or directly in your workload. -func generateCertificate(namespace, secretName string, secretRef workload.Secret) (*v1.Secret, error) { - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1.SchemeGroupVersion.String(), - Kind: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - }, - Data: grabData(secretRef.Data, v1.TLSCertKey, v1.TLSPrivateKeyKey), - Immutable: &secretRef.Immutable, - Type: v1.SecretTypeTLS, +func (g *secretGenerator) generateCertificate(secretName string, secretRef workload.Secret) (*v1.Secret, error) { + secret := initBasicSecret(g.namespace, secretName, v1.SecretTypeTLS, secretRef.Immutable) + secret.Data = grabData(secretRef.Data, v1.TLSCertKey, v1.TLSPrivateKeyKey) + return secret, nil +} + +// generateSecretWithExternalProvider retrieves target sensitive information from external secret provider and +// generates corresponding Kubernetes Secret object. +func (g *secretGenerator) generateSecretWithExternalProvider(secretName string, secretRef workload.Secret) (*v1.Secret, error) { + if g.secretStore == nil { + return nil, errors.New("secret store is missing, please add valid secret store spec in workspace") + } + + secret := initBasicSecret(g.namespace, secretName, v1.SecretTypeOpaque, secretRef.Immutable) + secret.Data = make(map[string][]byte) + + var allErrs []error + for key, ref := range secretRef.Data { + externalSecretRef, err := parseExternalSecretDataRef(ref) + if err != nil { + allErrs = append(allErrs, err) + continue + } + provider, exist := secrets.GetProvider(g.secretStore.Provider) + if !exist { + allErrs = append(allErrs, errors.New("no matched secret store found, please check workspace yaml")) + continue + } + secretStore, err := provider.NewSecretStore(*g.secretStore) + if err != nil { + allErrs = append(allErrs, err) + continue + } + secretData, err := secretStore.GetSecret(context.Background(), *externalSecretRef) + if err != nil { + allErrs = append(allErrs, err) + continue + } + secret.Data[key] = secretData + } + + if allErrs != nil { + return nil, utilerrors.NewAggregate(allErrs) } return secret, nil @@ -192,3 +198,59 @@ func grabData(from map[string]string, keys ...string) map[string][]byte { } return to } + +// parseExternalSecretDataRef knows how to parse the remote data ref string, returns the +// corresponding ExternalSecretRef object. +func parseExternalSecretDataRef(dataRefStr string) (*apiv1.ExternalSecretRef, error) { + uri, err := url.Parse(dataRefStr) + if err != nil { + return nil, err + } + + ref := &apiv1.ExternalSecretRef{} + if len(uri.Path) > 0 { + partialName, property := parsePath(uri.Path) + if len(partialName) > 0 { + ref.Name = uri.Host + "/" + partialName + } else { + ref.Name = uri.Host + } + ref.Property = property + } else { + ref.Name = uri.Host + } + + query := uri.Query() + if len(query) > 0 && len(query.Get("version")) > 0 { + ref.Version = query.Get("version") + } + + return ref, nil +} + +func parsePath(path string) (partialName string, property string) { + pathParts := strings.Split(path, "/") + if len(pathParts) > 1 { + partialName = strings.Join(pathParts[1:len(pathParts)-1], "/") + property = pathParts[len(pathParts)-1] + } else { + property = pathParts[0] + } + return partialName, property +} + +func initBasicSecret(namespace, name string, secretType v1.SecretType, immutable bool) *v1.Secret { + secret := &v1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Immutable: &immutable, + Type: secretType, + } + return secret +} diff --git a/pkg/modules/generators/workload/secret/secret_generator_test.go b/pkg/modules/generators/workload/secret/secret_generator_test.go index de8b0a5e..49036eff 100644 --- a/pkg/modules/generators/workload/secret/secret_generator_test.go +++ b/pkg/modules/generators/workload/secret/secret_generator_test.go @@ -1,6 +1,7 @@ package secret import ( + "reflect" "testing" "github.com/stretchr/testify/require" @@ -108,3 +109,68 @@ func TestGenerateSecret(t *testing.T) { }) } } + +func TestParseExternalSecretDataRef(t *testing.T) { + tests := []struct { + name string + dataRefStr string + want *apiv1.ExternalSecretRef + wantErr bool + }{ + { + name: "invalid data ref string", + dataRefStr: "$%#//invalid", + want: nil, + wantErr: true, + }, + { + name: "only secret name", + dataRefStr: "ref://secret-name", + want: &apiv1.ExternalSecretRef{ + Name: "secret-name", + }, + wantErr: false, + }, + { + name: "secret name with version", + dataRefStr: "ref://secret-name?version=1", + want: &apiv1.ExternalSecretRef{ + Name: "secret-name", + Version: "1", + }, + wantErr: false, + }, + { + name: "secret name with property and version", + dataRefStr: "ref://secret-name/property?version=1", + want: &apiv1.ExternalSecretRef{ + Name: "secret-name", + Property: "property", + Version: "1", + }, + wantErr: false, + }, + { + name: "nested secret name with property and version", + dataRefStr: "ref://customer/acme/customer_name?version=1", + want: &apiv1.ExternalSecretRef{ + Name: "customer/acme", + Property: "customer_name", + Version: "1", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseExternalSecretDataRef(tt.dataRefStr) + if (err != nil) != tt.wantErr { + t.Errorf("parseExternalSecretDataRef() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseExternalSecretDataRef() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/modules/inputs/workload/secret.go b/pkg/modules/inputs/workload/secret.go index 9c4f3800..ab0eb506 100644 --- a/pkg/modules/inputs/workload/secret.go +++ b/pkg/modules/inputs/workload/secret.go @@ -2,6 +2,7 @@ package workload type Secret struct { Type string `yaml:"type" json:"type"` + Params map[string]string `yaml:"params,omitempty" json:"params,omitempty"` Data map[string]string `yaml:"data,omitempty" json:"data,omitempty"` Immutable bool `yaml:"immutable,omitempty" json:"immutable,omitempty"` } diff --git a/pkg/modules/interfaces.go b/pkg/modules/interfaces.go index 9a860946..2ea2882b 100644 --- a/pkg/modules/interfaces.go +++ b/pkg/modules/interfaces.go @@ -47,4 +47,7 @@ type GeneratorContext struct { // TerraformConfig is the collection of provider configs for the terraform runtime. TerraformConfig v1.TerraformConfig + + // SecretStore is the external secret store spec + SecretStore *v1.SecretStoreSpec } diff --git a/pkg/secrets/providers.go b/pkg/secrets/providers.go index f8d797f1..a9cff62a 100644 --- a/pkg/secrets/providers.go +++ b/pkg/secrets/providers.go @@ -34,6 +34,20 @@ func GetProviderByName(providerName string) (SecretStoreFactory, bool) { return secretStoreProviders.getProviderByName(providerName) } +// GetProvider returns the provider from the provider spec. +func GetProvider(spec *v1.ProviderSpec) (SecretStoreFactory, bool) { + if spec == nil { + return nil, false + } + + providerName, err := getProviderName(spec) + if err != nil { + return nil, false + } + + return secretStoreProviders.getProviderByName(providerName) +} + type Providers struct { lock sync.RWMutex registry map[string]SecretStoreFactory diff --git a/pkg/secrets/providers/register/register.go b/pkg/secrets/providers/register/register.go index bc262085..294791a4 100644 --- a/pkg/secrets/providers/register/register.go +++ b/pkg/secrets/providers/register/register.go @@ -5,5 +5,6 @@ package register import ( _ "kusionstack.io/kusion/pkg/secrets/providers/alicloud/secretsmanager" _ "kusionstack.io/kusion/pkg/secrets/providers/aws/secretsmanager" + _ "kusionstack.io/kusion/pkg/secrets/providers/azure/keyvault" _ "kusionstack.io/kusion/pkg/secrets/providers/hashivault" )