Skip to content

Commit

Permalink
feat: move secret references replacement form generating to applying
Browse files Browse the repository at this point in the history
  • Loading branch information
SparkYuan committed Jun 20, 2024
1 parent d9a8064 commit d1a26ee
Show file tree
Hide file tree
Showing 20 changed files with 201 additions and 183 deletions.
2 changes: 2 additions & 0 deletions pkg/apis/api.kusion.io/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,8 @@ type Resource struct {
type Spec struct {
// Resources is the list of Resource this Spec contains.
Resources Resources `yaml:"resources" json:"resources"`
// SecretSore represents a external secret store location for storing secrets.
SecretStore *SecretStore `yaml:"secretStore" json:"secretStore"`
}

// State is a record of an operation's result. It is a mapping between resources in KCL and the actual
Expand Down
3 changes: 2 additions & 1 deletion pkg/engine/operation/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
apiv1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
v1 "kusionstack.io/kusion/pkg/apis/status/v1"
"kusionstack.io/kusion/pkg/engine/operation/graph"
models "kusionstack.io/kusion/pkg/engine/operation/models"
"kusionstack.io/kusion/pkg/engine/operation/models"
"kusionstack.io/kusion/pkg/engine/operation/parser"
"kusionstack.io/kusion/pkg/engine/release"
runtimeinit "kusionstack.io/kusion/pkg/engine/runtime/init"
Expand Down Expand Up @@ -94,6 +94,7 @@ func (ao *ApplyOperation) Apply(req *ApplyRequest) (rsp *ApplyResponse, s v1.Sta
Operation: models.Operation{
OperationType: models.Apply,
ReleaseStorage: o.ReleaseStorage,
SecretStore: req.Release.Spec.SecretStore,
CtxResourceIndex: map[string]*apiv1.Resource{},
PriorStateResourceIndex: priorStateResourceIndex,
StateResourceIndex: stateResourceIndex,
Expand Down
127 changes: 109 additions & 18 deletions pkg/engine/operation/graph/resource_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ import (
"context"
"errors"
"fmt"
"net/url"
"reflect"
"strings"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8sruntime "k8s.io/apimachinery/pkg/runtime"

apiv1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
v1 "kusionstack.io/kusion/pkg/apis/status/v1"
"kusionstack.io/kusion/pkg/engine"
"kusionstack.io/kusion/pkg/engine/operation/models"
"kusionstack.io/kusion/pkg/engine/runtime"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/secrets"
"kusionstack.io/kusion/pkg/util"
"kusionstack.io/kusion/pkg/util/diff"
"kusionstack.io/kusion/pkg/util/json"
Expand All @@ -32,30 +38,78 @@ const (

func (rn *ResourceNode) PreExecute(o *models.Operation) v1.Status {
value := reflect.ValueOf(rn.resource.Attributes)
var replaced reflect.Value
var s v1.Status

switch o.OperationType {
case models.ApplyPreview:
// first time apply. Do not replace implicit dependency ref
if len(o.PriorStateResourceIndex) == 0 {
_, replaced, s = ReplaceSecretRef(value)
} else {
_, replaced, s = ReplaceRef(value, o.CtxResourceIndex, OptionalImplicitReplaceFun)
// don't replace implicit dependency ref in the first time apply
if len(o.PriorStateResourceIndex) != 0 {
_, replaced, s := ReplaceRef(value, o.CtxResourceIndex, OptionalImplicitReplaceFun)
if v1.IsErr(s) {
return s
}
rn.resource.Attributes = replaced.Interface().(map[string]interface{})
}
case models.Apply:
// replace secret ref and implicit ref
_, replaced, s = ReplaceRef(value, o.CtxResourceIndex, MustImplicitReplaceFun)
// replace implicit refs
_, replaced, s := ReplaceRef(value, o.CtxResourceIndex, MustImplicitReplaceFun)
if v1.IsErr(s) {
return s
}
rn.resource.Attributes = replaced.Interface().(map[string]interface{})

// replace k8s secret refs
if rn.resource.Type == apiv1.Kubernetes {
un := &unstructured.Unstructured{}
un.SetUnstructuredContent(rn.resource.Attributes)
if un.GetKind() == "Secret" {
att, s := replaceSecretRef(o, un.Object)
if v1.IsErr(s) {
return s
}
if att != nil {
rn.resource.Attributes = att
}
}
}
default:
return nil
}
if v1.IsErr(s) {
return s

return nil
}

func replaceSecretRef(o *models.Operation, obj map[string]interface{}) (map[string]interface{}, v1.Status) {
secret := &corev1.Secret{}
converter := k8sruntime.DefaultUnstructuredConverter
err := converter.FromUnstructured(obj, secret)
if err != nil {
return nil, v1.NewErrorStatus(err)
}
if !replaced.IsZero() {
rn.resource.Attributes = replaced.Interface().(map[string]interface{})
for k, data := range secret.Data {
ref := string(data)
externalSecretRef, err := parseExternalSecretDataRef(ref)
if err != nil {
return nil, v1.NewErrorStatus(err)
}
provider, exist := secrets.GetProvider(o.SecretStore.Provider)
if !exist {
return nil, v1.NewErrorStatus(errors.New("no matched secret store found, please check workspace yaml"))
}
secretStore, err := provider.NewSecretStore(o.SecretStore)
if err != nil {
return nil, v1.NewErrorStatus(err)
}
secretData, err := secretStore.GetSecret(context.Background(), *externalSecretRef)
if err != nil {
return nil, v1.NewErrorStatus(err)
}
secret.Data[k] = secretData
}
return nil
un, err := converter.ToUnstructured(secret)
if err != nil {
return nil, v1.NewErrorStatus(err)
}
return un, nil
}

func (rn *ResourceNode) Execute(operation *models.Operation) (s v1.Status) {
Expand Down Expand Up @@ -251,6 +305,8 @@ func (rn *ResourceNode) applyResource(operation *models.Operation, prior, planed
} else {
res = prior
}
default:
return v1.NewErrorStatus(fmt.Errorf("unknown action type: %v", rn.Action))
}
if v1.IsErr(s) {
return s
Expand Down Expand Up @@ -300,10 +356,6 @@ func updateChangeOrder(ops *models.Operation, rn *ResourceNode, plan, live inter
order.ChangeSteps[rn.ID] = models.NewChangeStep(rn.ID, rn.Action, plan, live)
}

func ReplaceSecretRef(v reflect.Value) ([]string, reflect.Value, v1.Status) {
return ReplaceRef(v, nil, nil)
}

var MustImplicitReplaceFun = func(resourceIndex map[string]*apiv1.Resource, refPath string) (reflect.Value, v1.Status) {
return implicitReplaceFun(true, resourceIndex, refPath)
}
Expand Down Expand Up @@ -435,3 +487,42 @@ func ReplaceRef(
}
return result, v, nil
}

// 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
}
66 changes: 66 additions & 0 deletions pkg/engine/operation/graph/resource_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package graph

import (
"context"
"reflect"
"sync"
"testing"

Expand Down Expand Up @@ -248,3 +249,68 @@ func Test_removeNestedField(t *testing.T) {
assert.Len(t, ports[0], 2)
})
}

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)
}
})
}
}
3 changes: 3 additions & 0 deletions pkg/engine/operation/models/operation_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type Operation struct {
// ReleaseStorage represents the storage where state will be saved during this operation
ReleaseStorage release.Storage

// SecretStore represents the storage where secrets were saved
SecretStore *apiv1.SecretStore

// CtxResourceIndex represents resources updated by this operation
CtxResourceIndex map[string]*apiv1.Resource

Expand Down
5 changes: 5 additions & 0 deletions pkg/modules/generators/app_configurations_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ func (g *appConfigurationGenerator) Generate(spec *v1.Spec) error {
return err
}

// append secretStore in the Spec
if g.ws.SecretStore != nil {
spec.SecretStore = g.ws.SecretStore
}

return nil
}

Expand Down
Loading

0 comments on commit d1a26ee

Please sign in to comment.