diff --git a/README.md b/README.md index 01d7e75..eaa99a1 100644 --- a/README.md +++ b/README.md @@ -539,7 +539,9 @@ spec: By defining one or more special `ExtraResources`, you can ask Crossplane to retrieve additional resources from the local cluster and make them available to your templates. See the [docs](https://github.com/crossplane/crossplane/blob/main/design/design-doc-composition-functions-extra-resources.md) for more information. > With ExtraResources, you can fetch cluster-scoped resources, but not namespaced resources such as claims. -> If you need to get a composite resource via its claim name you can use `matchLabels` with `crossplane.io/claim-name: ` +> If you need to get a composite resource via its claim name you can use `matchLabels` with `crossplane.io/claim-name: `. + +> Namespace scoped resources can be queried with the `matchNamespace` field. Leaving the `matchNamespace` field empty or not defining it will query a cluster scoped resource. ```yaml apiVersion: krm.kcl.dev/v1alpha1 @@ -564,6 +566,20 @@ spec: apiVersion: "example.com/v1beta1", kind: "Bar", matchName: "my-bar" + }, + baz = { + apiVersion: "example.m.com/v1beta1", + kind: "Bar", + matchName: "my-bar" + matchNamespace: "my-baz-ns" + }, + quux = { + apiVersion: "example.m.com/v1beta1", + kind: "Quux", + matchLabels: { + "baz": "quux" + } + matchNamespace: "my-quux-ns" } } } diff --git a/examples/default/extra_resources_namespaced/Makefile b/examples/default/extra_resources_namespaced/Makefile new file mode 100644 index 0000000..908587d --- /dev/null +++ b/examples/default/extra_resources_namespaced/Makefile @@ -0,0 +1,2 @@ +run: + crossplane render --verbose xr.yaml composition.yaml functions.yaml -r --extra-resources extra_resources_namespaced.yaml diff --git a/examples/default/extra_resources_namespaced/README.md b/examples/default/extra_resources_namespaced/README.md new file mode 100644 index 0000000..23eedbf --- /dev/null +++ b/examples/default/extra_resources_namespaced/README.md @@ -0,0 +1,61 @@ +# Example Manifests + +You can run your function locally and test it using `crossplane render` +with these example manifests. + +```shell +# Run the function locally +$ go run . --insecure --debug +``` + +```shell +# Then, in another terminal, call it with these example manifests +$ crossplane render --verbose xr.yaml composition.yaml functions.yaml -r --extra-resources extra_resources_namespaced.yaml +--- +--- +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +status: + conditions: + - lastTransitionTime: "2024-01-01T00:00:00Z" + message: 'Unready resources: another-awesome-dev-bucket, my-awesome-dev-bucket' + reason: Creating + status: "False" + type: Ready +--- +apiVersion: example/v1alpha1 +kind: Foo +metadata: + annotations: + crossplane.io/composition-resource-name: another-awesome-dev-bucket + generateName: example- + labels: + crossplane.io/composite: example + name: another-awesome-dev-bucket + ownerReferences: + - apiVersion: example.crossplane.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example + uid: "" +--- +apiVersion: example/v1alpha1 +kind: Foo +metadata: + annotations: + crossplane.io/composition-resource-name: my-awesome-dev-bucket + generateName: example- + labels: + crossplane.io/composite: example + name: my-awesome-dev-bucket + ownerReferences: + - apiVersion: example.crossplane.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example + uid: "" +``` diff --git a/examples/default/extra_resources_namespaced/composition.yaml b/examples/default/extra_resources_namespaced/composition.yaml new file mode 100644 index 0000000..2e8ab83 --- /dev/null +++ b/examples/default/extra_resources_namespaced/composition.yaml @@ -0,0 +1,55 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: function-template-go +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1beta1 + kind: XR + mode: Pipeline + pipeline: + - step: normal + functionRef: + name: kcl-function + input: + apiVersion: krm.kcl.dev/v1alpha1 + kind: KCLInput + metadata: + annotations: + "krm.kcl.dev/default_ready": "True" + name: basic + spec: + source: | + oxr = option("params").oxr + er = option("params")?.extraResources + + foo = [{ + apiVersion: "example/v1alpha1" + kind: "Foo" + metadata = { + name: k.Resource.metadata.name + } + } for k in er?.bucket] if er?.bucket else [] + + dxr = { + **oxr + } + + details = { + apiVersion: "meta.krm.kcl.dev/v1alpha1" + kind: "ExtraResources" + requirements = { + bucket = { + apiVersion: "s3.aws.m.upbound.io/v1beta1", + kind: "Bucket", + matchLabels: { + "foo": "bar" + } + matchNamespace: "awesome-namespace" + } + } + } + items = [ + details + dxr + ] + foo diff --git a/examples/default/extra_resources_namespaced/extra_resources_namespaced.yaml b/examples/default/extra_resources_namespaced/extra_resources_namespaced.yaml new file mode 100644 index 0000000..aaaab1f --- /dev/null +++ b/examples/default/extra_resources_namespaced/extra_resources_namespaced.yaml @@ -0,0 +1,31 @@ +apiVersion: s3.aws.m.upbound.io/v1beta1 +kind: Bucket +metadata: + annotations: + crossplane.io/external-name: my-awesome-dev-bucket + labels: + foo: bar + name: my-awesome-dev-bucket + namespace: awesome-namespace +spec: + forProvider: + region: us-west-1 +status: + atProvider: + id: random-bucket-id +--- +apiVersion: s3.aws.m.upbound.io/v1beta1 +kind: Bucket +metadata: + annotations: + crossplane.io/external-name: my-awesome-dev-bucket + labels: + foo: bar + name: another-awesome-dev-bucket + namespace: awesome-namespace +spec: + forProvider: + region: us-west-1 +status: + atProvider: + id: random-bucket-id diff --git a/examples/default/extra_resources_namespaced/functions.yaml b/examples/default/extra_resources_namespaced/functions.yaml new file mode 100644 index 0000000..d5679cb --- /dev/null +++ b/examples/default/extra_resources_namespaced/functions.yaml @@ -0,0 +1,9 @@ +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: kcl-function + annotations: + # This tells crossplane render to connect to the function locally. + render.crossplane.io/runtime: Development +spec: + package: xpkg.upbound.io/crossplane-contrib/function-kcl:latest diff --git a/examples/default/extra_resources_namespaced/xr.yaml b/examples/default/extra_resources_namespaced/xr.yaml new file mode 100644 index 0000000..497db4f --- /dev/null +++ b/examples/default/extra_resources_namespaced/xr.yaml @@ -0,0 +1,6 @@ +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +spec: + count: 1 \ No newline at end of file diff --git a/fn_test.go b/fn_test.go index 38cafa0..961373e 100644 --- a/fn_test.go +++ b/fn_test.go @@ -214,8 +214,8 @@ func TestRunFunctionSimple(t *testing.T) { "spec": { "target": "Default", "source": "items = [\n{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ExtraResources\"\n requirements = {\n \"cool-extra-resource\" = {\n apiVersion: \"example.org/v1\"\n kind: \"CoolExtraResource\"\n matchName: \"cool-extra-resource\"\n }\n }\n},\n{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ExtraResources\"\n requirements = {\n \"another-cool-extra-resource\" = {\n apiVersion: \"example.org/v1\"\n kind: \"CoolExtraResource\"\n matchLabels = {\n key: \"value\"\n }\n }\n \"yet-another-cool-extra-resource\" = {\n apiVersion: \"example.org/v1\"\n kind: \"CoolExtraResource\"\n matchName: \"foo\"\n }\n }\n},\n{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ExtraResources\"\n requirements = {\n \"all-cool-resources\" = {\n apiVersion: \"example.org/v1\"\n kind: \"CoolExtraResource\"\n matchLabels = {}\n }\n }\n}\n]\n" - } - }`), + } + }`), Observed: &fnv1.State{ Composite: &fnv1.Resource{ Resource: resource.MustStructJSON(xr), @@ -286,6 +286,46 @@ func TestRunFunctionSimple(t *testing.T) { }, }, }, + "ExtraResourcesNamespacedMatchName": { + reason: "The Function should pass through a single extra namespaced resource with matchName and matchNamespace", + args: args{ + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "extra-resources-namespace-matchname"}, + Input: resource.MustStructJSON(`{ + "apiVersion": "krm.kcl.dev/v1alpha1", + "kind": "KCLInput", + "metadata": {"name": "basic"}, + "spec": { + "target": "Default", + "source": "items = [{ apiVersion: \"meta.krm.kcl.dev/v1alpha1\", kind: \"ExtraResources\", requirements = { \"cool-ns-resource-matchname\" = { apiVersion: \"example.m.org/v1\", kind: \"CoolExtraResource\", matchNamespace: \"cool-ns-scoped-ns\", matchName: \"cool-ns-scoped-resource\" } } }]" + } + }`), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{Resource: resource.MustStructJSON(xr)}, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{Resource: resource.MustStructJSON(xr)}, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: "extra-resources-namespace-matchname", Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1.Result{}, + Requirements: &fnv1.Requirements{ + ExtraResources: map[string]*fnv1.ResourceSelector{ + "cool-ns-resource-matchname": { + ApiVersion: "example.m.org/v1", + Kind: "CoolExtraResource", + Namespace: ptr.To[string]("cool-ns-scoped-ns"), + Match: &fnv1.ResourceSelector_MatchName{MatchName: "cool-ns-scoped-resource"}, + }, + }, + }, + Desired: &fnv1.State{Composite: &fnv1.Resource{Resource: resource.MustStructJSON(xr)}}, + }, + }, + }, "ExtraResourcesIn": { reason: "The Function should return the extra resources from the request.", args: args{ diff --git a/go.mod b/go.mod index 2b34148..4756244 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( dario.cat/mergo v1.0.1 github.com/alecthomas/kong v1.12.1 github.com/crossplane/crossplane-runtime v1.20.0 - github.com/crossplane/function-sdk-go v0.4.0 + github.com/crossplane/function-sdk-go v0.5.0-rc.0.0.20250805171053-2910b68d255d github.com/go-logr/logr v1.4.3 github.com/google/go-cmp v0.7.0 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index d7dffed..e7c8c94 100644 --- a/go.sum +++ b/go.sum @@ -750,8 +750,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/crossplane/crossplane-runtime v1.20.0 h1:I54uipRIecqZyms+vz1J/l62yjVQ7HV5w+Nh3RMrUtc= github.com/crossplane/crossplane-runtime v1.20.0/go.mod h1:lfV1VJenDc9PNVLxDC80YjPoTm+JdSZ13xlS2h37Dvg= -github.com/crossplane/function-sdk-go v0.4.0 h1:1jd+UIaZlVNQCUO4hLAgUqWBRnUKw2ObF9ZuMw5CpKk= -github.com/crossplane/function-sdk-go v0.4.0/go.mod h1:jLnzUG8pt8tn/U6/uvtNStAhDjhIq4wCR31yECT54NM= +github.com/crossplane/function-sdk-go v0.5.0-rc.0.0.20250805171053-2910b68d255d h1:bzt8qEg9I2GrLc216IuuTn4x+GECxc+DoGlDZ4PMuJY= +github.com/crossplane/function-sdk-go v0.5.0-rc.0.0.20250805171053-2910b68d255d/go.mod h1:fEwSBgMH6+kicaBeOWz6PZRwhjLg4tu9QEDeP/9O2yE= github.com/crossplane/upjet v1.4.1-0.20240911184956-3afbb7796d46 h1:2IH1YPTBrNmBj0Z1OCjEBTrQCuRaLutZbWLaswFeCFQ= github.com/crossplane/upjet v1.4.1-0.20240911184956-3afbb7796d46/go.mod h1:wkdZf/Cvhr6PI30VdHIOjg4dX39Z5uijqnLWFk5PbGM= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= diff --git a/package/input/template.fn.crossplane.io_kclinputs.yaml b/package/input/template.fn.crossplane.io_kclinputs.yaml index 9fce3c6..a15ac1d 100644 --- a/package/input/template.fn.crossplane.io_kclinputs.yaml +++ b/package/input/template.fn.crossplane.io_kclinputs.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.17.0 + controller-gen.kubebuilder.io/version: v0.19.0 name: kclinputs.template.fn.crossplane.io spec: group: template.fn.crossplane.io diff --git a/pkg/resource/extraresources.go b/pkg/resource/extraresources.go index 8192468..dcbe757 100644 --- a/pkg/resource/extraresources.go +++ b/pkg/resource/extraresources.go @@ -21,6 +21,9 @@ type ExtraResourcesRequirement struct { // MatchName defines the name to match the resource, if MatchLabels is // empty. MatchName string `json:"matchName,omitempty"` + // MatchNamespace defines the namespace to match a namespace scoped resource, if set. + // Otherwise the resource is assumed to be cluster scoped. + MatchNamespace string `json:"matchNamespace,omitempty"` } // ToResourceSelector converts the ExtraResourcesRequirement to a fnv1.ResourceSelector. @@ -29,6 +32,9 @@ func (e *ExtraResourcesRequirement) ToResourceSelector() *fnv1.ResourceSelector ApiVersion: e.APIVersion, Kind: e.Kind, } + if e.MatchNamespace != "" { + out.Namespace = &e.MatchNamespace + } if e.MatchName == "" { out.Match = &fnv1.ResourceSelector_MatchLabels{ MatchLabels: &fnv1.MatchLabels{Labels: e.MatchLabels},