Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move secret references replacement form generating to applying #1173

Merged
merged 1 commit into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading