Skip to content

Commit

Permalink
refactor: update secret generator for upcoming external secret
Browse files Browse the repository at this point in the history
  • Loading branch information
adohe committed Nov 21, 2023
1 parent f59e4e0 commit d875967
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 217 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package secret

import (
"fmt"

"golang.org/x/exp/maps"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"kusionstack.io/kusion/pkg/generator/appconfiguration"
"kusionstack.io/kusion/pkg/models"
"kusionstack.io/kusion/pkg/models/appconfiguration/workload"
"kusionstack.io/kusion/pkg/projectstack"
)

type secretGenerator struct {
project *projectstack.Project
secrets map[string]workload.Secret
}

func NewSecretGenerator(
project *projectstack.Project,
secrets map[string]workload.Secret,
) (appconfiguration.Generator, error) {
if len(project.Name) == 0 {
return nil, fmt.Errorf("project name must not be empty")
}

return &secretGenerator{
project: project,
secrets: secrets,
}, nil
}

func NewSecretGeneratorFunc(
project *projectstack.Project,
secrets map[string]workload.Secret,
) appconfiguration.NewGeneratorFunc {
return func() (appconfiguration.Generator, error) {
return NewSecretGenerator(project, secrets)
}
}

func (g *secretGenerator) Generate(spec *models.Intent) error {
if spec.Resources == nil {
spec.Resources = make(models.Resources, 0)
}

for secretName, secretRef := range g.secrets {
secret, err := generateSecret(g.project, secretName, secretRef)
if err != nil {
return err
}

resourceID := appconfiguration.KubernetesResourceID(secret.TypeMeta, secret.ObjectMeta)
err = appconfiguration.AppendToSpec(
models.Kubernetes,
resourceID,
spec,
secret,
)
if err != nil {
return err
}
}

return nil
}

// 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(project *projectstack.Project, secretName string, secretRef workload.Secret) (*v1.Secret, error) {
switch secretRef.Type {
case "basic":
return generateBasic(project, secretName, secretRef)
case "token":
return generateToken(project, secretName, secretRef)
case "opaque":
return generateOpaque(project, secretName, secretRef)
case "certificate":
return generateCertificate(project, secretName, secretRef)
default:
return nil, fmt.Errorf("unrecognized secret type %s", secretRef.Type)
}
}

func generateBasic(project *projectstack.Project, 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: project.Name,
},
Data: grabData(secretRef.Data, v1.BasicAuthUsernameKey, v1.BasicAuthPasswordKey),
Immutable: &secretRef.Immutable,
Type: v1.SecretTypeBasicAuth,
}

for _, key := range []string{v1.BasicAuthUsernameKey, v1.BasicAuthPasswordKey} {
if len(secret.Data[key]) == 0 {
v := GenerateRandomString(54)
secret.Data[key] = []byte(v)
}
}

return secret, nil
}

func generateToken(project *projectstack.Project, 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: project.Name,
},
Data: grabData(secretRef.Data, "token"),
Immutable: &secretRef.Immutable,
Type: v1.SecretTypeOpaque,
}

if len(secret.Data["token"]) == 0 {
v := GenerateRandomString(54)
secret.Data["token"] = []byte(v)
}

return secret, nil
}

func generateOpaque(project *projectstack.Project, 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: project.Name,
},
Data: grabData(secretRef.Data, maps.Keys(secretRef.Data)...),
Immutable: &secretRef.Immutable,
Type: v1.SecretTypeOpaque,
}

return secret, nil
}

func generateCertificate(project *projectstack.Project, 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: project.Name,
},
Data: grabData(secretRef.Data, v1.TLSCertKey, v1.TLSPrivateKeyKey),
Immutable: &secretRef.Immutable,
Type: v1.SecretTypeTLS,
}

return secret, nil
}

// grabData extracts keys mapping data from original string map.
func grabData(from map[string]string, keys ...string) map[string][]byte {
to := map[string][]byte{}
for _, key := range keys {
if v, ok := from[key]; ok {
// don't override a non-zero length value with zero length
if len(v) > 0 || len(to[key]) == 0 {
to[key] = []byte(v)
}
}
}
return to
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package secret

import (
"testing"

"github.com/stretchr/testify/require"

"kusionstack.io/kusion/pkg/models"
"kusionstack.io/kusion/pkg/models/appconfiguration/workload"
"kusionstack.io/kusion/pkg/projectstack"
)

func TestGenerateSecret(t *testing.T) {
tests := map[string]struct {
secretName string
secretType string
secretData map[string]string

expectErr string
}{
"create_basic_auth_secret": {
secretName: "secret-basic-auth",
secretType: "basic",
secretData: map[string]string{
"username": "admin",
"password": "t0p-Secret",
},
},
"create_basic_auth_secret_empty_input": {
secretName: "secret-basic-auth",
secretType: "basic",
secretData: map[string]string{},
},
"create_token_secret": {
secretName: "secret-token",
secretType: "token",
secretData: map[string]string{
"token": "YmFyCg==",
},
},
"create_token_secret_empty_input": {
secretName: "secret-token",
secretType: "token",
secretData: map[string]string{},
},
"create_opaque_secret": {
secretName: "empty-secret",
secretType: "opaque",
secretData: map[string]string{},
},
"create_opaque_secret_any_info": {
secretName: "empty-secret",
secretType: "opaque",
secretData: map[string]string{
"accessKey": "dHJ1ZQ==",
},
},
"create_certificate_secret": {
secretName: "secret-tls",
secretType: "certificate",
secretData: map[string]string{
"tls.crt": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNVakNDQWJz",
"tls.key": "RXhhbXBsZSBkYXRhIGZvciB0aGUgVExTIGNydCBmaWVsZA==",
},
},
"create_invalid_secret_invalid_type": {
secretName: "invalid-tls",
secretType: "cred",
expectErr: "unrecognized secret type cred",
},
}

project := &projectstack.Project{
ProjectConfiguration: projectstack.ProjectConfiguration{
Name: "helloworld",
},
}
// run all the tests
for name, test := range tests {
t.Run(name, func(t *testing.T) {
secrets := map[string]workload.Secret{
name: {
Type: test.secretType,
Data: test.secretData,
},
}
intent := &models.Intent{}
generator, _ := NewSecretGenerator(project, secrets)
err := generator.Generate(intent)
if test.expectErr == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.EqualError(t, err, test.expectErr)
}
})
}
}

This file was deleted.

Loading

0 comments on commit d875967

Please sign in to comment.