diff --git a/Makefile b/Makefile index d0bbc8587..212a9286d 100644 --- a/Makefile +++ b/Makefile @@ -216,9 +216,10 @@ install-proxy: kubectl apply -f hack/functionproxy .PHONY: render-diff +DEBUG="" render-diff: export IMG_TAG=$(shell git rev-parse --abbrev-ref HEAD | sed 's/\//_/g') render-diff: ## Render diff between the cluster in KUBECONF and the local branch # We check if the image is pullable, if so we pull it, otherwise we build the image # this will speed up the compare in CI/CD environments. if ! docker pull $(IMG); then $(MAKE) docker-build-branchtag; fi - hack/diff/compare.sh + hack/diff/compare.sh $(DEBUG) diff --git a/README.md b/README.md index e8614b9a4..ef64ab065 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,15 @@ This will: * Run `crank` again with the state already downloaded, to avoid any unintended diffs * Use `dyff` to generate the diffs between both results +### Generate diff against live version with debugger + +If you want to print the diff while using the debugger you can simply do this: + +* Start the local debugging session as described +* Run `make render-diff -e DEBUG=Development` + +NOTE: `crank render` has a 60s timeout, so you might run into it, if your debugging takes longer + # Run API Server locally To run the API server on your local machine you need to register the IDE running instance with kind cluster. This can be achieved with the following guide. diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index ad664e0be..9dc45e60c 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -25,3 +25,4 @@ * xref:explanations/disable-billing.adoc[] * xref:explanations/webhook-protection.adoc[] * xref:explanations/ordered-deletion.adoc[] +* xref:explanations/providerconfig-management.adoc[] diff --git a/docs/modules/ROOT/pages/explanations/providerconfig-management.adoc b/docs/modules/ROOT/pages/explanations/providerconfig-management.adoc new file mode 100644 index 000000000..2f852cc18 --- /dev/null +++ b/docs/modules/ROOT/pages/explanations/providerconfig-management.adoc @@ -0,0 +1,11 @@ += ProviderConfig Management + +AppCat is able to inject a given `ProviderConfig` reference into all managed resources. + +That can be controlled by setting the `appcat.vshn.io/provider-config` label on any given claim or composite. The value of this label will be injected as the `ProviderConfig` name. + +There are managed resources that should have the `ProviderConfig` overwritten. In such cases it's possible to deploy them with the `appcat.vshn.io/ignore-provider-config` and then no overwrite will happen. + +The `ProviderConfig` objects have to be provisioned beforehand, via `component-appcat` for example. + +If no labels are given, AppCat will use the hardcoded default `ProviderConfigs` for each provider. These also have to be provided by external means like `component-appcat`. diff --git a/hack/diff/compare.sh b/hack/diff/compare.sh index f326ed181..3a248b47e 100755 --- a/hack/diff/compare.sh +++ b/hack/diff/compare.sh @@ -2,6 +2,8 @@ set -e +debug=$1 + [ -z "${KUBECONFIG}" ] && echo "Please export KUBECONFIG" && exit 1 # get the state and all objects from each composite @@ -14,10 +16,11 @@ function get_state() { mkdir -p "$dir_name" - while read -r res_type res_name + while read -r res_type res_name api_version do - kubectl get "$res_type" "$res_name" -oyaml > "$dir_name/$res_type-$res_name.yaml" - done <<< "$(kubectl get "$type" "$name" -oyaml | yq -r '.spec.resourceRefs | .[] | .kind + " " + .name')" + group=$(echo "$api_version" | cut -d "/" -f1) + kubectl get "$res_type"."$group" "$res_name" -oyaml > "$dir_name/$res_type-$res_name.yaml" + done <<< "$(kubectl get "$type" "$name" -oyaml | yq -r '.spec.resourceRefs | .[] | .kind + " " + .name + " " + .apiVersion')" } @@ -67,6 +70,7 @@ function get_pnt_func_version() { function template_func_file() { export PNT_VERSION=$1 export APPCAT_VERSION=$2 + export DEBUG=$3 cat "$(dirname "$0")/function.yaml.tmpl" | envsubst > "$(dirname "$0")/function.yaml" } @@ -132,12 +136,12 @@ function clean() { clean trap clean EXIT -template_func_file "$(get_pnt_func_version)" "$(get_running_func_version)" +template_func_file "$(get_pnt_func_version)" "$(get_running_func_version)" "" echo "Render live manifests" first_diff -template_func_file "$(get_pnt_func_version)" "$(git rev-parse --abbrev-ref HEAD | sed 's/\//_/g')" +template_func_file "$(get_pnt_func_version)" "$(git rev-parse --abbrev-ref HEAD | sed 's/\//_/g')" "$debug" echo "Render against branch" second_diff diff --git a/hack/diff/function.yaml.tmpl b/hack/diff/function.yaml.tmpl index c49e52a84..cf25c3bc3 100644 --- a/hack/diff/function.yaml.tmpl +++ b/hack/diff/function.yaml.tmpl @@ -4,6 +4,7 @@ metadata: name: function-appcat annotations: render.crossplane.io/runtime-docker-cleanup: Stop + render.crossplane.io/runtime: $DEBUG spec: package: ghcr.io/vshn/appcat:$APPCAT_VERSION --- diff --git a/pkg/comp-functions/functions/common/backup/backup.go b/pkg/comp-functions/functions/common/backup/backup.go index 5ad9e957f..487502806 100644 --- a/pkg/comp-functions/functions/common/backup/backup.go +++ b/pkg/comp-functions/functions/common/backup/backup.go @@ -56,6 +56,9 @@ func createObjectBucket(ctx context.Context, comp common.InfoGetter, svc *runtim ob := &appcatv1.XObjectBucket{ ObjectMeta: metav1.ObjectMeta{ Name: comp.GetName() + "-backup", + Labels: map[string]string{ + runtime.ProviderConfigIgnoreLabel: "true", + }, }, Spec: appcatv1.XObjectBucketSpec{ Parameters: appcatv1.ObjectBucketParameters{ diff --git a/pkg/comp-functions/functions/vshnpostgres/postgresql_deploy.go b/pkg/comp-functions/functions/vshnpostgres/postgresql_deploy.go index 476cd2e1f..36df5ca6f 100644 --- a/pkg/comp-functions/functions/vshnpostgres/postgresql_deploy.go +++ b/pkg/comp-functions/functions/vshnpostgres/postgresql_deploy.go @@ -454,6 +454,9 @@ func createObjectBucket(comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime xObjectBucket := &appcatv1.XObjectBucket{ ObjectMeta: metav1.ObjectMeta{ Name: comp.GetName(), + Labels: map[string]string{ + runtime.ProviderConfigIgnoreLabel: "true", + }, }, Spec: appcatv1.XObjectBucketSpec{ Parameters: appcatv1.ObjectBucketParameters{ diff --git a/pkg/comp-functions/functions/vshnpostgres/user_management.go b/pkg/comp-functions/functions/vshnpostgres/user_management.go index 32736717e..532bb85a4 100644 --- a/pkg/comp-functions/functions/vshnpostgres/user_management.go +++ b/pkg/comp-functions/functions/vshnpostgres/user_management.go @@ -60,6 +60,9 @@ func addUser(comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime, username Annotations: map[string]string{ "crossplane.io/external-name": username, }, + Labels: map[string]string{ + runtime.ProviderConfigIgnoreLabel: "true", + }, }, Spec: pgv1alpha1.RoleSpec{ ForProvider: pgv1alpha1.RoleParameters{ @@ -209,6 +212,9 @@ func addDatabase(comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime, name Annotations: map[string]string{ "crossplane.io/external-name": name, }, + Labels: map[string]string{ + runtime.ProviderConfigIgnoreLabel: "true", + }, }, Spec: pgv1alpha1.DatabaseSpec{ ForProvider: pgv1alpha1.DatabaseParameters{}, @@ -241,6 +247,9 @@ func addGrants(comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime, usernam grant := &pgv1alpha1.Grant{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-%s-%s-grants", comp.GetName(), username, dbname), + Labels: map[string]string{ + runtime.ProviderConfigIgnoreLabel: "true", + }, }, Spec: pgv1alpha1.GrantSpec{ ForProvider: pgv1alpha1.GrantParameters{ diff --git a/pkg/comp-functions/runtime/function_mgr.go b/pkg/comp-functions/runtime/function_mgr.go index 87f28a58c..fcb551bf6 100644 --- a/pkg/comp-functions/runtime/function_mgr.go +++ b/pkg/comp-functions/runtime/function_mgr.go @@ -46,12 +46,14 @@ var ( ) const ( - OwnerKindAnnotation = "appcat.vshn.io/ownerkind" - OwnerVersionAnnotation = "appcat.vshn.io/ownerapiversion" - OwnerGroupAnnotation = "appcat.vshn.io/ownergroup" - ProtectedByAnnotation = "appcat.vshn.io/protectedby" - ProtectsAnnotation = "appcat.vshn.io/protects" - EventForwardAnnotation = "appcat.vshn.io/forward-events-to" + OwnerKindAnnotation = "appcat.vshn.io/ownerkind" + OwnerVersionAnnotation = "appcat.vshn.io/ownerapiversion" + OwnerGroupAnnotation = "appcat.vshn.io/ownergroup" + ProtectedByAnnotation = "appcat.vshn.io/protectedby" + ProtectsAnnotation = "appcat.vshn.io/protects" + EventForwardAnnotation = "appcat.vshn.io/forward-events-to" + providerConfigLabel = "appcat.vshn.io/provider-config" + ProviderConfigIgnoreLabel = "appcat.vshn.io/ignore-provider-config" ) // Step describes a single change within a service. @@ -359,6 +361,11 @@ func (s *ServiceRuntime) GetResponse() (*fnv1beta1.RunFunctionResponse, error) { return nil, err } + err = s.setProviderConfigs() + if err != nil { + return nil, err + } + err = response.SetDesiredComposedResources(resp, s.desiredResources) if err != nil { return nil, err @@ -1104,6 +1111,14 @@ func (s *ServiceRuntime) UsageOfBy(of, by string) error { ofAPIVersion, ofKind := ofUnstructured.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind() byAPIVersion, byKind := byUnstructured.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind() + removed, err := s.unwrapUsage(name) + if err != nil { + return fmt.Errorf("cannot remove kube object wrapper from uage: %w", err) + } + if !removed { + return nil + } + usage := &xpapi.Usage{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -1127,7 +1142,60 @@ func (s *ServiceRuntime) UsageOfBy(of, by string) error { }, } - return s.SetDesiredKubeObject(usage, name) + composedRes, err := composed.From(usage) + if err != nil { + return fmt.Errorf("cannot convert usage object to managed resource: %w", err) + } + + s.desiredResources[resource.Name(name)] = &resource.DesiredComposed{Resource: composedRes} + + return nil +} + +// unwrapUsage will check if the usage is wrapped into an object. +// It will set the wrapping obect to only observe it. Then on the next reconcile +// after it ensures that the right management policy is set, it will not add it +// to the desired state again. And finally on the third reconcile it will +// return true to indicate that it got removed properly. Allowing Crossplane +// to adopt the Usage objects without wrapping in a kube object. +func (s *ServiceRuntime) unwrapUsage(name string) (bool, error) { + usage := &xkube.Object{} + err := s.GetObservedComposedResource(usage, name) + if err != nil { + if err == ErrNotFound { + return true, nil + } + return false, err + } + + resources, err := request.GetObservedComposedResources(s.req) + if err != nil { + return false, err + } + res := resources[resource.Name(name)] + if res.Resource.GetKind() != "Object" { + return true, nil + } + + // we need to clean the objectmeta, or Crossplane will struggle with adding + // it to the `resourceRefs` array. + usage.ObjectMeta = metav1.ObjectMeta{ + Name: usage.GetName(), + Annotations: usage.Annotations, + Labels: usage.Labels, + OwnerReferences: usage.OwnerReferences, + } + + if len(usage.Spec.ManagementPolicies) == 0 || usage.Spec.ManagementPolicies[0] != xpv1.ManagementActionObserve { + usage.Spec.ManagementPolicies = xpv1.ManagementPolicies{ + xpv1.ManagementActionObserve, + } + err := s.SetDesiredComposedResourceWithName(usage, name) + if err != nil { + return false, err + } + } + return false, nil } func (s *ServiceRuntime) addUsages() error { @@ -1222,3 +1290,35 @@ func (s *ServiceRuntime) getCleanGVK() schema.GroupVersionKind { Kind: kind, } } + +// setProviderConfigs loops over all desired objects and adds the providerConfigs +// according to the annotations on the claim/composite. +func (s *ServiceRuntime) setProviderConfigs() error { + if val, exists := s.observedComposite.GetLabels()[providerConfigLabel]; !exists || val == "" || val == "local" { + return nil + } + + configName := s.observedComposite.GetLabels()[providerConfigLabel] + + for i := range s.desiredResources { + if _, exists := s.desiredResources[i].Resource.GetLabels()[ProviderConfigIgnoreLabel]; exists { + continue + } + // we set the providerConfig Ref + err := s.desiredResources[i].Resource.SetString("spec.providerConfigRef.name", configName) + if err != nil { + return fmt.Errorf("cannot set providerConfig for %s: %w", s.desiredResources[i].Resource.GetName(), err) + } + + // We also propagate the label, so if the resource is a composite, then + // it will automagically also set the right providerConfigs. + labels := s.desiredResources[i].Resource.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels[providerConfigLabel] = configName + s.desiredResources[i].Resource.SetLabels(labels) + } + + return nil +} diff --git a/pkg/comp-functions/runtime/function_mgr_test.go b/pkg/comp-functions/runtime/function_mgr_test.go new file mode 100644 index 000000000..c325eda25 --- /dev/null +++ b/pkg/comp-functions/runtime/function_mgr_test.go @@ -0,0 +1,135 @@ +package runtime + +import ( + "testing" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpapi "github.com/crossplane/crossplane/apis/apiextensions/v1alpha1" + xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" + "github.com/crossplane/function-sdk-go/resource" + "github.com/crossplane/function-sdk-go/resource/composed" + "github.com/stretchr/testify/assert" + xkube "github.com/vshn/appcat/v4/apis/kubernetes/v1alpha2" + "google.golang.org/protobuf/types/known/structpb" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestServiceRuntime_unwrapUsage(t *testing.T) { + tests := []struct { + name string + resName string + objects []client.Object + want bool + wantErr bool + desired bool + }{ + { + name: "GivenWrappedUsage_ThenUnwrap", + resName: "myusage", + objects: []client.Object{ + &xpapi.Usage{}, + &xkube.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myusage", + }, + Status: xkube.ObjectStatus{ + AtProvider: xkube.ObjectObservation{ + Manifest: runtime.RawExtension{ + Object: &xpapi.Usage{}, + }, + }, + }, + }, + }, + want: false, + desired: true, + }, + { + name: "GivenWrappedUsageWithManagementPolicy_ThenUnwrap", + resName: "myusage", + objects: []client.Object{ + &xpapi.Usage{}, + &xkube.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myusage", + }, + Spec: xkube.ObjectSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ManagementPolicies: xpv1.ManagementPolicies{ + xpv1.ManagementActionObserve, + }, + }, + }, + Status: xkube.ObjectStatus{ + AtProvider: xkube.ObjectObservation{ + Manifest: runtime.RawExtension{ + Object: &xpapi.Usage{}, + }, + }, + }, + }, + }, + want: false, + }, + { + name: "GivenUsage_ThenNoUnwrap", + resName: "myusage", + objects: []client.Object{ + &xpapi.Usage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myusage", + }, + Spec: xpapi.UsageSpec{ + Of: xpapi.Resource{ + Kind: "mykind", + }, + }, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + req := &xfnproto.RunFunctionRequest{ + Observed: &xfnproto.State{ + Resources: map[string]*xfnproto.Resource{}, + }, + } + + for _, obj := range tt.objects { + res, err := composed.From(obj) + assert.NoError(t, err) + + protoRes, err := structpb.NewStruct(res.UnstructuredContent()) + assert.NoError(t, err) + + req.Observed.Resources[obj.GetName()] = &xfnproto.Resource{Resource: protoRes} + } + + s := &ServiceRuntime{ + req: req, + desiredResources: map[resource.Name]*resource.DesiredComposed{}, + } + + res, err := s.unwrapUsage(tt.resName) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, res) + + if tt.desired { + assert.NotEmpty(t, s.GetAllDesired()) + } else { + assert.Empty(t, s.GetAllDesired()) + } + + }) + } +}