diff --git a/example/composition.yaml b/example/composition.yaml index 2c73250..f2b6d4b 100644 --- a/example/composition.yaml +++ b/example/composition.yaml @@ -19,12 +19,9 @@ spec: base: apiVersion: s3.aws.upbound.io/v1beta1 kind: Bucket - spec: - forProvider: - region: us-east-2 patches: - type: FromCompositeFieldPath - fromFieldPath: "location" + fromFieldPath: "spec.location" toFieldPath: "spec.forProvider.region" transforms: - type: map diff --git a/fn.go b/fn.go index 81727e2..0be03e2 100644 --- a/fn.go +++ b/fn.go @@ -7,6 +7,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" "github.com/crossplane/crossplane-runtime/pkg/logging" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" @@ -114,10 +115,14 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1beta1.RunFunctionRe } if input.Environment != nil { - // Run all patches that are from the (observed) XR to the environment or from the environment to the (desired) XR. - if err := RenderEnvironmentPatches(env, oxr.Resource, dxr.Resource, input.Environment.Patches); err != nil { - response.Fatal(rsp, errors.Wrapf(err, "cannot render ToEnvironment patches from the composite resource")) - return rsp, nil + // Run all patches that are from the (observed) XR to the environment or + // from the environment to the (desired) XR. + for i := range input.Environment.Patches { + p := &input.Environment.Patches[i] + if err := ApplyEnvironmentPatch(p, env, oxr.Resource, dxr.Resource); err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot apply the %q environment patch at index %d", p.GetType(), i)) + return rsp, nil + } } } @@ -155,8 +160,8 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1beta1.RunFunctionRe } } - ocd, ok := observed[resource.Name(t.Name)] - if ok { + ocd, exists := observed[resource.Name(t.Name)] + if exists { existing++ log.Debug("Resource template corresponds to existing composed resource", "metadata-name", ocd.Resource.GetName()) @@ -192,17 +197,56 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1beta1.RunFunctionRe "name", ocd.Resource.GetName()) } - errs, store := RenderComposedPatches(ocd.Resource, dcd.Resource, oxr.Resource, dxr.Resource, env, t.Patches) - for _, err := range errs { - response.Warning(rsp, errors.Wrapf(err, "cannot render patches for composed resource %q", t.Name)) - log.Info("Cannot render patches for composed resource", "warning", err) - warnings++ + // Run all patches that are to a desired composed resource, or from an + // observed composed resource. + skip := false + for i := range t.Patches { + p := &t.Patches[i] + if err := ApplyComposedPatch(p, ocd.Resource, dcd.Resource, oxr.Resource, dxr.Resource, env); err != nil { + if fieldpath.IsNotFound(err) { + // This is a patch from a required field path that does not + // exist. The point of FromFieldPathPolicyRequired is to + // block creation of the new 'to' resource until the 'from' + // field path exists. + // + // The only kind of resource we could be patching to that + // might not exist at this point is a composed resource. So + // if we're patching to a composed resource that doesn't + // exist we want to avoid creating it. Otherwise, we just + // treat the patch from a required field path the same way + // we'd treat a patch from an optional field path and skip + // it. + if p.GetPolicy().GetFromFieldPathPolicy() == v1beta1.FromFieldPathPolicyRequired { + if ToComposedResource(p) && !exists { + response.Warning(rsp, errors.Wrapf(err, "not adding new composed resource %q to desired state because %q patch at index %d has 'policy.fromFieldPath: Required'", t.Name, p.GetType(), i)) + + // There's no point processing further patches. + // They'll either be from an observed composed + // resource that doesn't exist yet, or to a desired + // composed resource that we'll discard. + skip = true + break + } + response.Warning(rsp, errors.Wrapf(err, "cannot render composed resource %q %q patch at index %d: ignoring 'policy.fromFieldPath: Required' because 'to' resource already exists", t.Name, p.GetType(), i)) + } + + // If any optional field path isn't found we just skip this + // patch and move on. The path may be populated by a + // subsequent patch. + continue + } + response.Fatal(rsp, errors.Wrapf(err, "cannot render composed resource %q %q patch at index %d", t.Name, p.GetType(), i)) + return rsp, nil + } } - if store { - // Add or replace our desired resource. - desired[resource.Name(t.Name)] = dcd + // Skip adding this resource to the desired state because it doesn't + // exist yet, and a required FromFieldPath was not (yet) found. + if skip { + continue } + + desired[resource.Name(t.Name)] = dcd } if err := response.SetDesiredCompositeResource(rsp, dxr); err != nil { diff --git a/fn_test.go b/fn_test.go index 9ed508e..85c3af7 100644 --- a/fn_test.go +++ b/fn_test.go @@ -384,17 +384,15 @@ func TestRunFunction(t *testing.T) { }, }, }, - "FailedPatchNotSaved": { - reason: "If we fail to patch a desired resource produced by a previous Function in the pipeline we should return a warning result, and leave the original desired resource untouched.", + "OptionalFieldPathNotFound": { + reason: "If we fail to patch a desired resource because an optional field path was not found we should skip the patch.", args: args{ req: &fnv1beta1.RunFunctionRequest{ Input: resource.MustStructObject(&v1beta1.Resources{ Resources: []v1beta1.ComposedTemplate{ { - // This template base no base, so we try to - // patch the resource named "cool-resource" in - // the desired resources array. Name: "cool-resource", + Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD","spec":{}}`)}, Patches: []v1beta1.ComposedPatch{ { // This patch should work. @@ -405,19 +403,11 @@ func TestRunFunction(t *testing.T) { }, }, { - // This patch should return an error, - // because the required path does not - // exist. + // This patch should be skipped, because + // the path is not found Type: v1beta1.PatchTypeFromCompositeFieldPath, Patch: v1beta1.Patch{ FromFieldPath: ptr.To[string]("spec.doesNotExist"), - ToFieldPath: ptr.To[string]("spec.explode"), - Policy: &v1beta1.PatchPolicy{ - FromFieldPath: func() *v1beta1.FromFieldPathPolicy { - r := v1beta1.FromFieldPathPolicyRequired - return &r - }(), - }, }, }, }, @@ -429,16 +419,94 @@ func TestRunFunction(t *testing.T) { Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10"}}`), }, }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, Desired: &fnv1beta1.State{ Composite: &fnv1beta1.Resource{ - Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10"}}`), + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR"}`), }, Resources: map[string]*fnv1beta1.Resource{ "cool-resource": { - Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":42}}`), + // Watchers becomes "10" because our first patch + // worked. We only skipped the second patch. + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":"10"}}`), }, }, }, + Context: &structpb.Struct{Fields: map[string]*structpb.Value{fncontext.KeyEnvironment: structpb.NewStructValue(nil)}}, + }, + }, + }, + "RequiredFieldPathNotFound": { + reason: "If we fail to patch a desired resource because a required field path was not found, and the resource doesn't exist, we should not add it to desired state (i.e. create it).", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Resources{ + Resources: []v1beta1.ComposedTemplate{ + { + Name: "new-resource", + Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD","spec":{}}`)}, + Patches: []v1beta1.ComposedPatch{ + { + // This patch will fail because the path + // is not found. + Type: v1beta1.PatchTypeFromCompositeFieldPath, + Patch: v1beta1.Patch{ + FromFieldPath: ptr.To[string]("spec.doesNotExist"), + Policy: &v1beta1.PatchPolicy{ + FromFieldPath: ptr.To[v1beta1.FromFieldPathPolicy](v1beta1.FromFieldPathPolicyRequired), + }, + }, + }, + }, + }, + { + Name: "existing-resource", + Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD","spec":{}}`)}, + Patches: []v1beta1.ComposedPatch{ + { + // This patch should work. + Type: v1beta1.PatchTypeFromCompositeFieldPath, + Patch: v1beta1.Patch{ + FromFieldPath: ptr.To[string]("spec.widgets"), + ToFieldPath: ptr.To[string]("spec.watchers"), + }, + }, + { + // This patch will fail because the path + // is not found. + Type: v1beta1.PatchTypeFromCompositeFieldPath, + Patch: v1beta1.Patch{ + FromFieldPath: ptr.To[string]("spec.doesNotExist"), + Policy: &v1beta1.PatchPolicy{ + FromFieldPath: ptr.To[v1beta1.FromFieldPathPolicy](v1beta1.FromFieldPathPolicyRequired), + }, + }, + }, + }, + }, + }, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10"}}`), + }, + Resources: map[string]*fnv1beta1.Resource{ + // "existing-resource" exists. + "existing-resource": {}, + + // Note "new-resource" doesn't appear in the + // observed resources. It doesn't yet exist. + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10"}}`), + }, + }, }, }, want: want{ @@ -449,20 +517,85 @@ func TestRunFunction(t *testing.T) { Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10"}}`), }, Resources: map[string]*fnv1beta1.Resource{ - "cool-resource": { - // spec.watchers would be "10" if we didn't - // discard the patch that worked. - Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":42}}`), + // Note that the first patch did work. We only + // skipped the patch from the required field path. + "existing-resource": { + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":"10"}}`), }, + + // Note "new-resource" doesn't appear here. }, }, + Context: &structpb.Struct{Fields: map[string]*structpb.Value{fncontext.KeyEnvironment: structpb.NewStructValue(nil)}}, Results: []*fnv1beta1.Result{ { Severity: fnv1beta1.Severity_SEVERITY_WARNING, - Message: fmt.Sprintf("cannot render patches for composed resource %q: cannot apply the %q patch at index 1: spec.doesNotExist: no such field", "cool-resource", "FromCompositeFieldPath"), + Message: `not adding new composed resource "new-resource" to desired state because "FromCompositeFieldPath" patch at index 0 has 'policy.fromFieldPath: Required': spec.doesNotExist: no such field`, + }, + { + Severity: fnv1beta1.Severity_SEVERITY_WARNING, + Message: `cannot render composed resource "existing-resource" "FromCompositeFieldPath" patch at index 1: ignoring 'policy.fromFieldPath: Required' because 'to' resource already exists: spec.doesNotExist: no such field`, + }, + }, + }, + }, + }, + "PatchErrorIsFatal": { + reason: "If we fail to patch a desired resource we should return a fatal result.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Resources{ + Resources: []v1beta1.ComposedTemplate{ + { + Name: "cool-resource", + Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD","spec":{}}`)}, + Patches: []v1beta1.ComposedPatch{ + { + // This patch should work. + Type: v1beta1.PatchTypeFromCompositeFieldPath, + Patch: v1beta1.Patch{ + FromFieldPath: ptr.To[string]("spec.widgets"), + ToFieldPath: ptr.To[string]("spec.watchers"), + }, + }, + { + // This patch should return an error, + // because the path is not an array. + Type: v1beta1.PatchTypeFromCompositeFieldPath, + Patch: v1beta1.Patch{ + FromFieldPath: ptr.To[string]("spec.widgets[0]"), + }, + }, + }, + }, + }, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10"}}`), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10"}}`), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10"}}`), + }, + }, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: fmt.Sprintf("cannot render composed resource %q %q patch at index 1: spec.widgets: not an array", "cool-resource", "FromCompositeFieldPath"), }, }, - Context: &structpb.Struct{Fields: map[string]*structpb.Value{fncontext.KeyEnvironment: structpb.NewStructValue(nil)}}, }, }, }, diff --git a/go.mod b/go.mod index 133e3d7..00a910c 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,6 @@ require ( github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect diff --git a/go.sum b/go.sum index 916ef00..7a106d3 100644 --- a/go.sum +++ b/go.sum @@ -67,11 +67,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/crossplane/crossplane-runtime v1.14.2 h1:pV5JMzyzi/kcbeVBVPCat5MHH8zS94MBUapAyGx/Ry0= -github.com/crossplane/crossplane-runtime v1.14.2/go.mod h1:aOP+5W2wKpvthVs3pFNbVOe1jwrKYbJho0ThGNCVz9o= github.com/crossplane/crossplane-runtime v1.14.3 h1:YNGALph/UJTtQO+cJ9KGQ5NfALI5453PeE93Aqy9SWg= github.com/crossplane/crossplane-runtime v1.14.3/go.mod h1:aOP+5W2wKpvthVs3pFNbVOe1jwrKYbJho0ThGNCVz9o= github.com/crossplane/crossplane-runtime v1.14.4 h1:64zSZ75g1QXIMxR2zSQvz4+TTSq5qCUU5lmpiVovVKE= @@ -97,11 +94,8 @@ github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -109,7 +103,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-json-experiment/json v0.0.0-20231013223334-54c864be5b8d h1:zqfo2jECgX5eYQseB/X+uV4Y5ocGOG/vG/LTztUCyPA= github.com/go-json-experiment/json v0.0.0-20231013223334-54c864be5b8d/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -258,8 +251,7 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -284,10 +276,10 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -309,8 +301,7 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -404,8 +395,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -440,8 +430,6 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -466,8 +454,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -506,15 +494,10 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -525,8 +508,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -584,8 +565,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -688,8 +668,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -715,48 +693,20 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= -k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= -k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= -k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= -k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU= -k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM= -k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= -k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= k8s.io/apiextensions-apiserver v0.29.1 h1:S9xOtyk9M3Sk1tIpQMu9wXHm5O2MX6Y1kIpPMimZBZw= k8s.io/apiextensions-apiserver v0.29.1/go.mod h1:zZECpujY5yTW58co8V2EQR4BD6A9pktVgHhvc0uLfeU= -k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= -k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= -k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= -k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= -k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= -k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= -k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= -k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= -k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo= -k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU= -k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= -k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= k8s.io/component-base v0.29.1 h1:MUimqJPCRnnHsskTTjKD+IC1EHBbRCVyi37IoFBrkYw= k8s.io/component-base v0.29.1/go.mod h1:fP9GFjxYrLERq1GcWWZAE3bqbNcDKDytn2srWuHTtKc= -k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= -k8s.io/utils v0.0.0-20231121161247-cf03d44ff3cf h1:iTzha1p7Fi83476ypNSz8nV9iR9932jIIs26F7gNLsU= -k8s.io/utils v0.0.0-20231121161247-cf03d44ff3cf/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= -k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= @@ -764,13 +714,10 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= -sigs.k8s.io/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI= -sigs.k8s.io/controller-tools v0.13.0/go.mod h1:5vw3En2NazbejQGCeWKRrE7q4P+CW8/klfVqP8QZkgA= +sigs.k8s.io/controller-tools v0.14.0 h1:rnNoCC5wSXlrNoBKKzL70LNJKIQKEzT6lloG6/LF73A= sigs.k8s.io/controller-tools v0.14.0/go.mod h1:TV7uOtNNnnR72SpzhStvPkoS/U5ir0nMudrkrC4M9Sc= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/input/v1beta1/resources_patches.go b/input/v1beta1/resources_patches.go index 9478a79..65d338d 100644 --- a/input/v1beta1/resources_patches.go +++ b/input/v1beta1/resources_patches.go @@ -36,8 +36,8 @@ const ( type PatchPolicy struct { // FromFieldPath specifies how to patch from a field path. The default is // 'Optional', which means the patch will be a no-op if the specified - // fromFieldPath does not exist. Use 'Required' if the patch should fail if - // the specified path does not exist. + // fromFieldPath does not exist. Use 'Required' to prevent the creation of a + // new composed resource until the required path exists. // +kubebuilder:validation:Enum=Optional;Required // +optional FromFieldPath *FromFieldPathPolicy `json:"fromFieldPath,omitempty"` diff --git a/package/input/pt.fn.crossplane.io_resources.yaml b/package/input/pt.fn.crossplane.io_resources.yaml index 1b68e97..052311b 100644 --- a/package/input/pt.fn.crossplane.io_resources.yaml +++ b/package/input/pt.fn.crossplane.io_resources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.13.0 + controller-gen.kubebuilder.io/version: v0.14.0 name: resources.pt.fn.crossplane.io spec: group: pt.fn.crossplane.io @@ -22,61 +22,72 @@ spec: description: Resources specifies Patch & Transform resource templates. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string environment: - description: "Environment represents the Composition environment. \n THIS - IS AN ALPHA FIELD. Do not use it in production. It may be changed or - removed without notice." + description: |- + Environment represents the Composition environment. + + + THIS IS AN ALPHA FIELD. + Do not use it in production. It may be changed or removed without notice. properties: patches: - description: Patches is a list of environment patches that are executed - before a composition's resources are composed. These patches are - between the XR and the Environment. Either from the Environment - to the XR, or vice versa. + description: |- + Patches is a list of environment patches that are executed before a + composition's resources are composed. These patches are between the XR + and the Environment. Either from the Environment to the XR, or vice + versa. items: - description: EnvironmentPatch objects are applied between the composite - resource and the environment. Their behaviour depends on the Type - selected. The default Type, FromCompositeFieldPath, copies a value - from the composite resource to the environment, applying any defined - transformers. + description: |- + EnvironmentPatch objects are applied between the composite resource and + the environment. Their behaviour depends on the Type selected. The default + Type, FromCompositeFieldPath, copies a value from the composite resource + to the environment, applying any defined transformers. properties: combine: - description: Combine is the patch configuration for a CombineFromComposite, + description: |- + Combine is the patch configuration for a CombineFromComposite, CombineToComposite patch. properties: strategy: - description: Strategy defines the strategy to use to combine - the input variable values. Currently only string is supported. + description: |- + Strategy defines the strategy to use to combine the input variable values. + Currently only string is supported. enum: - string type: string string: - description: String declares that input variables should - be combined into a single string, using the relevant settings - for formatting purposes. + description: |- + String declares that input variables should be combined into a single + string, using the relevant settings for formatting purposes. properties: fmt: - description: Format the input using a Go format string. - See https://golang.org/pkg/fmt/ for details. + description: |- + Format the input using a Go format string. See + https://golang.org/pkg/fmt/ for details. type: string required: - fmt type: object variables: - description: Variables are the list of variables whose values - will be retrieved and combined. + description: |- + Variables are the list of variables whose values will be retrieved and + combined. items: - description: A CombineVariable defines the source of a - value that is combined with others to form and patch - an output value. Currently, this only supports retrieving - values from a field path. + description: |- + A CombineVariable defines the source of a value that is combined with + others to form and patch an output value. Currently, this only supports + retrieving values from a field path. properties: fromFieldPath: - description: FromFieldPath is the path of the field - on the source whose value is to be used as input. + description: |- + FromFieldPath is the path of the field on the source whose value is + to be used as input. type: string required: - fromFieldPath @@ -88,49 +99,56 @@ spec: - variables type: object fromFieldPath: - description: FromFieldPath is the path of the field on the resource - whose value is to be used as input. Required when type is - FromCompositeFieldPath or ToCompositeFieldPath. + description: |- + FromFieldPath is the path of the field on the resource whose value is + to be used as input. Required when type is FromCompositeFieldPath or + ToCompositeFieldPath. type: string policy: description: Policy configures the specifics of patching behaviour. properties: fromFieldPath: - description: FromFieldPath specifies how to patch from a - field path. The default is 'Optional', which means the - patch will be a no-op if the specified fromFieldPath does - not exist. Use 'Required' if the patch should fail if - the specified path does not exist. + description: |- + FromFieldPath specifies how to patch from a field path. The default is + 'Optional', which means the patch will be a no-op if the specified + fromFieldPath does not exist. Use 'Required' to prevent the creation of a + new composed resource until the required path exists. enum: - Optional - Required type: string type: object toFieldPath: - description: ToFieldPath is the path of the field on the resource - whose value will be changed with the result of transforms. - Leave empty if you'd like to propagate to the same path as - fromFieldPath. + description: |- + ToFieldPath is the path of the field on the resource whose value will + be changed with the result of transforms. Leave empty if you'd like to + propagate to the same path as fromFieldPath. type: string transforms: - description: Transforms are the list of functions that are used - as a FIFO pipe for the input to be transformed. + description: |- + Transforms are the list of functions that are used as a FIFO pipe for the + input to be transformed. items: - description: Transform is a unit of process whose input is - transformed into an output with the supplied configuration. + description: |- + Transform is a unit of process whose input is transformed into an output with + the supplied configuration. properties: convert: description: Convert is used to cast the input into the given output type. properties: format: - description: "The expected input format. \n * `quantity` - - parses the input as a K8s [`resource.Quantity`](https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity). + description: |- + The expected input format. + + + * `quantity` - parses the input as a K8s [`resource.Quantity`](https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity). Only used during `string -> float64` conversions. - * `json` - parses the input as a JSON string. Only - used during `string -> object` or `string -> list` - conversions. \n If this property is null, the default - conversion is applied." + * `json` - parses the input as a JSON string. + Only used during `string -> object` or `string -> list` conversions. + + + If this property is null, the default conversion is applied. enum: - none - quantity @@ -170,26 +188,29 @@ spec: - Input type: string fallbackValue: - description: The fallback value that should be returned - by the transform if now pattern matches. + description: |- + The fallback value that should be returned by the transform if now pattern + matches. x-kubernetes-preserve-unknown-fields: true patterns: - description: The patterns that should be tested against - the input string. Patterns are tested in order. - The value of the first match is used as result of - this transform. + description: |- + The patterns that should be tested against the input string. + Patterns are tested in order. The value of the first match is used as + result of this transform. items: - description: MatchTransformPattern is a transform - that returns the value that matches a pattern. + description: |- + MatchTransformPattern is a transform that returns the value that matches a + pattern. properties: literal: - description: Literal exactly matches the input - string (case sensitive). Is required if `type` - is `literal`. + description: |- + Literal exactly matches the input string (case sensitive). + Is required if `type` is `literal`. type: string regexp: - description: Regexp to match against the input - string. Is required if `type` is `regexp`. + description: |- + Regexp to match against the input string. + Is required if `type` is `regexp`. type: string result: description: The value that is used as result @@ -197,14 +218,17 @@ spec: x-kubernetes-preserve-unknown-fields: true type: default: literal - description: "Type specifies how the pattern - matches the input. \n * `literal` - the pattern - value has to exactly match (case sensitive) - the input string. This is the default. \n - * `regexp` - the pattern treated as a regular - expression against which the input string - is tested. Crossplane will throw an error - if the key is not a valid regexp." + description: |- + Type specifies how the pattern matches the input. + + + * `literal` - the pattern value has to exactly match (case sensitive) the + input string. This is the default. + + + * `regexp` - the pattern treated as a regular expression against + which the input string is tested. Crossplane will throw an error if the + key is not a valid regexp. enum: - literal - regexp @@ -216,8 +240,9 @@ spec: type: array type: object math: - description: Math is used to transform the input via mathematical - operations such as multiplication. + description: |- + Math is used to transform the input via mathematical operations such as + multiplication. properties: clampMax: description: ClampMax makes sure that the value is @@ -243,18 +268,18 @@ spec: type: string type: object string: - description: String is used to transform the input into - a string or a different kind of string. Note that the - input does not necessarily need to be a string. + description: |- + String is used to transform the input into a string or a different kind + of string. Note that the input does not necessarily need to be a string. properties: convert: - description: Optional conversion method to be specified. - `ToUpper` and `ToLower` change the letter case of - the input string. `ToBase64` and `FromBase64` perform - a base64 conversion based on the input string. `ToJson` - converts any input value into its raw JSON representation. - `ToSha1`, `ToSha256` and `ToSha512` generate a hash - value based on the input converted to JSON. + description: |- + Optional conversion method to be specified. + `ToUpper` and `ToLower` change the letter case of the input string. + `ToBase64` and `FromBase64` perform a base64 conversion based on the input string. + `ToJson` converts any input value into its raw JSON representation. + `ToSha1`, `ToSha256` and `ToSha512` generate a hash value based on the input + converted to JSON. enum: - ToUpper - ToLower @@ -266,8 +291,9 @@ spec: - ToSha512 type: string fmt: - description: Format the input using a Go format string. - See https://golang.org/pkg/fmt/ for details. + description: |- + Format the input using a Go format string. See + https://golang.org/pkg/fmt/ for details. type: string regexp: description: Extract a match from the input using @@ -278,9 +304,9 @@ spec: matches the entire expression. type: integer match: - description: Match string. May optionally include - submatches, aka capture groups. See https://pkg.go.dev/regexp/ - for details. + description: |- + Match string. May optionally include submatches, aka capture groups. + See https://pkg.go.dev/regexp/ for details. type: string required: - match @@ -314,9 +340,9 @@ spec: type: array type: default: FromCompositeFieldPath - description: Type sets the patching behaviour to be used. Each - patch type may require its own fields to be set on the Patch - object. + description: |- + Type sets the patching behaviour to be used. Each patch type may require + its own fields to be set on the Patch object. enum: - FromCompositeFieldPath - ToCompositeFieldPath @@ -327,15 +353,19 @@ spec: type: array type: object kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object patchSets: - description: PatchSets define a named set of patches that may be included - by any resource. PatchSets cannot themselves refer to other PatchSets. + description: |- + PatchSets define a named set of patches that may be included by any + resource. PatchSets cannot themselves refer to other PatchSets. items: description: A PatchSet is a set of patches that can be reused from all resources. @@ -346,44 +376,49 @@ spec: patches: description: Patches will be applied as an overlay to the base resource. items: - description: PatchSetPatch defines a set of Patches that can be - referenced by name by other patches of type PatchSet. + description: |- + PatchSetPatch defines a set of Patches that can be referenced by name by + other patches of type PatchSet. properties: combine: - description: Combine is the patch configuration for a CombineFromComposite, + description: |- + Combine is the patch configuration for a CombineFromComposite, CombineToComposite patch. properties: strategy: - description: Strategy defines the strategy to use to combine - the input variable values. Currently only string is - supported. + description: |- + Strategy defines the strategy to use to combine the input variable values. + Currently only string is supported. enum: - string type: string string: - description: String declares that input variables should - be combined into a single string, using the relevant - settings for formatting purposes. + description: |- + String declares that input variables should be combined into a single + string, using the relevant settings for formatting purposes. properties: fmt: - description: Format the input using a Go format string. - See https://golang.org/pkg/fmt/ for details. + description: |- + Format the input using a Go format string. See + https://golang.org/pkg/fmt/ for details. type: string required: - fmt type: object variables: - description: Variables are the list of variables whose - values will be retrieved and combined. + description: |- + Variables are the list of variables whose values will be retrieved and + combined. items: - description: A CombineVariable defines the source of - a value that is combined with others to form and patch - an output value. Currently, this only supports retrieving - values from a field path. + description: |- + A CombineVariable defines the source of a value that is combined with + others to form and patch an output value. Currently, this only supports + retrieving values from a field path. properties: fromFieldPath: - description: FromFieldPath is the path of the field - on the source whose value is to be used as input. + description: |- + FromFieldPath is the path of the field on the source whose value is + to be used as input. type: string required: - fromFieldPath @@ -395,49 +430,56 @@ spec: - variables type: object fromFieldPath: - description: FromFieldPath is the path of the field on the - resource whose value is to be used as input. Required when - type is FromCompositeFieldPath or ToCompositeFieldPath. + description: |- + FromFieldPath is the path of the field on the resource whose value is + to be used as input. Required when type is FromCompositeFieldPath or + ToCompositeFieldPath. type: string policy: description: Policy configures the specifics of patching behaviour. properties: fromFieldPath: - description: FromFieldPath specifies how to patch from - a field path. The default is 'Optional', which means - the patch will be a no-op if the specified fromFieldPath - does not exist. Use 'Required' if the patch should fail - if the specified path does not exist. + description: |- + FromFieldPath specifies how to patch from a field path. The default is + 'Optional', which means the patch will be a no-op if the specified + fromFieldPath does not exist. Use 'Required' to prevent the creation of a + new composed resource until the required path exists. enum: - Optional - Required type: string type: object toFieldPath: - description: ToFieldPath is the path of the field on the resource - whose value will be changed with the result of transforms. - Leave empty if you'd like to propagate to the same path - as fromFieldPath. + description: |- + ToFieldPath is the path of the field on the resource whose value will + be changed with the result of transforms. Leave empty if you'd like to + propagate to the same path as fromFieldPath. type: string transforms: - description: Transforms are the list of functions that are - used as a FIFO pipe for the input to be transformed. + description: |- + Transforms are the list of functions that are used as a FIFO pipe for the + input to be transformed. items: - description: Transform is a unit of process whose input - is transformed into an output with the supplied configuration. + description: |- + Transform is a unit of process whose input is transformed into an output with + the supplied configuration. properties: convert: description: Convert is used to cast the input into the given output type. properties: format: - description: "The expected input format. \n * `quantity` - - parses the input as a K8s [`resource.Quantity`](https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity). + description: |- + The expected input format. + + + * `quantity` - parses the input as a K8s [`resource.Quantity`](https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity). Only used during `string -> float64` conversions. * `json` - parses the input as a JSON string. - Only used during `string -> object` or `string - -> list` conversions. \n If this property is null, - the default conversion is applied." + Only used during `string -> object` or `string -> list` conversions. + + + If this property is null, the default conversion is applied. enum: - none - quantity @@ -477,26 +519,29 @@ spec: - Input type: string fallbackValue: - description: The fallback value that should be returned - by the transform if now pattern matches. + description: |- + The fallback value that should be returned by the transform if now pattern + matches. x-kubernetes-preserve-unknown-fields: true patterns: - description: The patterns that should be tested - against the input string. Patterns are tested - in order. The value of the first match is used - as result of this transform. + description: |- + The patterns that should be tested against the input string. + Patterns are tested in order. The value of the first match is used as + result of this transform. items: - description: MatchTransformPattern is a transform - that returns the value that matches a pattern. + description: |- + MatchTransformPattern is a transform that returns the value that matches a + pattern. properties: literal: - description: Literal exactly matches the input - string (case sensitive). Is required if - `type` is `literal`. + description: |- + Literal exactly matches the input string (case sensitive). + Is required if `type` is `literal`. type: string regexp: - description: Regexp to match against the input - string. Is required if `type` is `regexp`. + description: |- + Regexp to match against the input string. + Is required if `type` is `regexp`. type: string result: description: The value that is used as result @@ -504,15 +549,17 @@ spec: x-kubernetes-preserve-unknown-fields: true type: default: literal - description: "Type specifies how the pattern - matches the input. \n * `literal` - the - pattern value has to exactly match (case - sensitive) the input string. This is the - default. \n * `regexp` - the pattern treated - as a regular expression against which the - input string is tested. Crossplane will - throw an error if the key is not a valid - regexp." + description: |- + Type specifies how the pattern matches the input. + + + * `literal` - the pattern value has to exactly match (case sensitive) the + input string. This is the default. + + + * `regexp` - the pattern treated as a regular expression against + which the input string is tested. Crossplane will throw an error if the + key is not a valid regexp. enum: - literal - regexp @@ -524,8 +571,9 @@ spec: type: array type: object math: - description: Math is used to transform the input via - mathematical operations such as multiplication. + description: |- + Math is used to transform the input via mathematical operations such as + multiplication. properties: clampMax: description: ClampMax makes sure that the value @@ -551,19 +599,18 @@ spec: type: string type: object string: - description: String is used to transform the input into - a string or a different kind of string. Note that - the input does not necessarily need to be a string. + description: |- + String is used to transform the input into a string or a different kind + of string. Note that the input does not necessarily need to be a string. properties: convert: - description: Optional conversion method to be specified. - `ToUpper` and `ToLower` change the letter case - of the input string. `ToBase64` and `FromBase64` - perform a base64 conversion based on the input - string. `ToJson` converts any input value into - its raw JSON representation. `ToSha1`, `ToSha256` - and `ToSha512` generate a hash value based on - the input converted to JSON. + description: |- + Optional conversion method to be specified. + `ToUpper` and `ToLower` change the letter case of the input string. + `ToBase64` and `FromBase64` perform a base64 conversion based on the input string. + `ToJson` converts any input value into its raw JSON representation. + `ToSha1`, `ToSha256` and `ToSha512` generate a hash value based on the input + converted to JSON. enum: - ToUpper - ToLower @@ -575,8 +622,9 @@ spec: - ToSha512 type: string fmt: - description: Format the input using a Go format - string. See https://golang.org/pkg/fmt/ for details. + description: |- + Format the input using a Go format string. See + https://golang.org/pkg/fmt/ for details. type: string regexp: description: Extract a match from the input using @@ -587,9 +635,9 @@ spec: matches the entire expression. type: integer match: - description: Match string. May optionally include - submatches, aka capture groups. See https://pkg.go.dev/regexp/ - for details. + description: |- + Match string. May optionally include submatches, aka capture groups. + See https://pkg.go.dev/regexp/ for details. type: string required: - match @@ -625,9 +673,9 @@ spec: type: array type: default: FromCompositeFieldPath - description: Type sets the patching behaviour to be used. - Each patch type may require its own fields to be set on - the ComposedPatch object. + description: |- + Type sets the patching behaviour to be used. Each patch type may require + its own fields to be set on the ComposedPatch object. enum: - FromCompositeFieldPath - ToCompositeFieldPath @@ -646,59 +694,64 @@ spec: type: object type: array resources: - description: Resources is a list of resource templates that will be used - when a composite resource is created. + description: |- + Resources is a list of resource templates that will be used when a + composite resource is created. items: - description: ComposedTemplate is used to provide information about how - the composed resource should be processed. + description: |- + ComposedTemplate is used to provide information about how the composed + resource should be processed. properties: base: - description: Base of the composed resource that patches will be - applied to and from. If base is omitted, a previous Function within - the pipeline must have produced the named composed resource. Patches - will be applied to and from that resource. If base is specified, - and a previous Function within the pipeline produced the name - composed resource, it will be overwritten. + description: |- + Base of the composed resource that patches will be applied to and from. + If base is omitted, a previous Function within the pipeline must have + produced the named composed resource. Patches will be applied to and from + that resource. If base is specified, and a previous Function within the + pipeline produced the name composed resource, it will be overwritten. type: object x-kubernetes-embedded-resource: true x-kubernetes-preserve-unknown-fields: true connectionDetails: - description: ConnectionDetails lists the propagation secret keys - from this composed resource to the composition instance connection - secret. + description: |- + ConnectionDetails lists the propagation secret keys from this composed + resource to the composition instance connection secret. items: - description: ConnectionDetail includes the information about the - propagation of the connection information from one secret to - another. + description: |- + ConnectionDetail includes the information about the propagation of the connection + information from one secret to another. properties: fromConnectionSecretKey: - description: FromConnectionSecretKey is the key that will - be used to fetch the value from the composed resource's - connection secret. + description: |- + FromConnectionSecretKey is the key that will be used to fetch the value + from the composed resource's connection secret. type: string fromFieldPath: - description: FromFieldPath is the path of the field on the - composed resource whose value to be used as input. Name - must be specified if the type is FromFieldPath. + description: |- + FromFieldPath is the path of the field on the composed resource whose + value to be used as input. Name must be specified if the type is + FromFieldPath. type: string name: - description: Name of the connection secret key that will be - propagated to the connection secret of the composed resource. + description: |- + Name of the connection secret key that will be propagated to the + connection secret of the composed resource. type: string type: - description: Type sets the connection detail fetching behavior - to be used. Each connection detail type may require its - own fields to be set on the ConnectionDetail object. + description: |- + Type sets the connection detail fetching behavior to be used. Each + connection detail type may require its own fields to be set on the + ConnectionDetail object. enum: - FromConnectionSecretKey - FromFieldPath - FromValue type: string value: - description: Value that will be propagated to the connection - secret of the composite resource. May be set to inject a - fixed, non-sensitive connection secret value, for example - a well-known port. + description: |- + Value that will be propagated to the connection secret of the composite + resource. May be set to inject a fixed, non-sensitive connection secret + value, for example a well-known port. type: string required: - name @@ -712,47 +765,51 @@ spec: patches: description: Patches to and from the composed resource. items: - description: ComposedPatch objects are applied between composite - and composed resources. Their behaviour depends on the Type - selected. The default Type, FromCompositeFieldPath, copies a - value from the composite resource to the composed resource, - applying any defined transformers. + description: |- + ComposedPatch objects are applied between composite and composed resources. + Their behaviour depends on the Type selected. The default Type, + FromCompositeFieldPath, copies a value from the composite resource to the + composed resource, applying any defined transformers. properties: combine: - description: Combine is the patch configuration for a CombineFromComposite, + description: |- + Combine is the patch configuration for a CombineFromComposite, CombineToComposite patch. properties: strategy: - description: Strategy defines the strategy to use to combine - the input variable values. Currently only string is - supported. + description: |- + Strategy defines the strategy to use to combine the input variable values. + Currently only string is supported. enum: - string type: string string: - description: String declares that input variables should - be combined into a single string, using the relevant - settings for formatting purposes. + description: |- + String declares that input variables should be combined into a single + string, using the relevant settings for formatting purposes. properties: fmt: - description: Format the input using a Go format string. - See https://golang.org/pkg/fmt/ for details. + description: |- + Format the input using a Go format string. See + https://golang.org/pkg/fmt/ for details. type: string required: - fmt type: object variables: - description: Variables are the list of variables whose - values will be retrieved and combined. + description: |- + Variables are the list of variables whose values will be retrieved and + combined. items: - description: A CombineVariable defines the source of - a value that is combined with others to form and patch - an output value. Currently, this only supports retrieving - values from a field path. + description: |- + A CombineVariable defines the source of a value that is combined with + others to form and patch an output value. Currently, this only supports + retrieving values from a field path. properties: fromFieldPath: - description: FromFieldPath is the path of the field - on the source whose value is to be used as input. + description: |- + FromFieldPath is the path of the field on the source whose value is + to be used as input. type: string required: - fromFieldPath @@ -764,9 +821,10 @@ spec: - variables type: object fromFieldPath: - description: FromFieldPath is the path of the field on the - resource whose value is to be used as input. Required when - type is FromCompositeFieldPath or ToCompositeFieldPath. + description: |- + FromFieldPath is the path of the field on the resource whose value is + to be used as input. Required when type is FromCompositeFieldPath or + ToCompositeFieldPath. type: string patchSetName: description: PatchSetName to include patches from. Required @@ -776,41 +834,47 @@ spec: description: Policy configures the specifics of patching behaviour. properties: fromFieldPath: - description: FromFieldPath specifies how to patch from - a field path. The default is 'Optional', which means - the patch will be a no-op if the specified fromFieldPath - does not exist. Use 'Required' if the patch should fail - if the specified path does not exist. + description: |- + FromFieldPath specifies how to patch from a field path. The default is + 'Optional', which means the patch will be a no-op if the specified + fromFieldPath does not exist. Use 'Required' to prevent the creation of a + new composed resource until the required path exists. enum: - Optional - Required type: string type: object toFieldPath: - description: ToFieldPath is the path of the field on the resource - whose value will be changed with the result of transforms. - Leave empty if you'd like to propagate to the same path - as fromFieldPath. + description: |- + ToFieldPath is the path of the field on the resource whose value will + be changed with the result of transforms. Leave empty if you'd like to + propagate to the same path as fromFieldPath. type: string transforms: - description: Transforms are the list of functions that are - used as a FIFO pipe for the input to be transformed. + description: |- + Transforms are the list of functions that are used as a FIFO pipe for the + input to be transformed. items: - description: Transform is a unit of process whose input - is transformed into an output with the supplied configuration. + description: |- + Transform is a unit of process whose input is transformed into an output with + the supplied configuration. properties: convert: description: Convert is used to cast the input into the given output type. properties: format: - description: "The expected input format. \n * `quantity` - - parses the input as a K8s [`resource.Quantity`](https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity). + description: |- + The expected input format. + + + * `quantity` - parses the input as a K8s [`resource.Quantity`](https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity). Only used during `string -> float64` conversions. * `json` - parses the input as a JSON string. - Only used during `string -> object` or `string - -> list` conversions. \n If this property is null, - the default conversion is applied." + Only used during `string -> object` or `string -> list` conversions. + + + If this property is null, the default conversion is applied. enum: - none - quantity @@ -850,26 +914,29 @@ spec: - Input type: string fallbackValue: - description: The fallback value that should be returned - by the transform if now pattern matches. + description: |- + The fallback value that should be returned by the transform if now pattern + matches. x-kubernetes-preserve-unknown-fields: true patterns: - description: The patterns that should be tested - against the input string. Patterns are tested - in order. The value of the first match is used - as result of this transform. + description: |- + The patterns that should be tested against the input string. + Patterns are tested in order. The value of the first match is used as + result of this transform. items: - description: MatchTransformPattern is a transform - that returns the value that matches a pattern. + description: |- + MatchTransformPattern is a transform that returns the value that matches a + pattern. properties: literal: - description: Literal exactly matches the input - string (case sensitive). Is required if - `type` is `literal`. + description: |- + Literal exactly matches the input string (case sensitive). + Is required if `type` is `literal`. type: string regexp: - description: Regexp to match against the input - string. Is required if `type` is `regexp`. + description: |- + Regexp to match against the input string. + Is required if `type` is `regexp`. type: string result: description: The value that is used as result @@ -877,15 +944,17 @@ spec: x-kubernetes-preserve-unknown-fields: true type: default: literal - description: "Type specifies how the pattern - matches the input. \n * `literal` - the - pattern value has to exactly match (case - sensitive) the input string. This is the - default. \n * `regexp` - the pattern treated - as a regular expression against which the - input string is tested. Crossplane will - throw an error if the key is not a valid - regexp." + description: |- + Type specifies how the pattern matches the input. + + + * `literal` - the pattern value has to exactly match (case sensitive) the + input string. This is the default. + + + * `regexp` - the pattern treated as a regular expression against + which the input string is tested. Crossplane will throw an error if the + key is not a valid regexp. enum: - literal - regexp @@ -897,8 +966,9 @@ spec: type: array type: object math: - description: Math is used to transform the input via - mathematical operations such as multiplication. + description: |- + Math is used to transform the input via mathematical operations such as + multiplication. properties: clampMax: description: ClampMax makes sure that the value @@ -924,19 +994,18 @@ spec: type: string type: object string: - description: String is used to transform the input into - a string or a different kind of string. Note that - the input does not necessarily need to be a string. + description: |- + String is used to transform the input into a string or a different kind + of string. Note that the input does not necessarily need to be a string. properties: convert: - description: Optional conversion method to be specified. - `ToUpper` and `ToLower` change the letter case - of the input string. `ToBase64` and `FromBase64` - perform a base64 conversion based on the input - string. `ToJson` converts any input value into - its raw JSON representation. `ToSha1`, `ToSha256` - and `ToSha512` generate a hash value based on - the input converted to JSON. + description: |- + Optional conversion method to be specified. + `ToUpper` and `ToLower` change the letter case of the input string. + `ToBase64` and `FromBase64` perform a base64 conversion based on the input string. + `ToJson` converts any input value into its raw JSON representation. + `ToSha1`, `ToSha256` and `ToSha512` generate a hash value based on the input + converted to JSON. enum: - ToUpper - ToLower @@ -948,8 +1017,9 @@ spec: - ToSha512 type: string fmt: - description: Format the input using a Go format - string. See https://golang.org/pkg/fmt/ for details. + description: |- + Format the input using a Go format string. See + https://golang.org/pkg/fmt/ for details. type: string regexp: description: Extract a match from the input using @@ -960,9 +1030,9 @@ spec: matches the entire expression. type: integer match: - description: Match string. May optionally include - submatches, aka capture groups. See https://pkg.go.dev/regexp/ - for details. + description: |- + Match string. May optionally include submatches, aka capture groups. + See https://pkg.go.dev/regexp/ for details. type: string required: - match @@ -998,9 +1068,9 @@ spec: type: array type: default: FromCompositeFieldPath - description: Type sets the patching behaviour to be used. - Each patch type may require its own fields to be set on - the ComposedPatch object. + description: |- + Type sets the patching behaviour to be used. Each patch type may require + its own fields to be set on the ComposedPatch object. enum: - FromCompositeFieldPath - PatchSet @@ -1020,13 +1090,15 @@ spec: status: "True" type: Ready type: MatchCondition - description: ReadinessChecks allows users to define custom readiness - checks. All checks have to return true in order for resource to - be considered ready. The default readiness check is to have the - "Ready" condition to be "True". + description: |- + ReadinessChecks allows users to define custom readiness checks. All + checks have to return true in order for resource to be considered ready. + The default readiness check is to have the "Ready" condition to be + "True". items: - description: ReadinessCheck is used to indicate how to tell whether - a resource is ready for consumption + description: |- + ReadinessCheck is used to indicate how to tell whether a resource is ready + for consumption properties: fieldPath: description: FieldPath shows the path of the field whose value diff --git a/patches.go b/patches.go index 19fb918..7cdce55 100644 --- a/patches.go +++ b/patches.go @@ -5,17 +5,19 @@ import ( "strings" "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "github.com/crossplane/crossplane-runtime/pkg/fieldpath" - "github.com/crossplane/crossplane-runtime/pkg/resource" + + "github.com/crossplane/function-sdk-go/resource/composed" + "github.com/crossplane/function-sdk-go/resource/composite" "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1" ) const ( - errPatchSetType = "a patch in a PatchSet cannot be of type PatchSet" - errCombineRequiresVariables = "combine patch types require at least one variable" + errPatchSetType = "a patch in a PatchSet cannot be of type PatchSet" errFmtUndefinedPatchSet = "cannot find PatchSet by name %s" errFmtInvalidPatchType = "patch type %s is unsupported" @@ -41,50 +43,6 @@ type PatchWithPatchSetName interface { GetPatchSetName() string } -// Apply executes a patching operation between the from and to resources. -// Applies all patch types unless an 'only' filter is supplied. -func Apply(p PatchInterface, xr resource.Composite, cd resource.Composed, only ...v1beta1.PatchType) error { - return ApplyToObjects(p, xr, cd, only...) -} - -// ApplyToObjects works like Apply but accepts any kind of runtime.Object. It -// might be vulnerable to conversion panics (see -// https://github.com/crossplane/crossplane/pull/3394 for details). -func ApplyToObjects(p PatchInterface, a, b runtime.Object, only ...v1beta1.PatchType) error { - if filterPatch(p, only...) { - return nil - } - - switch p.GetType() { - case v1beta1.PatchTypeFromCompositeFieldPath, v1beta1.PatchTypeFromEnvironmentFieldPath: - return ApplyFromFieldPathPatch(p, a, b) - case v1beta1.PatchTypeToCompositeFieldPath, v1beta1.PatchTypeToEnvironmentFieldPath: - return ApplyFromFieldPathPatch(p, b, a) - case v1beta1.PatchTypeCombineFromComposite, v1beta1.PatchTypeCombineFromEnvironment: - return ApplyCombineFromVariablesPatch(p, a, b) - case v1beta1.PatchTypeCombineToComposite, v1beta1.PatchTypeCombineToEnvironment: - return ApplyCombineFromVariablesPatch(p, b, a) - case v1beta1.PatchTypePatchSet: - // Already resolved - nothing to do. - } - return errors.Errorf(errFmtInvalidPatchType, p.GetType()) -} - -// filterPatch returns true if patch should be filtered (not applied) -func filterPatch(p PatchInterface, only ...v1beta1.PatchType) bool { - // filter does not apply if not set - if len(only) == 0 { - return false - } - - for _, patchType := range only { - if patchType == p.GetType() { - return false - } - } - return true -} - // ResolveTransforms applies a list of transforms to a patch value. func ResolveTransforms(ts []v1beta1.Transform, input any) (any, error) { var err error @@ -101,19 +59,12 @@ func ResolveTransforms(ts []v1beta1.Transform, input any) (any, error) { // on the "from" resource. Values may be transformed if any are defined on // the patch. func ApplyFromFieldPathPatch(p PatchInterface, from, to runtime.Object) error { - if p.GetFromFieldPath() == "" { - return errors.Errorf(errFmtRequiredField, "FromFieldPath", p.GetType()) - } - fromMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(from) if err != nil { return err } in, err := fieldpath.Pave(fromMap).GetValue(p.GetFromFieldPath()) - if IsOptionalFieldPathNotFound(err, p.GetPolicy()) { - return nil - } if err != nil { return err } @@ -133,39 +84,23 @@ func ApplyFromFieldPathPatch(p PatchInterface, from, to runtime.Object) error { } // ApplyCombineFromVariablesPatch patches the "to" resource, taking a list of -// input variables and combining them into a single output value. -// The single output value may then be further transformed if they are defined -// on the patch. +// input variables and combining them into a single output value. The single +// output value may then be further transformed if they are defined on the +// patch. func ApplyCombineFromVariablesPatch(p PatchInterface, from, to runtime.Object) error { - // Combine patch requires configuration - if p.GetCombine() == nil { - return errors.Errorf(errFmtRequiredField, "Combine", p.GetType()) - } - // Destination field path is required since we can't default to multiple - // fields. - if p.GetToFieldPath() == "" { - return errors.Errorf(errFmtRequiredField, "ToFieldPath", p.GetType()) - } - - combine := p.GetCombine() - vl := len(combine.Variables) - - if vl < 1 { - return errors.New(errCombineRequiresVariables) - } - fromMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(from) if err != nil { return err } - in := make([]any, vl) + c := p.GetCombine() + in := make([]any, len(c.Variables)) // Get value of each variable // NOTE: This currently assumes all variables define a 'fromFieldPath' // value. If we add new variable types, this may not be the case and // this code may be better served split out into a dedicated function. - for i, sp := range combine.Variables { + for i, sp := range c.Variables { iv, err := fieldpath.Pave(fromMap).GetValue(sp.FromFieldPath) // If any source field is not found, we will not @@ -174,9 +109,6 @@ func ApplyCombineFromVariablesPatch(p PatchInterface, from, to runtime.Object) e // number of inputs (e.g. a string format // expecting 3 fields '%s-%s-%s' but only // receiving 2 values). - if IsOptionalFieldPathNotFound(err, p.GetPolicy()) { - return nil - } if err != nil { return err } @@ -184,7 +116,7 @@ func ApplyCombineFromVariablesPatch(p PatchInterface, from, to runtime.Object) e } // Combine input values - cb, err := Combine(*p.GetCombine(), in) + cb, err := Combine(*c, in) if err != nil { return err } @@ -198,20 +130,108 @@ func ApplyCombineFromVariablesPatch(p PatchInterface, from, to runtime.Object) e return errors.Wrap(patchFieldValueToObject(p.GetToFieldPath(), out, to), "cannot patch to object") } -// IsOptionalFieldPathNotFound returns true if the supplied error indicates a -// field path was not found, and the supplied policy indicates a patch from that -// field path was optional. -func IsOptionalFieldPathNotFound(err error, p *v1beta1.PatchPolicy) bool { - switch { - case p == nil: - fallthrough - case p.FromFieldPath == nil: - fallthrough - case *p.FromFieldPath == v1beta1.FromFieldPathPolicyOptional: - return fieldpath.IsNotFound(err) - default: +// ApplyEnvironmentPatch applies a patch to or from the environment. Patches to +// the environment are always from the observed XR. Patches from the environment +// are always to the desired XR. +func ApplyEnvironmentPatch(p *v1beta1.EnvironmentPatch, env *unstructured.Unstructured, oxr, dxr *composite.Unstructured) error { + switch p.GetType() { + // From observed XR to environment. + case v1beta1.PatchTypeFromCompositeFieldPath: + return ApplyFromFieldPathPatch(p, oxr, env) + case v1beta1.PatchTypeCombineFromComposite: + return ApplyCombineFromVariablesPatch(p, oxr, env) + + // From environment to desired XR. + case v1beta1.PatchTypeToCompositeFieldPath: + return ApplyFromFieldPathPatch(p, env, dxr) + case v1beta1.PatchTypeCombineToComposite: + return ApplyCombineFromVariablesPatch(p, env, dxr) + + // Invalid patch types in this context. + case v1beta1.PatchTypeFromEnvironmentFieldPath, + v1beta1.PatchTypeCombineFromEnvironment, + v1beta1.PatchTypeToEnvironmentFieldPath, + v1beta1.PatchTypeCombineToEnvironment: + // Nothing to do. + + case v1beta1.PatchTypePatchSet: + // Already resolved - nothing to do. + } + return nil +} + +// ApplyComposedPatch applies a patch to or from a composed resource. Patches +// from an observed composed resource can be to the desired XR, or to the +// environment. Patches to a desired composed resource can be from the observed +// XR, or from the environment. +func ApplyComposedPatch(p *v1beta1.ComposedPatch, ocd, dcd *composed.Unstructured, oxr, dxr *composite.Unstructured, env *unstructured.Unstructured) error { //nolint:gocyclo // Just a long switch. + // Don't return an error if we're patching from a composed resource that + // doesn't exist yet. We'll try patch from it once it's been created. + if ocd == nil && !ToComposedResource(p) { + return nil + } + + // We always patch from observed state to desired state. This is because + // folks will often want to patch from status fields, which only appear in + // observed state. Observed state should also eventually be consistent with + // desired state. + switch t := p.GetType(); t { + + // From observed composed resource to desired XR. + case v1beta1.PatchTypeToCompositeFieldPath: + return ApplyFromFieldPathPatch(p, ocd, dxr) + case v1beta1.PatchTypeCombineToComposite: + return ApplyCombineFromVariablesPatch(p, ocd, dxr) + + // From observed composed resource to environment. + case v1beta1.PatchTypeToEnvironmentFieldPath: + return ApplyFromFieldPathPatch(p, ocd, env) + case v1beta1.PatchTypeCombineToEnvironment: + return ApplyCombineFromVariablesPatch(p, ocd, env) + + // From observed XR to desired composed resource. + case v1beta1.PatchTypeFromCompositeFieldPath: + return ApplyFromFieldPathPatch(p, oxr, dcd) + case v1beta1.PatchTypeCombineFromComposite: + return ApplyCombineFromVariablesPatch(p, oxr, dcd) + + // From environment to desired composed resource. + case v1beta1.PatchTypeFromEnvironmentFieldPath: + return ApplyFromFieldPathPatch(p, env, dcd) + case v1beta1.PatchTypeCombineFromEnvironment: + return ApplyCombineFromVariablesPatch(p, env, dcd) + + case v1beta1.PatchTypePatchSet: + // Already resolved - nothing to do. + } + + return nil +} + +// ToComposedResource returns true if the supplied patch is to a composed +// resource, not from it. +func ToComposedResource(p *v1beta1.ComposedPatch) bool { + switch p.GetType() { + + // From observed XR to desired composed resource. + case v1beta1.PatchTypeFromCompositeFieldPath, v1beta1.PatchTypeCombineFromComposite: + return true + // From environment to desired composed resource. + case v1beta1.PatchTypeFromEnvironmentFieldPath, v1beta1.PatchTypeCombineFromEnvironment: + return true + + // From composed resource to composite. + case v1beta1.PatchTypeToCompositeFieldPath, v1beta1.PatchTypeCombineToComposite: + return false + // From composed resource to environment. + case v1beta1.PatchTypeToEnvironmentFieldPath, v1beta1.PatchTypeCombineToEnvironment: + return false + // We can ignore patchsets; they're inlined. + case v1beta1.PatchTypePatchSet: return false } + + return false } // Combine calls the appropriate combiner. diff --git a/patches_test.go b/patches_test.go index a98f158..bc26cbe 100644 --- a/patches_test.go +++ b/patches_test.go @@ -8,34 +8,27 @@ import ( "github.com/pkg/errors" extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" "github.com/crossplane/crossplane-runtime/pkg/fieldpath" - "github.com/crossplane/crossplane-runtime/pkg/resource" - "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed" "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/function-sdk-go/resource/composed" "github.com/crossplane/function-sdk-go/resource/composite" "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1" ) -func TestPatchApply(t *testing.T) { - errNotFound := func(path string) error { - p := &fieldpath.Paved{} - _, err := p.GetValue(path) - return err - } - +func TestApplyFromFieldPathPatch(t *testing.T) { type args struct { - patch v1beta1.ComposedPatch - xr *composite.Unstructured - cd *composed.Unstructured - only []v1beta1.PatchType + p PatchInterface + from runtime.Object + to runtime.Object } + type want struct { - xr *composite.Unstructured - cd *composed.Unstructured + to runtime.Object err error } @@ -44,559 +37,249 @@ func TestPatchApply(t *testing.T) { args want }{ - "InvalidCompositeFieldPathPatch": { - reason: "Should return error when required fields not passed to applyFromFieldPathPatch", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeFromCompositeFieldPath, - // This is missing fields. - }, - xr: &composite.Unstructured{}, - cd: &composed.Unstructured{}, - }, - want: want{ - err: errors.Errorf(errFmtRequiredField, "FromFieldPath", v1beta1.PatchTypeFromCompositeFieldPath), - }, - }, - "Invalidv1.PatchType": { - reason: "Should return an error if an invalid patch type is specified", + "ValidFromCompositeFieldPath": { + reason: "Should correctly apply a valid FromCompositeFieldPath patch", args: args{ - patch: v1beta1.ComposedPatch{ - Type: "invalid-patchtype", - }, - xr: &composite.Unstructured{}, - cd: &composed.Unstructured{}, - }, - want: want{ - err: errors.Errorf(errFmtInvalidPatchType, "invalid-patchtype"), - }, - }, - "ValidCompositeFieldPathPatch": { - reason: "Should correctly apply a CompositeFieldPathPatch with valid settings", - args: args{ - patch: v1beta1.ComposedPatch{ + p: &v1beta1.ComposedPatch{ Type: v1beta1.PatchTypeFromCompositeFieldPath, Patch: v1beta1.Patch{ FromFieldPath: ptr.To[string]("metadata.labels"), ToFieldPath: ptr.To[string]("metadata.labels"), }, }, - xr: &composite.Unstructured{ + from: &composite.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "labels": { - "test": "blah" + "apiVersion": "test.crossplane.io/v1", + "kind": "XR", + "metadata": { + "labels": { + "test": "blah" + } } - } - }`)}, + }`)}, }, - cd: &composed.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "name": "cd" - } - }`)}, + "apiVersion": "test.crossplane.io/v1", + "kind": "Composed", + "metadata": { + "name": "cd" + } + }`)}, }, }, want: want{ - cd: &composed.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "name": "cd", - "labels": { - "test": "blah" + "apiVersion": "test.crossplane.io/v1", + "kind": "Composed", + "metadata": { + "name": "cd", + "labels": { + "test": "blah" + } } - } - }`)}, + }`)}, }, err: nil, }, }, - "ValidCompositeFieldPathPatchWithWildcards": { + "ValidFromFieldPathWithWildcards": { reason: "When passed a wildcarded path, adds a field to each element of an array", args: args{ - patch: v1beta1.ComposedPatch{ + p: &v1beta1.ComposedPatch{ Type: v1beta1.PatchTypeFromCompositeFieldPath, Patch: v1beta1.Patch{ FromFieldPath: ptr.To[string]("metadata.name"), ToFieldPath: ptr.To[string]("metadata.ownerReferences[*].name"), }, }, - xr: &composite.Unstructured{ + from: &composite.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "name": "test" - } - }`)}, + "apiVersion": "test.crossplane.io/v1", + "kind": "XR", + "metadata": { + "name": "test" + } + }`)}, }, - cd: &composed.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "ownerReferences": [ - { - "name": "" - }, - { - "name": "" - } - ] - } - }`)}, + "apiVersion": "test.crossplane.io/v1", + "kind": "Composed", + "metadata": { + "ownerReferences": [ + { + "name": "" + }, + { + "name": "" + } + ] + } + }`)}, }, }, want: want{ - cd: &composed.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "ownerReferences": [ - { - "name": "test" - }, - { - "name": "test" - } - ] - } - }`)}, + "apiVersion": "test.crossplane.io/v1", + "kind": "Composed", + "metadata": { + "ownerReferences": [ + { + "name": "test" + }, + { + "name": "test" + } + ] + } + }`)}, }, }, }, "InvalidCompositeFieldPathPatchWithWildcards": { reason: "When passed a wildcarded path, throws an error if ToFieldPath cannot be expanded", args: args{ - patch: v1beta1.ComposedPatch{ + p: &v1beta1.ComposedPatch{ Type: v1beta1.PatchTypeFromCompositeFieldPath, Patch: v1beta1.Patch{ FromFieldPath: ptr.To[string]("metadata.name"), ToFieldPath: ptr.To[string]("metadata.ownerReferences[*].badField"), }, }, - xr: &composite.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "name": "test" - } - }`)}, - }, - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "ownerReferences": [ - { - "name": "test" - }, - { - "name": "test" - } - ] - } - }`)}, - }, - }, - want: want{ - err: errors.Errorf(errFmtExpandingArrayFieldPaths, "metadata.ownerReferences[*].badField"), - }, - }, - "MissingOptionalFieldPath": { - reason: "A FromFieldPath patch should be a no-op when an optional fromFieldPath doesn't exist", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeFromCompositeFieldPath, - Patch: v1beta1.Patch{ - FromFieldPath: ptr.To[string]("metadata.labels"), - ToFieldPath: ptr.To[string]("metadata.labels"), - }, - }, - xr: &composite.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "name": "test" - } - }`)}, - }, - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "name": "test" - } - }`)}, - }, - }, - want: want{ - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "name": "test" - } - }`)}, - }, - err: nil, - }, - }, - "MissingRequiredFieldPath": { - reason: "A FromFieldPath patch should return an error when a required fromFieldPath doesn't exist", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeFromCompositeFieldPath, - Patch: v1beta1.Patch{ - FromFieldPath: ptr.To[string]("wat"), - Policy: &v1beta1.PatchPolicy{ - FromFieldPath: func() *v1beta1.FromFieldPathPolicy { - s := v1beta1.FromFieldPathPolicyRequired - return &s - }(), - }, - ToFieldPath: ptr.To[string]("wat"), - }, - }, - xr: &composite.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "name": "test" - } - }`)}, - }, - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "name": "test" - } - }`)}, - }, - }, - want: want{ - cd: &composed.Unstructured{ + from: &composite.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "name": "test" - } - }`)}, - }, - err: errNotFound("wat"), - }, - }, - "FilterExcludeCompositeFieldPathPatch": { - reason: "Should not apply the patch as the v1.PatchType is not present in filter.", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeFromCompositeFieldPath, - Patch: v1beta1.Patch{ - FromFieldPath: ptr.To[string]("metadata.labels"), - ToFieldPath: ptr.To[string]("metadata.labels"), - }, - }, - xr: &composite.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "labels": { - "test": "blah" + "apiVersion": "test.crossplane.io/v1", + "kind": "XR", + "metadata": { + "name": "test" } - } - }`)}, - }, - cd: &composed.Unstructured{}, - only: []v1beta1.PatchType{v1beta1.PatchTypePatchSet}, - }, - want: want{ - cd: &composed.Unstructured{}, - err: nil, - }, - }, - "FilterIncludeCompositeFieldPathPatch": { - reason: "Should apply the patch as the v1.PatchType is present in filter.", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeFromCompositeFieldPath, - Patch: v1beta1.Patch{ - FromFieldPath: ptr.To[string]("metadata.labels"), - ToFieldPath: ptr.To[string]("metadata.labels"), - }, + }`)}, }, - xr: &composite.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "labels": { - "test": "blah" + "apiVersion": "test.crossplane.io/v1", + "kind": "Composed", + "metadata": { + "ownerReferences": [ + { + "name": "test" + }, + { + "name": "test" + } + ] } - } - }`)}, - }, - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed" - }`)}, + }`)}, }, - only: []v1beta1.PatchType{v1beta1.PatchTypeFromCompositeFieldPath}, }, want: want{ - cd: &composed.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "labels": { - "test": "blah" + "apiVersion": "test.crossplane.io/v1", + "kind": "Composed", + "metadata": { + "ownerReferences": [ + { + "name": "test" + }, + { + "name": "test" + } + ] } - } - }`)}, + }`)}, }, - err: nil, + err: errors.Errorf(errFmtExpandingArrayFieldPaths, "metadata.ownerReferences[*].badField"), }, }, "DefaultToFieldCompositeFieldPathPatch": { reason: "Should correctly default the ToFieldPath value if not specified.", args: args{ - patch: v1beta1.ComposedPatch{ + p: &v1beta1.ComposedPatch{ Type: v1beta1.PatchTypeFromCompositeFieldPath, Patch: v1beta1.Patch{ FromFieldPath: ptr.To[string]("metadata.labels"), }, }, - xr: &composite.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "labels": { - "test": "blah" - } - } - }`)}, - }, - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed" - }`)}, - }, - }, - want: want{ - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "labels": { - "test": "blah" - } - } - }`)}, - }, - err: nil, - }, - }, - "ValidToCompositeFieldPathPatch": { - reason: "Should correctly apply a ToCompositeFieldPath patch with valid settings", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeToCompositeFieldPath, - Patch: v1beta1.Patch{ - FromFieldPath: ptr.To[string]("metadata.labels"), - ToFieldPath: ptr.To[string]("metadata.labels"), - }, - }, - xr: &composite.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR" - }`)}, - }, - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "labels": { - "test": "blah" - } - } - }`)}, - }, - }, - want: want{ - xr: &composite.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "labels": { - "test": "blah" - } - } - }`)}, - }, - err: nil, - }, - }, - "ValidToCompositeFieldPathPatchWithWildcards": { - reason: "When passed a wildcarded path, adds a field to each element of an array", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeToCompositeFieldPath, - Patch: v1beta1.Patch{ - FromFieldPath: ptr.To[string]("metadata.name"), - ToFieldPath: ptr.To[string]("metadata.ownerReferences[*].name"), - }, - }, - xr: &composite.Unstructured{ + from: &composite.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "ownerReferences": [ - { - "name": "" - }, - { - "name": "" + "apiVersion": "test.crossplane.io/v1", + "kind": "XR", + "metadata": { + "labels": { + "test": "blah" } - ] - } - }`)}, + } + }`)}, }, - cd: &composed.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "name": "test" - } - }`)}, + "apiVersion": "test.crossplane.io/v1", + "kind": "Composed" + }`)}, }, }, want: want{ - xr: &composite.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "ownerReferences": [ - { - "name": "test" - }, - { - "name": "test" + "apiVersion": "test.crossplane.io/v1", + "kind": "Composed", + "metadata": { + "labels": { + "test": "blah" } - ] - } - }`)}, - }, - }, - }, - "MissingCombineFromCompositeConfig": { - reason: "Should return an error if Combine config is not passed", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeCombineFromComposite, - Patch: v1beta1.Patch{ - ToFieldPath: ptr.To[string]("metadata.labels.destination"), - // Missing a Combine field - Combine: nil, - }, - }, - xr: &composite.Unstructured{}, - cd: &composed.Unstructured{}, - }, - want: want{ - xr: &composite.Unstructured{}, - cd: &composed.Unstructured{}, - err: errors.Errorf(errFmtRequiredField, "Combine", v1beta1.PatchTypeCombineFromComposite), - }, - }, - "MissingCombineStrategyFromCompositeConfig": { - reason: "Should return an error if Combine strategy config is not passed", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeCombineFromComposite, - Patch: v1beta1.Patch{ - Combine: &v1beta1.Combine{ - Variables: []v1beta1.CombineVariable{ - {FromFieldPath: "metadata.labels.source1"}, - {FromFieldPath: "metadata.labels.source2"}, - }, - Strategy: v1beta1.CombineStrategyString, - // Missing a String combine config. - }, - ToFieldPath: ptr.To[string]("metadata.labels.destination"), - }, - }, - xr: &composite.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "labels": { - "source1": "a", - "source2": "b" } - } - }`)}, - }, - }, - want: want{ - err: errors.Errorf(errFmtCombineConfigMissing, v1beta1.CombineStrategyString), - }, - }, - "MissingCombineVariablesFromCompositeConfig": { - reason: "Should return an error if no variables have been passed", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeCombineFromComposite, - Patch: v1beta1.Patch{ - Combine: &v1beta1.Combine{ - // This is empty. - Variables: []v1beta1.CombineVariable{}, - Strategy: v1beta1.CombineStrategyString, - String: &v1beta1.StringCombine{Format: "%s-%s"}, - }, - ToFieldPath: ptr.To[string]("objectMeta.labels.destination"), - }, + }`)}, }, - }, - want: want{ - err: errors.New(errCombineRequiresVariables), + err: nil, }, }, - "NoOpOptionalInputFieldFromCompositeConfig": { - // Note: OptionalFieldPathNotFound is tested below, but we want to - // test that we abort the patch if _any_ of our source fields are - // not available. + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := ApplyFromFieldPathPatch(tc.args.p, tc.args.from, tc.args.to) + + if diff := cmp.Diff(tc.want.to, tc.args.to); diff != "" { + t.Errorf("\n%s\nApplyFromFieldPathPatch(...): -want, +got:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nApplyFromFieldPathPatch(): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +func TestApplyCombineFromVariablesPatch(t *testing.T) { + errNotFound := func(path string) error { + p := &fieldpath.Paved{} + _, err := p.GetValue(path) + return err + } + + type args struct { + p PatchInterface + from runtime.Object + to runtime.Object + } + + type want struct { + to runtime.Object + err error + } + + cases := map[string]struct { + reason string + args + want + }{ + "VariableFromFieldPathNotFound": { reason: "Should return no error and not apply patch if an optional variable is missing", args: args{ - patch: v1beta1.ComposedPatch{ + p: &v1beta1.ComposedPatch{ Type: v1beta1.PatchTypeCombineFromComposite, Patch: v1beta1.Patch{ Combine: &v1beta1.Combine{ @@ -611,27 +294,21 @@ func TestPatchApply(t *testing.T) { ToFieldPath: ptr.To[string]("metadata.labels.destination"), }, }, - xr: &composite.Unstructured{ + from: &composite.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "labels": { - "source1": "foo", - "source3": "baz" - } - } - }`)}, + "apiVersion": "test.crossplane.io/v1", + "kind": "XR" + }`)}, }, }, want: want{ - err: nil, + err: errNotFound("metadata"), }, }, "ValidCombineFromComposite": { reason: "Should correctly apply a CombineFromComposite patch with valid settings", args: args{ - patch: v1beta1.ComposedPatch{ + p: &v1beta1.ComposedPatch{ Type: v1beta1.PatchTypeCombineFromComposite, Patch: v1beta1.Patch{ Combine: &v1beta1.Combine{ @@ -645,92 +322,35 @@ func TestPatchApply(t *testing.T) { ToFieldPath: ptr.To[string]("metadata.labels.destination"), }, }, - xr: &composite.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "XR", - "metadata": { - "labels": { - "source1": "foo", - "source2": "bar" - } - } - }`)}, - }, - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "labels": { - "test": "blah" - } - } - }`)}, - }, - }, - want: want{ - cd: &composed.Unstructured{ - Unstructured: unstructured.Unstructured{Object: MustObject(`{ - "apiVersion": "test.crossplane.io/v1", - "kind": "Composed", - "metadata": { - "labels": { - "destination": "foo-bar", - "test": "blah" - } - } - }`)}, - }, - err: nil, - }, - }, - "ValidCombineToComposite": { - reason: "Should correctly apply a CombineToComposite patch with valid settings", - args: args{ - patch: v1beta1.ComposedPatch{ - Type: v1beta1.PatchTypeCombineToComposite, - Patch: v1beta1.Patch{ - Combine: &v1beta1.Combine{ - Variables: []v1beta1.CombineVariable{ - {FromFieldPath: "metadata.labels.source1"}, - {FromFieldPath: "metadata.labels.source2"}, - }, - Strategy: v1beta1.CombineStrategyString, - String: &v1beta1.StringCombine{Format: "%s-%s"}, - }, - ToFieldPath: ptr.To[string]("metadata.labels.destination"), - }, - }, - xr: &composite.Unstructured{ + from: &composite.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ "apiVersion": "test.crossplane.io/v1", "kind": "XR", "metadata": { "labels": { - "test": "blah" + "source1": "foo", + "source2": "bar" } } }`)}, }, - cd: &composed.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ "apiVersion": "test.crossplane.io/v1", "kind": "Composed", "metadata": { "labels": { - "source1": "foo", - "source2": "bar" + "test": "blah" } } }`)}, }, }, want: want{ - xr: &composite.Unstructured{ + to: &composed.Unstructured{ Unstructured: unstructured.Unstructured{Object: MustObject(`{ "apiVersion": "test.crossplane.io/v1", - "kind": "XR", + "kind": "Composed", "metadata": { "labels": { "destination": "foo-bar", @@ -745,21 +365,14 @@ func TestPatchApply(t *testing.T) { } for name, tc := range cases { t.Run(name, func(t *testing.T) { - ncp := tc.args.xr.DeepCopyObject().(resource.Composite) - err := Apply(&tc.args.patch, ncp, tc.args.cd, tc.args.only...) + err := ApplyCombineFromVariablesPatch(tc.args.p, tc.args.from, tc.args.to) - if tc.want.xr != nil { - if diff := cmp.Diff(tc.want.xr, ncp); diff != "" { - t.Errorf("\n%s\nApply(cp): -want, +got:\n%s", tc.reason, diff) - } - } - if tc.want.cd != nil { - if diff := cmp.Diff(tc.want.cd, tc.args.cd); diff != "" { - t.Errorf("\n%s\nApply(cd): -want, +got:\n%s", tc.reason, diff) - } + if diff := cmp.Diff(tc.want.to, tc.args.to); diff != "" { + t.Errorf("\n%s\nApplyCombineFromVariablesPatch(...): -want, +got:\n%s", tc.reason, diff) } + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { - t.Errorf("\n%s\nApply(err): -want, +got:\n%s", tc.reason, diff) + t.Errorf("\n%s\nApplyCombineFromVariablesPatch(): -want error, +got error:\n%s", tc.reason, diff) } }) } @@ -773,84 +386,6 @@ func MustObject(j string) map[string]any { return out } -func TestOptionalFieldPathNotFound(t *testing.T) { - errBoom := errors.New("boom") - errNotFound := func() error { - p := &fieldpath.Paved{} - _, err := p.GetValue("boom") - return err - } - required := v1beta1.FromFieldPathPolicyRequired - optional := v1beta1.FromFieldPathPolicyOptional - type args struct { - err error - p *v1beta1.PatchPolicy - } - - cases := map[string]struct { - reason string - args - want bool - }{ - "NotAnError": { - reason: "Should perform patch if no error finding field.", - args: args{}, - want: false, - }, - "NotFieldNotFoundError": { - reason: "Should return error if something other than field not found.", - args: args{ - err: errBoom, - }, - want: false, - }, - "DefaultOptionalNoPolicy": { - reason: "Should return no-op if field not found and no patch policy specified.", - args: args{ - err: errNotFound(), - }, - want: true, - }, - "DefaultOptionalNoPathPolicy": { - reason: "Should return no-op if field not found and empty patch policy specified.", - args: args{ - p: &v1beta1.PatchPolicy{}, - err: errNotFound(), - }, - want: true, - }, - "OptionalNotFound": { - reason: "Should return no-op if field not found and optional patch policy explicitly specified.", - args: args{ - p: &v1beta1.PatchPolicy{ - FromFieldPath: &optional, - }, - err: errNotFound(), - }, - want: true, - }, - "RequiredNotFound": { - reason: "Should return error if field not found and required patch policy explicitly specified.", - args: args{ - p: &v1beta1.PatchPolicy{ - FromFieldPath: &required, - }, - err: errNotFound(), - }, - want: false, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - got := IsOptionalFieldPathNotFound(tc.args.err, tc.args.p) - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("IsOptionalFieldPathNotFound(...): -want, +got:\n%s", diff) - } - }) - } -} - func TestComposedTemplates(t *testing.T) { asJSON := func(val interface{}) extv1.JSON { raw, err := json.Marshal(val) diff --git a/render.go b/render.go index e3e5aec..ec9f640 100644 --- a/render.go +++ b/render.go @@ -1,30 +1,18 @@ package main import ( - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/json" "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource" - - "github.com/crossplane/function-sdk-go/resource/composed" - "github.com/crossplane/function-sdk-go/resource/composite" - - "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1" ) // Error strings const ( errUnmarshalJSON = "cannot unmarshal JSON data" - errFmtKindChanged = "cannot change the kind of a composed resource from %s to %s (possible composed resource template mismatch)" - errFmtNamePrefixLabel = "cannot find top-level composite resource name label %q in composite resource metadata" - - // TODO(negz): Include more detail such as field paths if they exist. - // Perhaps require each patch type to have a String() method to help - // identify it. - errFmtPatch = "cannot apply the %q patch at index %d" + errFmtKindChanged = "cannot change the kind of a composed resource from %s to %s (possible composed resource template mismatch)" ) // RenderFromJSON renders the supplied resource from JSON bytes. @@ -59,93 +47,3 @@ func RenderFromJSON(o resource.Object, data []byte) error { return nil } - -// RenderEnvironmentPatches renders the supplied environment by applying all -// patches that are to the environment, from the supplied XR. -func RenderEnvironmentPatches(env *unstructured.Unstructured, oxr, dxr *composite.Unstructured, ps []v1beta1.EnvironmentPatch) error { - for i, p := range ps { - p := p - switch p.GetType() { - case v1beta1.PatchTypeFromCompositeFieldPath, v1beta1.PatchTypeCombineFromComposite: - if err := ApplyToObjects(&p, oxr, env); err != nil { - return errors.Wrapf(err, errFmtPatch, p.GetType(), i) - } - case v1beta1.PatchTypeToCompositeFieldPath, v1beta1.PatchTypeCombineToComposite: - if err := ApplyToObjects(&p, dxr, env); err != nil { - return errors.Wrapf(err, errFmtPatch, p.GetType(), i) - } - case v1beta1.PatchTypePatchSet, v1beta1.PatchTypeFromEnvironmentFieldPath, v1beta1.PatchTypeCombineFromEnvironment, v1beta1.PatchTypeToEnvironmentFieldPath, v1beta1.PatchTypeCombineToEnvironment: - // nothing to do - } - } - return nil -} - -// RenderComposedPatches renders the supplied composed resource by applying all -// patches that are to or from the supplied composite resource and environment -// in the order they were defined. Properly selecting the right source or -// destination between observed and desired resources. -func RenderComposedPatches( //nolint:gocyclo // just a switch - ocd *composed.Unstructured, - dcd *composed.Unstructured, - oxr *composite.Unstructured, - dxr *composite.Unstructured, - env *unstructured.Unstructured, - ps []v1beta1.ComposedPatch, -) (errs []error, store bool) { - for i, p := range ps { - p := p - switch t := p.GetType(); t { - case v1beta1.PatchTypeToCompositeFieldPath, v1beta1.PatchTypeCombineToComposite: - // TODO(negz): Should failures to patch the XR be terminal? It could - // indicate a required patch failed. A required patch means roughly - // "this patch has to succeed before you mutate the resource". This - // is useful to make sure we never create a composed resource in the - // wrong state. It's less clear how useful it is for the XR, given - // we'll only ever be updating it, not creating it. - - // We want to patch the XR from observed composed resources, not - // from desired state. This is because folks will typically be - // patching from a field that is set once the observed resource is - // applied such as its status. - if ocd == nil { - continue - } - if err := ApplyToObjects(&p, dxr, ocd); err != nil { - errs = append(errs, errors.Wrapf(err, errFmtPatch, t, i)) - } - case v1beta1.PatchTypeToEnvironmentFieldPath, v1beta1.PatchTypeCombineToEnvironment: - // TODO(negz): Same as above, but for the Environment. What does it - // mean for a required patch to the environment to fail? Should it - // be terminal? - - // Run all patches that are from the (observed) composed resource to - // the environment. - if ocd == nil { - continue - } - if err := ApplyToObjects(&p, env, ocd); err != nil { - errs = append(errs, errors.Wrapf(err, errFmtPatch, t, i)) - } - // If either of the below renderings return an error, most likely a - // required FromComposite or FromEnvironment patch failed. A required - // patch means roughly "this patch has to succeed before you mutate the - // resource." This is useful to make sure we never create a composed - // resource in the wrong state. To that end, we don't want to add this - // resource to our accumulated desired state. - case v1beta1.PatchTypeFromCompositeFieldPath, v1beta1.PatchTypeCombineFromComposite: - if err := ApplyToObjects(&p, oxr, dcd); err != nil { - errs = append(errs, errors.Wrapf(err, errFmtPatch, t, i)) - return errs, false - } - case v1beta1.PatchTypeFromEnvironmentFieldPath, v1beta1.PatchTypeCombineFromEnvironment: - if err := ApplyToObjects(&p, env, dcd); err != nil { - errs = append(errs, errors.Wrapf(err, errFmtPatch, t, i)) - return errs, false - } - case v1beta1.PatchTypePatchSet: - // Already resolved - nothing to do. - } - } - return errs, true -} diff --git a/validate.go b/validate.go index 5d87f07..05d0140 100644 --- a/validate.go +++ b/validate.go @@ -189,6 +189,7 @@ func ValidatePatch(p PatchInterface) *field.Error { //nolint: gocyclo // This is if p.GetToFieldPath() == "" { return field.Required(field.NewPath("toFieldPath"), fmt.Sprintf("toFieldPath must be set for patch type %s", p.GetType())) } + return WrapFieldError(ValidateCombine(p.GetCombine()), field.NewPath("combine")) default: // Should never happen return field.Invalid(field.NewPath("type"), p.GetType(), "unknown patch type") @@ -202,6 +203,32 @@ func ValidatePatch(p PatchInterface) *field.Error { //nolint: gocyclo // This is return nil } +// ValidateCombine validates a Combine. +func ValidateCombine(c *v1beta1.Combine) *field.Error { + switch c.Strategy { + case v1beta1.CombineStrategyString: + if c.String == nil { + return field.Required(field.NewPath("string"), fmt.Sprintf("string must be set for combine strategy %s", c.Strategy)) + } + case "": + return field.Required(field.NewPath("strategy"), "a combine strategy must be provided") + default: + return field.Invalid(field.NewPath("strategy"), c.Strategy, "unknown strategy type") + } + + if len(c.Variables) == 0 { + return field.Required(field.NewPath("variables"), "at least one variable must be provided") + } + + for i := range c.Variables { + if c.Variables[i].FromFieldPath == "" { + return field.Required(field.NewPath("variables").Index(i).Child("fromFieldPath"), "fromFieldPath must be set for each combine variable") + } + } + + return nil +} + // ValidateTransform validates a Transform. func ValidateTransform(t v1beta1.Transform) *field.Error { //nolint:gocyclo // This is a long but simple/same-y switch. switch t.Type { diff --git a/validate_test.go b/validate_test.go index dde1333..5b46a07 100644 --- a/validate_test.go +++ b/validate_test.go @@ -380,6 +380,131 @@ func TestValidatePatch(t *testing.T) { } } +func TestValidateCombine(t *testing.T) { + type args struct { + combine v1beta1.Combine + } + type want struct { + err *field.Error + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "MissingStrategy": { + reason: "A combine with no strategy is invalid", + args: args{ + combine: v1beta1.Combine{}, + }, + want: want{ + err: &field.Error{ + Type: field.ErrorTypeRequired, + Field: "strategy", + }, + }, + }, + "InvalidStrategy": { + reason: "A combine with an unknown strategy is invalid", + args: args{ + combine: v1beta1.Combine{ + Strategy: "Smoosh", + }, + }, + want: want{ + err: &field.Error{ + Type: field.ErrorTypeInvalid, + Field: "strategy", + }, + }, + }, + "ValidStringCombine": { + reason: "A string combine with variables and a format string should be valid", + args: args{ + combine: v1beta1.Combine{ + Strategy: v1beta1.CombineStrategyString, + Variables: []v1beta1.CombineVariable{ + {FromFieldPath: "a"}, + {FromFieldPath: "b"}, + }, + String: &v1beta1.StringCombine{ + Format: "%s-%s", + }, + }, + }, + want: want{ + err: nil, + }, + }, + "MissingVariables": { + reason: "A combine with no variables is invalid", + args: args{ + combine: v1beta1.Combine{ + Strategy: v1beta1.CombineStrategyString, + String: &v1beta1.StringCombine{ + Format: "%s-%s", + }, + }, + }, + want: want{ + err: &field.Error{ + Type: field.ErrorTypeRequired, + Field: "variables", + }, + }, + }, + "VariableMissingFromFieldPath": { + reason: "A variable with no fromFieldPath is invalid", + args: args{ + combine: v1beta1.Combine{ + Strategy: v1beta1.CombineStrategyString, + Variables: []v1beta1.CombineVariable{ + {FromFieldPath: "a"}, + {FromFieldPath: ""}, // Missing. + }, + String: &v1beta1.StringCombine{ + Format: "%s-%s", + }, + }, + }, + want: want{ + err: &field.Error{ + Type: field.ErrorTypeRequired, + Field: "variables[1].fromFieldPath", + }, + }, + }, + "MissingStringConfig": { + reason: "A string combine with no string config is invalid", + args: args{ + combine: v1beta1.Combine{ + Strategy: v1beta1.CombineStrategyString, + Variables: []v1beta1.CombineVariable{ + {FromFieldPath: "a"}, + {FromFieldPath: "b"}, + }, + }, + }, + want: want{ + err: &field.Error{ + Type: field.ErrorTypeRequired, + Field: "string", + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := ValidateCombine(&tc.args.combine) + if diff := cmp.Diff(tc.want.err, err, cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" { + t.Errorf("%s\nValidateCombine(...): -want, +got:\n%s", tc.reason, diff) + } + }) + } +} + func TestValidateTransform(t *testing.T) { type args struct { transform v1beta1.Transform