├── diagram.png ├── examples ├── kubecon-2024 │ ├── xr.yaml │ ├── provider.yaml │ ├── functions.yaml │ ├── environmentConfigs.yaml │ ├── xrd.yaml │ ├── README.md │ └── composition.yaml ├── reference_name │ ├── xr.yaml │ ├── provider.yaml │ ├── environmentConfigs.yaml │ ├── xrd.yaml │ ├── functions.yaml │ ├── composition.yaml │ └── README.md ├── match_label_single │ ├── xr.yaml │ ├── provider.yaml │ ├── environmentConfigs.yaml │ ├── xrd.yaml │ ├── functions.yaml │ ├── README.md │ └── composition.yaml └── match_label_multiple │ ├── xr.yaml │ ├── provider.yaml │ ├── environmentConfigs.yaml │ ├── functions.yaml │ ├── xrd.yaml │ ├── README.md │ └── composition.yaml ├── .gitignore ├── OWNERS.md ├── input ├── generate.go └── v1beta1 │ ├── input.go │ ├── zz_generated.deepcopy.go │ └── composition_environment.go ├── .github ├── workflows │ ├── tag.yml │ └── ci.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── CODEOWNERS ├── package ├── crossplane.yaml └── input │ └── environmentconfigs.fn.crossplane.io_inputs.yaml ├── main.go ├── renovate.json ├── Dockerfile ├── go.mod ├── .golangci.yml ├── LICENSE ├── fn.go ├── README.md ├── go.sum └── fn_test.go /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossplane-contrib/function-environment-configs/HEAD/diagram.png -------------------------------------------------------------------------------- /examples/kubecon-2024/xr.yaml: -------------------------------------------------------------------------------- 1 | # Replace this with your XR! 2 | apiVersion: example.crossplane.io/v1 3 | kind: XR 4 | metadata: 5 | name: example-xr 6 | spec: {} 7 | -------------------------------------------------------------------------------- /examples/reference_name/xr.yaml: -------------------------------------------------------------------------------- 1 | # Replace this with your XR! 2 | apiVersion: example.crossplane.io/v1 3 | kind: XR 4 | metadata: 5 | name: example-xr 6 | spec: {} 7 | -------------------------------------------------------------------------------- /examples/match_label_single/xr.yaml: -------------------------------------------------------------------------------- 1 | # Replace this with your XR! 2 | apiVersion: example.crossplane.io/v1 3 | kind: XR 4 | metadata: 5 | name: example-xr 6 | spec: {} 7 | -------------------------------------------------------------------------------- /examples/match_label_multiple/xr.yaml: -------------------------------------------------------------------------------- 1 | # Replace this with your XR! 2 | apiVersion: example.crossplane.io/v1 3 | kind: XR 4 | metadata: 5 | name: example-xr 6 | spec: {} 7 | -------------------------------------------------------------------------------- /examples/kubecon-2024/provider.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1 3 | kind: Provider 4 | metadata: 5 | name: provider-nop 6 | spec: 7 | package: xpkg.upbound.io/crossplane-contrib/provider-nop:v0.4.0 8 | ignoreCrossplaneConstraints: true 9 | -------------------------------------------------------------------------------- /examples/reference_name/provider.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1 3 | kind: Provider 4 | metadata: 5 | name: provider-nop 6 | spec: 7 | package: xpkg.upbound.io/crossplane-contrib/provider-nop:v0.4.0 8 | ignoreCrossplaneConstraints: true 9 | -------------------------------------------------------------------------------- /examples/match_label_multiple/provider.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1 3 | kind: Provider 4 | metadata: 5 | name: provider-nop 6 | spec: 7 | package: xpkg.upbound.io/crossplane-contrib/provider-nop:v0.4.0 8 | ignoreCrossplaneConstraints: true 9 | -------------------------------------------------------------------------------- /examples/match_label_single/provider.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1 3 | kind: Provider 4 | metadata: 5 | name: provider-nop 6 | spec: 7 | package: xpkg.upbound.io/crossplane-contrib/provider-nop:v0.4.0 8 | ignoreCrossplaneConstraints: true 9 | -------------------------------------------------------------------------------- /examples/reference_name/environmentConfigs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.crossplane.io/v1beta1 3 | kind: EnvironmentConfig 4 | metadata: 5 | name: example-config 6 | data: 7 | complex: 8 | a: b 9 | c: 10 | d: e 11 | f: "1" 12 | -------------------------------------------------------------------------------- /examples/kubecon-2024/functions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1beta1 3 | kind: Function 4 | metadata: 5 | name: function-environment-configs 6 | spec: 7 | package: xpkg.upbound.io/crossplane-contrib/function-environment-configs:v0.4.0 8 | --- 9 | apiVersion: pkg.crossplane.io/v1beta1 10 | kind: Function 11 | metadata: 12 | name: function-go-templating 13 | spec: 14 | package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.10.0 15 | -------------------------------------------------------------------------------- /examples/kubecon-2024/environmentConfigs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.crossplane.io/v1beta1 3 | kind: EnvironmentConfig 4 | metadata: 5 | name: example-config 6 | data: 7 | complex: 8 | a: b 9 | c: 10 | d: e 11 | f: "1" 12 | --- 13 | apiVersion: apiextensions.crossplane.io/v1beta1 14 | kind: EnvironmentConfig 15 | metadata: 16 | name: example-config-by-label 17 | labels: 18 | org: foo 19 | data: 20 | complex: 21 | c: 22 | d: by-label 23 | g: by-label 24 | -------------------------------------------------------------------------------- /examples/match_label_single/environmentConfigs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.crossplane.io/v1beta1 3 | kind: EnvironmentConfig 4 | metadata: 5 | name: example-config-one 6 | labels: 7 | matchingLabel: "matchingKey" 8 | data: 9 | singleStanza: 10 | key: not-this-value 11 | --- 12 | apiVersion: apiextensions.crossplane.io/v1beta1 13 | kind: EnvironmentConfig 14 | metadata: 15 | name: example-config-two 16 | labels: 17 | matchingLabel: "matchingOtherKey" 18 | data: 19 | singleStanza: 20 | key: here-it-is -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /examples/match_label_multiple/environmentConfigs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.crossplane.io/v1beta1 3 | kind: EnvironmentConfig 4 | metadata: 5 | name: example-config-one 6 | labels: 7 | matchingLabel: "matchingKey" 8 | data: 9 | multiple: 10 | a: b 11 | c: 12 | d: e 13 | f: "1" 14 | --- 15 | apiVersion: apiextensions.crossplane.io/v1beta1 16 | kind: EnvironmentConfig 17 | metadata: 18 | name: example-config-two 19 | labels: 20 | matchingLabel: "matchingKey" 21 | data: 22 | multiple: 23 | g: h 24 | i: 25 | j: k 26 | -------------------------------------------------------------------------------- /OWNERS.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # OWNERS 8 | 9 | This page lists all maintainers for **this** repository. Each repository in the 10 | [Crossplane Contrib organization](https://github.com/crossplane-contrib/) will list their 11 | repository maintainers in their own `OWNERS.md` file. 12 | 13 | ## Maintainers 14 | * Philippe Scorsolini ([phisco](https://github.com/phisco) 15 | 16 | See [CODEOWNERS](./CODEOWNERS) for automatic PR assignment. 17 | -------------------------------------------------------------------------------- /input/generate.go: -------------------------------------------------------------------------------- 1 | //go:build generate 2 | // +build generate 3 | 4 | // NOTE(negz): See the below link for details on what is happening here. 5 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 6 | 7 | // Remove existing and generate new input manifests 8 | //go:generate rm -rf ../package/input/ 9 | //go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen paths=./v1beta1 object crd:crdVersions=v1 output:artifacts:config=../package/input 10 | 11 | package input 12 | 13 | import ( 14 | _ "sigs.k8s.io/controller-tools/cmd/controller-gen" //nolint:typecheck 15 | ) 16 | -------------------------------------------------------------------------------- /examples/kubecon-2024/xrd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.crossplane.io/v1 3 | kind: CompositeResourceDefinition 4 | metadata: 5 | name: xrs.example.crossplane.io 6 | spec: 7 | group: example.crossplane.io 8 | names: 9 | kind: XR 10 | plural: xrs 11 | connectionSecretKeys: 12 | - test 13 | versions: 14 | - name: v1 15 | served: true 16 | referenceable: true 17 | schema: 18 | openAPIV3Schema: 19 | type: object 20 | properties: 21 | status: 22 | type: object 23 | properties: 24 | fromEnv: 25 | type: string -------------------------------------------------------------------------------- /examples/reference_name/xrd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.crossplane.io/v1 3 | kind: CompositeResourceDefinition 4 | metadata: 5 | name: xrs.example.crossplane.io 6 | spec: 7 | group: example.crossplane.io 8 | names: 9 | kind: XR 10 | plural: xrs 11 | connectionSecretKeys: 12 | - test 13 | versions: 14 | - name: v1 15 | served: true 16 | referenceable: true 17 | schema: 18 | openAPIV3Schema: 19 | type: object 20 | properties: 21 | status: 22 | type: object 23 | properties: 24 | fromEnv: 25 | type: string -------------------------------------------------------------------------------- /examples/match_label_single/xrd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.crossplane.io/v1 3 | kind: CompositeResourceDefinition 4 | metadata: 5 | name: xrs.example.crossplane.io 6 | spec: 7 | group: example.crossplane.io 8 | names: 9 | kind: XR 10 | plural: xrs 11 | connectionSecretKeys: 12 | - test 13 | versions: 14 | - name: v1 15 | served: true 16 | referenceable: true 17 | schema: 18 | openAPIV3Schema: 19 | type: object 20 | properties: 21 | status: 22 | type: object 23 | properties: 24 | fromEnvOne: 25 | type: string -------------------------------------------------------------------------------- /examples/reference_name/functions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1beta1 3 | kind: Function 4 | metadata: 5 | name: function-environment-configs 6 | annotations: 7 | # This tells crossplane beta render to connect to the function locally. 8 | render.crossplane.io/runtime: Development 9 | spec: 10 | # This is ignored when using the Development runtime. 11 | package: xpkg.upbound.io/crossplane-contrib/function-environment-configs:v0.4.0 12 | --- 13 | apiVersion: pkg.crossplane.io/v1beta1 14 | kind: Function 15 | metadata: 16 | name: function-go-templating 17 | spec: 18 | package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.10.0 19 | -------------------------------------------------------------------------------- /examples/match_label_multiple/functions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1beta1 3 | kind: Function 4 | metadata: 5 | name: function-environment-configs 6 | annotations: 7 | # This tells crossplane beta render to connect to the function locally. 8 | render.crossplane.io/runtime: Development 9 | spec: 10 | # This is ignored when using the Development runtime. 11 | package: xpkg.upbound.io/crossplane-contrib/function-environment-configs:v0.4.0 12 | --- 13 | apiVersion: pkg.crossplane.io/v1beta1 14 | kind: Function 15 | metadata: 16 | name: function-go-templating 17 | spec: 18 | package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.10.0 19 | -------------------------------------------------------------------------------- /examples/match_label_single/functions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1beta1 3 | kind: Function 4 | metadata: 5 | name: function-environment-configs 6 | annotations: 7 | # This tells crossplane beta render to connect to the function locally. 8 | render.crossplane.io/runtime: Development 9 | spec: 10 | # This is ignored when using the Development runtime. 11 | package: xpkg.upbound.io/crossplane-contrib/function-environment-configs:v0.4.0 12 | --- 13 | apiVersion: pkg.crossplane.io/v1beta1 14 | kind: Function 15 | metadata: 16 | name: function-go-templating 17 | spec: 18 | package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.10.0 19 | -------------------------------------------------------------------------------- /examples/match_label_multiple/xrd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.crossplane.io/v1 3 | kind: CompositeResourceDefinition 4 | metadata: 5 | name: xrs.example.crossplane.io 6 | spec: 7 | group: example.crossplane.io 8 | names: 9 | kind: XR 10 | plural: xrs 11 | connectionSecretKeys: 12 | - test 13 | versions: 14 | - name: v1 15 | served: true 16 | referenceable: true 17 | schema: 18 | openAPIV3Schema: 19 | type: object 20 | properties: 21 | status: 22 | type: object 23 | properties: 24 | fromEnvOne: 25 | type: string 26 | fromEnvTwo: 27 | type: string -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version (e.g. v0.1.0)' 8 | required: true 9 | message: 10 | description: 'Tag message' 11 | required: true 12 | 13 | jobs: 14 | create-tag: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | 21 | - name: Create Tag 22 | uses: negz/create-tag@39bae1e0932567a58c20dea5a1a0d18358503320 # v1 23 | with: 24 | version: ${{ github.event.inputs.version }} 25 | message: ${{ github.event.inputs.message }} 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Help us make this Function more useful 4 | labels: enhancement 5 | --- 6 | 13 | 14 | ### What problem are you facing? 15 | 20 | 21 | ### How could this Function help solve your problem? 22 | 25 | -------------------------------------------------------------------------------- /input/v1beta1/input.go: -------------------------------------------------------------------------------- 1 | // Package v1beta1 contains the input type for this Function 2 | // +kubebuilder:object:generate=true 3 | // +groupName=environmentconfigs.fn.crossplane.io 4 | // +versionName=v1beta1 5 | package v1beta1 6 | 7 | import ( 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // This isn't a custom resource, in the sense that we never install its CRD. 12 | // It is a KRM-like object, so we generate a CRD to describe its schema. 13 | 14 | // TODO: Add your input type here! It doesn't need to be called 'Input', you can 15 | // rename it to anything you like. 16 | 17 | // Input can be used to provide input to this Function. 18 | // +kubebuilder:object:root=true 19 | // +kubebuilder:storageversion 20 | // +kubebuilder:resource:categories=crossplane 21 | type Input struct { 22 | metav1.TypeMeta `json:",inline"` 23 | metav1.ObjectMeta `json:"metadata,omitempty"` 24 | 25 | Spec InputSpec `json:"spec,omitempty"` 26 | } 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Description of your changes 10 | 11 | 21 | 22 | Fixes # 23 | 24 | I have: 25 | 26 | - [ ] Read and followed Crossplane's [contribution process]. 27 | - [ ] Added or updated unit tests for my change. 28 | 29 | [contribution process]: https://git.io/fj2m9 30 | [docs]: https://docs.crossplane.io/contribute/contribute 31 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | 2 | # SPDX-FileCopyrightText: 2025 The Crossplane Authors 3 | # 4 | # SPDX-License-Identifier: CC0-1.0 5 | 6 | # This file controls automatic PR reviewer assignment. See the following docs: 7 | # 8 | # * https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 9 | # * https://docs.github.com/en/organizations/organizing-members-into-teams/managing-code-review-settings-for-your-team 10 | # 11 | # The goal of this file is for most PRs to automatically and fairly have one 12 | # maintainer set as PR reviewers. All maintainers have permission to approve 13 | # and merge PRs. All PRs must be approved by at least one maintainer before being merged. 14 | # 15 | # Where possible, prefer explicitly specifying a maintainer who is a subject 16 | # matter expert for a particular part of the codebase rather than using fallback 17 | # owners. Fallback owners are listed at the bottom of this file. 18 | # 19 | # See also OWNERS.md for governance details 20 | 21 | # Fallback owners 22 | * @phisco 23 | -------------------------------------------------------------------------------- /examples/reference_name/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: function-environment-configs 5 | spec: 6 | compositeTypeRef: 7 | apiVersion: example.crossplane.io/v1 8 | kind: XR 9 | mode: Pipeline 10 | pipeline: 11 | - step: environmentConfigs 12 | functionRef: 13 | name: function-environment-configs 14 | input: 15 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 16 | kind: Input 17 | spec: 18 | environmentConfigs: 19 | - type: Reference 20 | ref: 21 | name: example-config 22 | - step: go-templating 23 | functionRef: 24 | name: function-go-templating 25 | input: 26 | apiVersion: gotemplating.fn.crossplane.io/v1beta1 27 | kind: GoTemplate 28 | source: Inline 29 | inline: 30 | template: | 31 | --- 32 | apiVersion: example.crossplane.io/v1 33 | kind: XR 34 | status: 35 | fromEnv: {{ index .context "apiextensions.crossplane.io/environment" "complex" "c" "d" }} -------------------------------------------------------------------------------- /package/crossplane.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: meta.pkg.crossplane.io/v1beta1 3 | kind: Function 4 | metadata: 5 | name: function-environment-configs 6 | annotations: 7 | meta.crossplane.io/maintainer: Crossplane Maintainers 8 | meta.crossplane.io/source: github.com/crossplane-contrib/function-environment-configs 9 | meta.crossplane.io/license: Apache-2.0 10 | meta.crossplane.io/description: | 11 | A composition function that can retrieve and merge EnvironmentConfigs 12 | into the Context. 13 | meta.crossplane.io/readme: | 14 | This composition function retrives EnvironmentConfigs and merge them in 15 | the specified order into the Context at a well-known key, 16 | apiextensions.crossplane.io/environment, a.k.a. the in-memory environment 17 | as expected by function-patch-and-transform and potentially other 18 | functions. Reimplements almost fully the same exact functionality 19 | available in core Crossplane via Compositions' 20 | spec.environment.environmentConfigs. 21 | spec: 22 | crossplane: 23 | version: ">=v1.18.0-0" 24 | -------------------------------------------------------------------------------- /examples/match_label_single/README.md: -------------------------------------------------------------------------------- 1 | # Example manifests 2 | 3 | You can run your function locally and test it using `crossplane beta render` 4 | with these example manifests. 5 | 6 | ```shell 7 | # Run the function locally 8 | $ go run . --insecure --debug 9 | ``` 10 | 11 | ```shell 12 | # Then, in another terminal, call it with these example manifests 13 | $ crossplane render \ 14 | --extra-resources match_label_single/environmentConfigs.yaml \ 15 | --include-context \ 16 | match_label_single/xr.yaml match_label_single/composition.yaml match_label_single/functions.yaml 17 | --- 18 | apiVersion: example.crossplane.io/v1 19 | kind: XR 20 | metadata: 21 | name: example-xr 22 | status: 23 | conditions: 24 | - lastTransitionTime: "2024-01-01T00:00:00Z" 25 | reason: Available 26 | status: "True" 27 | type: Ready 28 | fromEnvOne: here-it-is 29 | --- 30 | apiVersion: render.crossplane.io/v1beta1 31 | fields: 32 | apiextensions.crossplane.io/environment: 33 | apiVersion: internal.crossplane.io/v1alpha1 34 | kind: Environment 35 | singleStanza: 36 | key: here-it-is 37 | kind: Context 38 | ``` 39 | -------------------------------------------------------------------------------- /examples/reference_name/README.md: -------------------------------------------------------------------------------- 1 | # Example manifests 2 | 3 | You can run your function locally and test it using `crossplane beta render` 4 | with these example manifests. 5 | 6 | ```shell 7 | # Run the function locally 8 | $ go run . --insecure --debug 9 | ``` 10 | 11 | ```shell 12 | # Then, in another terminal, call it with these example manifests 13 | $ crossplane render \ 14 | --extra-resources reference_name/environmentConfigs.yaml \ 15 | --include-context \ 16 | reference_name/xr.yaml reference_name/composition.yaml reference_name/functions.yaml 17 | --- 18 | apiVersion: example.crossplane.io/v1 19 | kind: XR 20 | metadata: 21 | name: example-xr 22 | status: 23 | conditions: 24 | - lastTransitionTime: "2024-01-01T00:00:00Z" 25 | reason: Available 26 | status: "True" 27 | type: Ready 28 | fromEnv: e 29 | --- 30 | apiVersion: render.crossplane.io/v1beta1 31 | fields: 32 | apiextensions.crossplane.io/environment: 33 | apiVersion: internal.crossplane.io/v1alpha1 34 | complex: 35 | a: b 36 | c: 37 | d: e 38 | f: "1" 39 | kind: Environment 40 | kind: Context 41 | ``` 42 | -------------------------------------------------------------------------------- /examples/match_label_multiple/README.md: -------------------------------------------------------------------------------- 1 | # Example manifests 2 | 3 | You can run your function locally and test it using `crossplane beta render` 4 | with these example manifests. 5 | 6 | ```shell 7 | # Run the function locally 8 | $ go run . --insecure --debug 9 | ``` 10 | 11 | ```shell 12 | # Then, in another terminal, call it with these example manifests 13 | $ crossplane render \ 14 | --extra-resources match_label_multiple/environmentConfigs.yaml \ 15 | --include-context \ 16 | match_label_multiple/xr.yaml match_label_multiple/composition.yaml match_label_multiple/functions.yaml 17 | --- 18 | apiVersion: example.crossplane.io/v1 19 | kind: XR 20 | metadata: 21 | name: example-xr 22 | status: 23 | conditions: 24 | - lastTransitionTime: "2024-01-01T00:00:00Z" 25 | reason: Available 26 | status: "True" 27 | type: Ready 28 | fromEnvOne: e 29 | fromEnvTwo: k 30 | --- 31 | apiVersion: render.crossplane.io/v1beta1 32 | fields: 33 | apiextensions.crossplane.io/environment: 34 | apiVersion: internal.crossplane.io/v1alpha1 35 | kind: Environment 36 | multiple: 37 | a: b 38 | c: 39 | d: e 40 | f: "1" 41 | g: h 42 | i: 43 | j: k 44 | kind: Context 45 | ``` 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Help us diagnose and fix bugs in this Function 4 | labels: bug 5 | --- 6 | 13 | 14 | ### What happened? 15 | 19 | 20 | 21 | ### How can we reproduce it? 22 | 27 | 28 | ### What environment did it happen in? 29 | Function version: 30 | 31 | 41 | -------------------------------------------------------------------------------- /examples/match_label_single/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: function-environment-configs 5 | spec: 6 | compositeTypeRef: 7 | apiVersion: example.crossplane.io/v1 8 | kind: XR 9 | mode: Pipeline 10 | pipeline: 11 | - step: environmentConfigs 12 | functionRef: 13 | name: function-environment-configs 14 | input: 15 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 16 | kind: Input 17 | spec: 18 | environmentConfigs: 19 | - type: Selector 20 | selector: 21 | mode: Multiple 22 | matchLabels: 23 | - type: Value 24 | key: matchingLabel 25 | value: matchingOtherKey 26 | - step: go-templating 27 | functionRef: 28 | name: function-go-templating 29 | input: 30 | apiVersion: gotemplating.fn.crossplane.io/v1beta1 31 | kind: GoTemplate 32 | source: Inline 33 | inline: 34 | template: | 35 | --- 36 | apiVersion: example.crossplane.io/v1 37 | kind: XR 38 | status: 39 | fromEnvOne: {{ index .context "apiextensions.crossplane.io/environment" "singleStanza" "key" }} -------------------------------------------------------------------------------- /examples/kubecon-2024/README.md: -------------------------------------------------------------------------------- 1 | # Example manifests 2 | 3 | Before, one had to define references at `spec.environment.environmentConfigs`, 4 | see [./old/composition.yaml]() and needed to have Crossplane actually deployed 5 | to be able to validate that part of the logic. 6 | 7 | The same can now be achieved using `function-environment-config`, which allows 8 | to leverage the power of `crossplane beta render`: 9 | 10 | ```shell 11 | $ crossplane beta render \ 12 | --extra-resources environmentConfigs.yaml \ 13 | --include-context \ 14 | xr.yaml composition.yaml functions.yaml 15 | ``` 16 | 17 | Which will output both the `Context` containing the `environment` and the `XR` 18 | itself: 19 | 20 | ```yaml 21 | --- 22 | apiVersion: example.crossplane.io/v1 23 | kind: XR 24 | metadata: 25 | name: example-xr 26 | status: 27 | conditions: 28 | - lastTransitionTime: "2024-01-01T00:00:00Z" 29 | reason: Available 30 | status: "True" 31 | type: Ready 32 | fromEnv: by-label 33 | --- 34 | apiVersion: render.crossplane.io/v1beta1 35 | kind: Context 36 | fields: 37 | apiextensions.crossplane.io/environment: 38 | apiVersion: internal.crossplane.io/v1alpha1 39 | kind: Environment 40 | complex: 41 | a: b 42 | c: 43 | d: by-label 44 | f: "1" 45 | g: by-label 46 | ``` 47 | -------------------------------------------------------------------------------- /examples/kubecon-2024/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: function-environment-configs 5 | spec: 6 | compositeTypeRef: 7 | apiVersion: example.crossplane.io/v1 8 | kind: XR 9 | mode: Pipeline 10 | pipeline: 11 | - step: environmentConfigs 12 | functionRef: 13 | name: function-environment-configs 14 | input: 15 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 16 | kind: Input 17 | spec: 18 | environmentConfigs: 19 | - type: Reference 20 | ref: 21 | name: example-config 22 | - type: Selector 23 | selector: 24 | mode: Single 25 | matchLabels: 26 | - type: Value 27 | key: org 28 | value: foo 29 | - step: go-templating 30 | functionRef: 31 | name: function-go-templating 32 | input: 33 | apiVersion: gotemplating.fn.crossplane.io/v1beta1 34 | kind: GoTemplate 35 | source: Inline 36 | inline: 37 | template: | 38 | --- 39 | apiVersion: example.crossplane.io/v1 40 | kind: XR 41 | status: 42 | fromEnv: {{ index .context "apiextensions.crossplane.io/environment" "complex" "c" "d" }} 43 | -------------------------------------------------------------------------------- /examples/match_label_multiple/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: function-environment-configs 5 | spec: 6 | compositeTypeRef: 7 | apiVersion: example.crossplane.io/v1 8 | kind: XR 9 | mode: Pipeline 10 | pipeline: 11 | - step: environmentConfigs 12 | functionRef: 13 | name: function-environment-configs 14 | input: 15 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 16 | kind: Input 17 | spec: 18 | environmentConfigs: 19 | - type: Selector 20 | selector: 21 | mode: Multiple 22 | matchLabels: 23 | - type: Value 24 | key: matchingLabel 25 | value: matchingKey 26 | - step: go-templating 27 | functionRef: 28 | name: function-go-templating 29 | input: 30 | apiVersion: gotemplating.fn.crossplane.io/v1beta1 31 | kind: GoTemplate 32 | source: Inline 33 | inline: 34 | template: | 35 | --- 36 | apiVersion: example.crossplane.io/v1 37 | kind: XR 38 | status: 39 | fromEnvOne: {{ index .context "apiextensions.crossplane.io/environment" "multiple" "c" "d" }} 40 | fromEnvTwo: {{ index .context "apiextensions.crossplane.io/environment" "multiple" "i" "j" }} -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main implements a Composition Function. 2 | package main 3 | 4 | import ( 5 | "github.com/alecthomas/kong" 6 | 7 | "github.com/crossplane/function-sdk-go" 8 | ) 9 | 10 | // CLI of this Function. 11 | type CLI struct { 12 | Debug bool `short:"d" help:"Emit debug logs in addition to info logs."` 13 | 14 | Network string `help:"Network on which to listen for gRPC connections." default:"tcp"` 15 | Address string `help:"Address at which to listen for gRPC connections." default:":9443"` 16 | TLSCertsDir string `help:"Directory containing server certs (tls.key, tls.crt) and the CA used to verify client certificates (ca.crt)" env:"TLS_SERVER_CERTS_DIR"` 17 | Insecure bool `help:"Run without mTLS credentials. If you supply this flag --tls-server-certs-dir will be ignored."` 18 | MaxRecvMessageSize int `help:"Maximum size of received messages in MB." default:"4"` 19 | } 20 | 21 | // Run this Function. 22 | func (c *CLI) Run() error { 23 | log, err := function.NewLogger(c.Debug) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return function.Serve(&Function{log: log}, 29 | function.Listen(c.Network, c.Address), 30 | function.MTLSCertificates(c.TLSCertsDir), 31 | function.Insecure(c.Insecure), 32 | function.MaxRecvMessageSize(c.MaxRecvMessageSize*1024*1024)) 33 | } 34 | 35 | func main() { 36 | ctx := kong.Parse(&CLI{}, kong.Description("A Crossplane Composition Function.")) 37 | ctx.FatalIfErrorf(ctx.Run()) 38 | } 39 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":semanticCommits" 6 | ], 7 | "crossplane": { 8 | "fileMatch": ["(^|/)example/.*\\.ya?ml$"] 9 | }, 10 | "rebaseWhen": "conflicted", 11 | "packageRules": [ 12 | { 13 | "matchManagers": ["crossplane"], 14 | "matchFileNames": ["example/**"], 15 | "groupName": "examples" 16 | }, 17 | { 18 | "description": "Group all go version updates", 19 | "matchDatasources": [ 20 | "golang-version" 21 | ], 22 | "groupName": "golang version" 23 | } 24 | ], 25 | "postUpdateOptions": [ 26 | "gomodTidy" 27 | ], 28 | "customManagers": [ 29 | { 30 | "customType": "regex", 31 | "description": "Bump Go version used in workflows", 32 | "fileMatch": ["^\\.github\\/workflows\\/[^/]+\\.ya?ml$"], 33 | "matchStrings": [ 34 | "GO_VERSION: '(?.*?)'\\n" 35 | ], 36 | "datasourceTemplate": "golang-version", 37 | "depNameTemplate": "golang" 38 | }, { 39 | "customType": "regex", 40 | "description": "Bump golangci-lint version in workflows", 41 | "fileMatch": ["^\\.github\\/workflows\\/[^/]+\\.ya?ml$"], 42 | "matchStrings": [ 43 | "GOLANGCI_VERSION: 'v(?.*?)'\\n", 44 | "GOLANGCILINT_VERSION = (?.*?)\\n" 45 | ], 46 | "datasourceTemplate": "github-tags", 47 | "depNameTemplate": "golangci/golangci-lint", 48 | "extractVersionTemplate": "^v(?.*)$" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # We use the latest Go 1.x version unless asked to use something else. 4 | # The GitHub Actions CI job sets this argument for a consistent Go version. 5 | ARG GO_VERSION=1 6 | 7 | # Setup the base environment. The BUILDPLATFORM is set automatically by Docker. 8 | # The --platform=${BUILDPLATFORM} flag tells Docker to build the function using 9 | # the OS and architecture of the host running the build, not the OS and 10 | # architecture that we're building the function for. 11 | FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS build 12 | 13 | WORKDIR /fn 14 | 15 | # Most functions don't want or need CGo support, so we disable it. 16 | ENV CGO_ENABLED=0 17 | 18 | # We run go mod download in a separate step so that we can cache its results. 19 | # This lets us avoid re-downloading modules if we don't need to. The type=target 20 | # mount tells Docker to mount the current directory read-only in the WORKDIR. 21 | # The type=cache mount tells Docker to cache the Go modules cache across builds. 22 | RUN --mount=target=. --mount=type=cache,target=/go/pkg/mod go mod download 23 | 24 | # The TARGETOS and TARGETARCH args are set by docker. We set GOOS and GOARCH to 25 | # these values to ask Go to compile a binary for these architectures. If 26 | # TARGETOS and TARGETOS are different from BUILDPLATFORM, Go will cross compile 27 | # for us (e.g. compile a linux/amd64 binary on a linux/arm64 build machine). 28 | ARG TARGETOS 29 | ARG TARGETARCH 30 | 31 | # Build the function binary. The type=target mount tells Docker to mount the 32 | # current directory read-only in the WORKDIR. The type=cache mount tells Docker 33 | # to cache the Go modules cache across builds. 34 | RUN --mount=target=. \ 35 | --mount=type=cache,target=/go/pkg/mod \ 36 | --mount=type=cache,target=/root/.cache/go-build \ 37 | GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /function . 38 | 39 | # Produce the Function image. We use a very lightweight 'distroless' image that 40 | # does not include any of the build tools used in previous stages. 41 | FROM gcr.io/distroless/static-debian12:nonroot AS image 42 | WORKDIR / 43 | COPY --from=build /function /function 44 | EXPOSE 9443 45 | USER nonroot:nonroot 46 | ENTRYPOINT ["/function"] 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crossplane-contrib/function-environment-configs 2 | 3 | go 1.23 4 | 5 | toolchain go1.24.4 6 | 7 | require ( 8 | github.com/alecthomas/kong v1.4.0 9 | github.com/crossplane/crossplane-runtime v1.18.0 10 | github.com/crossplane/function-sdk-go v0.4.0 11 | github.com/google/go-cmp v0.6.0 12 | google.golang.org/protobuf v1.34.3-0.20240816073751-94ecbc261689 13 | k8s.io/apiextensions-apiserver v0.31.0 14 | k8s.io/apimachinery v0.31.0 15 | k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 16 | sigs.k8s.io/controller-tools v0.16.0 17 | ) 18 | 19 | require ( 20 | dario.cat/mergo v1.0.1 // indirect 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 22 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 23 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 24 | github.com/fatih/color v1.17.0 // indirect 25 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 26 | github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 // indirect 27 | github.com/go-logr/logr v1.4.2 // indirect 28 | github.com/go-logr/zapr v1.3.0 // indirect 29 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 30 | github.com/go-openapi/jsonreference v0.20.2 // indirect 31 | github.com/go-openapi/swag v0.22.4 // indirect 32 | github.com/gobuffalo/flect v1.0.2 // indirect 33 | github.com/gogo/protobuf v1.3.2 // indirect 34 | github.com/golang/protobuf v1.5.4 // indirect 35 | github.com/google/gnostic-models v0.6.8 // indirect 36 | github.com/google/gofuzz v1.2.0 // indirect 37 | github.com/google/uuid v1.6.0 // indirect 38 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 39 | github.com/josharian/intern v1.0.0 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/mailru/easyjson v0.7.7 // indirect 42 | github.com/mattn/go-colorable v0.1.13 // indirect 43 | github.com/mattn/go-isatty v0.0.20 // indirect 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 45 | github.com/modern-go/reflect2 v1.0.2 // indirect 46 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 47 | github.com/pkg/errors v0.9.1 // indirect 48 | github.com/spf13/afero v1.11.0 // indirect 49 | github.com/spf13/cobra v1.8.1 // indirect 50 | github.com/spf13/pflag v1.0.5 // indirect 51 | github.com/x448/float16 v0.8.4 // indirect 52 | go.uber.org/multierr v1.11.0 // indirect 53 | go.uber.org/zap v1.27.0 // indirect 54 | golang.org/x/mod v0.21.0 // indirect 55 | golang.org/x/net v0.29.0 // indirect 56 | golang.org/x/oauth2 v0.22.0 // indirect 57 | golang.org/x/sync v0.10.0 // indirect 58 | golang.org/x/sys v0.28.0 // indirect 59 | golang.org/x/term v0.27.0 // indirect 60 | golang.org/x/text v0.21.0 // indirect 61 | golang.org/x/time v0.5.0 // indirect 62 | golang.org/x/tools v0.25.0 // indirect 63 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect 64 | google.golang.org/grpc v1.67.0 // indirect 65 | gopkg.in/inf.v0 v0.9.1 // indirect 66 | gopkg.in/yaml.v2 v2.4.0 // indirect 67 | gopkg.in/yaml.v3 v3.0.1 // indirect 68 | k8s.io/api v0.31.0 // indirect 69 | k8s.io/client-go v0.31.0 // indirect 70 | k8s.io/klog/v2 v2.130.1 // indirect 71 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect 72 | sigs.k8s.io/controller-runtime v0.19.0 // indirect 73 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 74 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 75 | sigs.k8s.io/yaml v1.4.0 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release-* 8 | pull_request: {} 9 | workflow_dispatch: 10 | inputs: 11 | version: 12 | description: Package version (e.g. v0.1.0) 13 | required: false 14 | 15 | env: 16 | # Common versions 17 | GO_VERSION: '1.24.4' 18 | GOLANGCI_VERSION: 'v2.1.6' 19 | DOCKER_BUILDX_VERSION: 'v0.24.0' 20 | 21 | # These environment variables are important to the Crossplane CLI install.sh 22 | # script. They determine what version it installs. 23 | XP_CHANNEL: stable 24 | XP_VERSION: v1.20.0 25 | 26 | # This CI job will automatically push new builds to xpkg.upbound.io if the 27 | # XPKG_ACCESS_ID and XPKG_TOKEN secrets are set in the GitHub respository (or 28 | # organization) settings. Create a token at https://accounts.upbound.io. 29 | XPKG_ACCESS_ID: ${{ secrets.XPKG_ACCESS_ID }} 30 | 31 | # The package to push, without a version tag. The default matches GitHub. For 32 | # example xpkg.upbound.io/crossplane/function-template-go. 33 | XPKG: xpkg.upbound.io/${{ github.repository}} 34 | CROSSPLANE_REGORG: ghcr.io/${{ github.repository}} # xpkg.crossplane.io/crossplane-contrib 35 | 36 | # The package version to push. The default is 0.0.0-gitsha. 37 | XPKG_VERSION: ${{ inputs.version }} 38 | 39 | jobs: 40 | lint: 41 | runs-on: ubuntu-24.04 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | 46 | - name: Setup Go 47 | uses: actions/setup-go@v5 48 | with: 49 | go-version: ${{ env.GO_VERSION }} 50 | cache: false # The golangci-lint action does its own caching. 51 | 52 | - name: Check go mod tidy 53 | run: go mod tidy && git diff --exit-code go.mod go.sum 54 | 55 | - name: Lint 56 | uses: golangci/golangci-lint-action@v8 57 | with: 58 | version: ${{ env.GOLANGCI_VERSION }} 59 | 60 | unit-test: 61 | runs-on: ubuntu-24.04 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | 66 | - name: Setup Go 67 | uses: actions/setup-go@v5 68 | with: 69 | go-version: ${{ env.GO_VERSION }} 70 | 71 | - name: Run Unit Tests 72 | run: go test -v -cover ./... 73 | 74 | # We want to build most packages for the amd64 and arm64 architectures. To 75 | # speed this up we build single-platform packages in parallel. We then upload 76 | # those packages to GitHub as a build artifact. The push job downloads those 77 | # artifacts and pushes them as a single multi-platform package. 78 | build: 79 | runs-on: ubuntu-24.04 80 | strategy: 81 | fail-fast: true 82 | matrix: 83 | arch: 84 | - amd64 85 | - arm64 86 | steps: 87 | - name: Setup QEMU 88 | uses: docker/setup-qemu-action@v3 89 | with: 90 | platforms: all 91 | 92 | - name: Setup Docker Buildx 93 | uses: docker/setup-buildx-action@v3 94 | with: 95 | version: ${{ env.DOCKER_BUILDX_VERSION }} 96 | install: true 97 | 98 | - name: Checkout 99 | uses: actions/checkout@v4 100 | 101 | # We ask Docker to use GitHub Action's native caching support to speed up 102 | # the build, per https://docs.docker.com/build/cache/backends/gha/. 103 | - name: Build Runtime 104 | id: image 105 | uses: docker/build-push-action@v6 106 | with: 107 | context: . 108 | platforms: linux/${{ matrix.arch }} 109 | cache-from: type=gha 110 | cache-to: type=gha,mode=max 111 | target: image 112 | build-args: 113 | GO_VERSION=${{ env.GO_VERSION }} 114 | outputs: type=docker,dest=runtime-${{ matrix.arch }}.tar 115 | 116 | - name: Setup the Crossplane CLI 117 | run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh" 118 | 119 | - name: Build Package 120 | run: ./crossplane xpkg build --package-file=${{ matrix.arch }}.xpkg --package-root=package/ --embed-runtime-image-tarball=runtime-${{ matrix.arch }}.tar 121 | 122 | - name: Upload Single-Platform Package 123 | uses: actions/upload-artifact@v4 124 | with: 125 | name: package-${{ matrix.arch }} 126 | path: "*.xpkg" 127 | if-no-files-found: error 128 | retention-days: 1 129 | 130 | # This job downloads the single-platform packages built by the build job, and 131 | # pushes them as a multi-platform package. 132 | push: 133 | runs-on: ubuntu-24.04 134 | permissions: 135 | contents: read 136 | packages: write 137 | needs: 138 | - build 139 | steps: 140 | - name: Checkout 141 | uses: actions/checkout@v4 142 | 143 | - name: Download Single-Platform Packages 144 | uses: actions/download-artifact@v4 145 | with: 146 | # See https://github.com/docker/build-push-action/blob/263435/README.md#summaries 147 | pattern: "!*.dockerbuild" 148 | path: . 149 | merge-multiple: true 150 | 151 | - name: Setup the Crossplane CLI 152 | run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh" 153 | 154 | - name: Login to Upbound 155 | uses: docker/login-action@v3 156 | if: env.XPKG_ACCESS_ID != '' 157 | with: 158 | registry: xpkg.upbound.io 159 | username: ${{ secrets.XPKG_ACCESS_ID }} 160 | password: ${{ secrets.XPKG_TOKEN }} 161 | 162 | # If a version wasn't explicitly passed as a workflow_dispatch input we 163 | # default to version v0.0.0--, for example 164 | # v0.0.0-20231101115142-1091066df799. This is a simple implementation of 165 | # Go's pseudo-versions: https://go.dev/ref/mod#pseudo-versions. 166 | - name: Set Default Multi-Platform Package Version 167 | if: env.XPKG_VERSION == '' 168 | run: echo "XPKG_VERSION=v0.0.0-$(date -d@$(git show -s --format=%ct) +%Y%m%d%H%M%S)-$(git rev-parse --short=12 HEAD)" >> $GITHUB_ENV 169 | 170 | - name: Push Multi-Platform Package to Upbound 171 | if: env.XPKG_ACCESS_ID != '' 172 | run: "./crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${{ env.XPKG }}:${{ env.XPKG_VERSION }}" 173 | 174 | - name: Login to GHCR 175 | uses: docker/login-action@v3.3.0 176 | with: 177 | registry: ghcr.io 178 | username: ${{ github.repository_owner }} 179 | password: ${{ secrets.GITHUB_TOKEN }} 180 | 181 | - name: Push Multi-Platform Package to GHCR 182 | run: "./crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${{ env.CROSSPLANE_REGORG }}:${{ env.XPKG_VERSION }}" 183 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | output: 4 | formats: 5 | text: 6 | path: stdout 7 | 8 | linters: 9 | enable: 10 | - asasalint 11 | - asciicheck 12 | - bidichk 13 | - bodyclose 14 | - contextcheck 15 | - durationcheck 16 | - errchkjson 17 | - errorlint 18 | - exhaustive 19 | - gocheckcompilerdirectives 20 | - gochecksumtype 21 | - goconst 22 | - gocritic 23 | - gocyclo 24 | - gosec 25 | - gosmopolitan 26 | - loggercheck 27 | - makezero 28 | - misspell 29 | - musttag 30 | - nakedret 31 | - nilerr 32 | - nilnesserr 33 | - noctx 34 | - nolintlint 35 | - prealloc 36 | - protogetter 37 | - reassign 38 | - recvcheck 39 | - revive 40 | - rowserrcheck 41 | - spancheck 42 | - sqlclosecheck 43 | - testifylint 44 | - unconvert 45 | - unparam 46 | - zerologlint 47 | 48 | settings: 49 | dupl: 50 | # tokens count to trigger issue, 150 by default 51 | threshold: 100 52 | 53 | errcheck: 54 | # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 55 | # default is false: such cases aren't reported by default. 56 | check-type-assertions: false 57 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 58 | # default is false: such cases aren't reported by default. 59 | check-blank: false 60 | # List of functions to exclude from checking, where each entry is a single function to exclude. 61 | # See https://github.com/kisielk/errcheck#excluding-functions for details. 62 | exclude-functions: 63 | - io/ioutil.ReadFile 64 | 65 | goconst: 66 | # minimal length of string constant, 3 by default 67 | min-len: 3 68 | # minimal occurrences count to trigger, 3 by default 69 | min-occurrences: 5 70 | 71 | gocritic: 72 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks. 73 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 74 | enabled-tags: 75 | - performance 76 | settings: # settings passed to gocritic 77 | captLocal: # must be valid enabled check name 78 | paramsOnly: true 79 | rangeValCopy: 80 | sizeThreshold: 32 81 | 82 | gocyclo: 83 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 84 | min-complexity: 10 85 | 86 | govet: 87 | # report about shadowed variables 88 | disable: 89 | - shadow 90 | 91 | lll: 92 | # tab width in spaces. Default to 1. 93 | tab-width: 1 94 | 95 | nakedret: 96 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 97 | max-func-lines: 30 98 | 99 | nolintlint: 100 | require-explanation: true 101 | require-specific: true 102 | 103 | prealloc: 104 | # XXX: we don't recommend using this linter before doing performance profiling. 105 | # For most programs usage of prealloc will be a premature optimization. 106 | 107 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 108 | # True by default. 109 | simple: true 110 | range-loops: true 111 | for-loops: false 112 | 113 | unparam: 114 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 115 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 116 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 117 | # with golangci-lint call it on a directory with the changed file. 118 | check-exported: false 119 | 120 | exclusions: 121 | generated: lax 122 | rules: 123 | 124 | # Excluding configuration per-path and per-linter 125 | - linters: 126 | - all 127 | path: zz_generated\..+\.go$ 128 | 129 | # Exclude some linters from running on tests files. 130 | - linters: 131 | - dupl 132 | - errcheck 133 | - gocyclo 134 | - gosec 135 | - scopelint 136 | - unparam 137 | path: _test(ing)?\.go 138 | 139 | # Ease some gocritic warnings on test files. 140 | - linters: 141 | - gocritic 142 | path: _test\.go 143 | text: (unnamedResult|exitAfterDefer) 144 | 145 | # These are performance optimisations rather than style issues per se. 146 | # They warn when function arguments or range values copy a lot of memory 147 | # rather than using a pointer. 148 | - linters: 149 | - gocritic 150 | text: '(hugeParam|rangeValCopy):' 151 | 152 | # This "TestMain should call os.Exit to set exit code" warning is not clever 153 | # enough to notice that we call a helper method that calls os.Exit. 154 | - linters: 155 | - staticcheck 156 | text: 'SA3000:' 157 | 158 | # This is a "potential hardcoded credentials" warning. It's triggered by 159 | # any variable with 'secret' in the same, and thus hits a lot of false 160 | # positives in Kubernetes land where a Secret is an object type. 161 | - linters: 162 | - gosec 163 | text: 'G101:' 164 | 165 | # This is an 'errors unhandled' warning that duplicates errcheck. 166 | - linters: 167 | - gosec 168 | text: 'G104:' 169 | 170 | # Some k8s dependencies do not have JSON tags on all fields in structs. 171 | - linters: 172 | - musttag 173 | path: k8s.io/ 174 | paths: 175 | - third_party$ 176 | - builtin$ 177 | - examples$ 178 | 179 | issues: 180 | # maximum issues count per one linter. set to 0 to disable. default is 50. 181 | max-issues-per-linter: 0 182 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 183 | max-same-issues: 0 184 | # Show only new issues: if there are unstaged changes or untracked files, 185 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 186 | # It's a super-useful option for integration of golangci-lint into existing 187 | # large codebase. It's not practical to fix all existing issues at the moment 188 | # of integration: much better don't allow issues in new code. 189 | # Default is false. 190 | new: false 191 | 192 | formatters: 193 | enable: 194 | - gci 195 | - gofmt 196 | settings: 197 | gci: 198 | sections: 199 | - standard 200 | - default 201 | - prefix(github.com/crossplane) 202 | - prefix(github.com/crossplane-contrib) 203 | - blank 204 | - dot 205 | custom-order: true 206 | gofmt: 207 | # simplify code: gofmt with `-s` option, true by default 208 | simplify: true 209 | exclusions: 210 | generated: lax 211 | paths: 212 | - third_party$ 213 | - builtin$ 214 | - examples$ 215 | -------------------------------------------------------------------------------- /input/v1beta1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | // Code generated by controller-gen. DO NOT EDIT. 4 | 5 | package v1beta1 6 | 7 | import ( 8 | commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 9 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | ) 12 | 13 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 14 | func (in *EnvironmentSource) DeepCopyInto(out *EnvironmentSource) { 15 | *out = *in 16 | if in.Ref != nil { 17 | in, out := &in.Ref, &out.Ref 18 | *out = new(EnvironmentSourceReference) 19 | **out = **in 20 | } 21 | if in.Selector != nil { 22 | in, out := &in.Selector, &out.Selector 23 | *out = new(EnvironmentSourceSelector) 24 | (*in).DeepCopyInto(*out) 25 | } 26 | } 27 | 28 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvironmentSource. 29 | func (in *EnvironmentSource) DeepCopy() *EnvironmentSource { 30 | if in == nil { 31 | return nil 32 | } 33 | out := new(EnvironmentSource) 34 | in.DeepCopyInto(out) 35 | return out 36 | } 37 | 38 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 39 | func (in *EnvironmentSourceReference) DeepCopyInto(out *EnvironmentSourceReference) { 40 | *out = *in 41 | } 42 | 43 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvironmentSourceReference. 44 | func (in *EnvironmentSourceReference) DeepCopy() *EnvironmentSourceReference { 45 | if in == nil { 46 | return nil 47 | } 48 | out := new(EnvironmentSourceReference) 49 | in.DeepCopyInto(out) 50 | return out 51 | } 52 | 53 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 54 | func (in *EnvironmentSourceSelector) DeepCopyInto(out *EnvironmentSourceSelector) { 55 | *out = *in 56 | if in.MaxMatch != nil { 57 | in, out := &in.MaxMatch, &out.MaxMatch 58 | *out = new(uint64) 59 | **out = **in 60 | } 61 | if in.MinMatch != nil { 62 | in, out := &in.MinMatch, &out.MinMatch 63 | *out = new(uint64) 64 | **out = **in 65 | } 66 | if in.MatchLabels != nil { 67 | in, out := &in.MatchLabels, &out.MatchLabels 68 | *out = make([]EnvironmentSourceSelectorLabelMatcher, len(*in)) 69 | for i := range *in { 70 | (*in)[i].DeepCopyInto(&(*out)[i]) 71 | } 72 | } 73 | } 74 | 75 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvironmentSourceSelector. 76 | func (in *EnvironmentSourceSelector) DeepCopy() *EnvironmentSourceSelector { 77 | if in == nil { 78 | return nil 79 | } 80 | out := new(EnvironmentSourceSelector) 81 | in.DeepCopyInto(out) 82 | return out 83 | } 84 | 85 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 86 | func (in *EnvironmentSourceSelectorLabelMatcher) DeepCopyInto(out *EnvironmentSourceSelectorLabelMatcher) { 87 | *out = *in 88 | if in.ValueFromFieldPath != nil { 89 | in, out := &in.ValueFromFieldPath, &out.ValueFromFieldPath 90 | *out = new(string) 91 | **out = **in 92 | } 93 | if in.FromFieldPathPolicy != nil { 94 | in, out := &in.FromFieldPathPolicy, &out.FromFieldPathPolicy 95 | *out = new(FromFieldPathPolicy) 96 | **out = **in 97 | } 98 | if in.Value != nil { 99 | in, out := &in.Value, &out.Value 100 | *out = new(string) 101 | **out = **in 102 | } 103 | } 104 | 105 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvironmentSourceSelectorLabelMatcher. 106 | func (in *EnvironmentSourceSelectorLabelMatcher) DeepCopy() *EnvironmentSourceSelectorLabelMatcher { 107 | if in == nil { 108 | return nil 109 | } 110 | out := new(EnvironmentSourceSelectorLabelMatcher) 111 | in.DeepCopyInto(out) 112 | return out 113 | } 114 | 115 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 116 | func (in *Input) DeepCopyInto(out *Input) { 117 | *out = *in 118 | out.TypeMeta = in.TypeMeta 119 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 120 | in.Spec.DeepCopyInto(&out.Spec) 121 | } 122 | 123 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Input. 124 | func (in *Input) DeepCopy() *Input { 125 | if in == nil { 126 | return nil 127 | } 128 | out := new(Input) 129 | in.DeepCopyInto(out) 130 | return out 131 | } 132 | 133 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 134 | func (in *Input) DeepCopyObject() runtime.Object { 135 | if c := in.DeepCopy(); c != nil { 136 | return c 137 | } 138 | return nil 139 | } 140 | 141 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 142 | func (in *InputSpec) DeepCopyInto(out *InputSpec) { 143 | *out = *in 144 | if in.DefaultData != nil { 145 | in, out := &in.DefaultData, &out.DefaultData 146 | *out = make(map[string]v1.JSON, len(*in)) 147 | for key, val := range *in { 148 | (*out)[key] = *val.DeepCopy() 149 | } 150 | } 151 | if in.EnvironmentConfigs != nil { 152 | in, out := &in.EnvironmentConfigs, &out.EnvironmentConfigs 153 | *out = make([]EnvironmentSource, len(*in)) 154 | for i := range *in { 155 | (*in)[i].DeepCopyInto(&(*out)[i]) 156 | } 157 | } 158 | if in.Policy != nil { 159 | in, out := &in.Policy, &out.Policy 160 | *out = new(Policy) 161 | (*in).DeepCopyInto(*out) 162 | } 163 | } 164 | 165 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InputSpec. 166 | func (in *InputSpec) DeepCopy() *InputSpec { 167 | if in == nil { 168 | return nil 169 | } 170 | out := new(InputSpec) 171 | in.DeepCopyInto(out) 172 | return out 173 | } 174 | 175 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 176 | func (in *PatchPolicy) DeepCopyInto(out *PatchPolicy) { 177 | *out = *in 178 | if in.FromFieldPath != nil { 179 | in, out := &in.FromFieldPath, &out.FromFieldPath 180 | *out = new(FromFieldPathPolicy) 181 | **out = **in 182 | } 183 | if in.MergeOptions != nil { 184 | in, out := &in.MergeOptions, &out.MergeOptions 185 | *out = new(commonv1.MergeOptions) 186 | (*in).DeepCopyInto(*out) 187 | } 188 | } 189 | 190 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchPolicy. 191 | func (in *PatchPolicy) DeepCopy() *PatchPolicy { 192 | if in == nil { 193 | return nil 194 | } 195 | out := new(PatchPolicy) 196 | in.DeepCopyInto(out) 197 | return out 198 | } 199 | 200 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 201 | func (in *Policy) DeepCopyInto(out *Policy) { 202 | *out = *in 203 | if in.Resolution != nil { 204 | in, out := &in.Resolution, &out.Resolution 205 | *out = new(commonv1.ResolutionPolicy) 206 | **out = **in 207 | } 208 | } 209 | 210 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy. 211 | func (in *Policy) DeepCopy() *Policy { 212 | if in == nil { 213 | return nil 214 | } 215 | out := new(Policy) 216 | in.DeepCopyInto(out) 217 | return out 218 | } 219 | -------------------------------------------------------------------------------- /package/input/environmentconfigs.fn.crossplane.io_inputs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: inputs.environmentconfigs.fn.crossplane.io 8 | spec: 9 | group: environmentconfigs.fn.crossplane.io 10 | names: 11 | categories: 12 | - crossplane 13 | kind: Input 14 | listKind: InputList 15 | plural: inputs 16 | singular: input 17 | scope: Namespaced 18 | versions: 19 | - name: v1beta1 20 | schema: 21 | openAPIV3Schema: 22 | description: Input can be used to provide input to this Function. 23 | properties: 24 | apiVersion: 25 | description: |- 26 | APIVersion defines the versioned schema of this representation of an object. 27 | Servers should convert recognized schemas to the latest internal value, and 28 | may reject unrecognized values. 29 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 30 | type: string 31 | kind: 32 | description: |- 33 | Kind is a string value representing the REST resource this object represents. 34 | Servers may infer this from the endpoint the client submits requests to. 35 | Cannot be updated. 36 | In CamelCase. 37 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 38 | type: string 39 | metadata: 40 | type: object 41 | spec: 42 | description: |- 43 | An InputSpec specifies the environment for rendering composed 44 | resources. 45 | properties: 46 | defaultData: 47 | additionalProperties: 48 | x-kubernetes-preserve-unknown-fields: true 49 | description: |- 50 | DefaultData statically defines the initial state of the environment. 51 | It has the same schema-less structure as the data field in 52 | environment configs. 53 | It is overwritten by the selected environment configs. 54 | type: object 55 | environmentConfigs: 56 | description: |- 57 | EnvironmentConfigs selects a list of `EnvironmentConfig`s. The resolved 58 | resources are stored in the composite resource at 59 | `spec.environmentConfigRefs` and is only updated if it is null. 60 | 61 | 62 | The list of references is used to compute an in-memory environment at 63 | compose time. The data of all object is merged in the order they are 64 | listed, meaning the values of EnvironmentConfigs with a larger index take 65 | priority over ones with smaller indices. 66 | 67 | 68 | The computed environment can be accessed in a composition using 69 | `FromEnvironmentFieldPath` and `CombineFromEnvironment` patches. 70 | items: 71 | description: EnvironmentSource selects a EnvironmentConfig resource. 72 | properties: 73 | ref: 74 | description: |- 75 | Ref is a named reference to a single EnvironmentConfig. 76 | Either Ref or Selector is required. 77 | properties: 78 | name: 79 | description: The name of the object. 80 | type: string 81 | required: 82 | - name 83 | type: object 84 | selector: 85 | description: Selector selects EnvironmentConfig(s) via labels. 86 | properties: 87 | matchLabels: 88 | description: MatchLabels ensures an object with matching 89 | labels is selected. 90 | items: 91 | description: |- 92 | An EnvironmentSourceSelectorLabelMatcher acts like a k8s label selector but 93 | can draw the label value from a different path. 94 | properties: 95 | fromFieldPathPolicy: 96 | default: Required 97 | description: |- 98 | FromFieldPathPolicy specifies the policy for the valueFromFieldPath. 99 | The default is Required, meaning that an error will be returned if the 100 | field is not found in the composite resource. 101 | Optional means that if the field is not found in the composite resource, 102 | that label pair will just be skipped. N.B. other specified label 103 | matchers will still be used to retrieve the desired 104 | environment config, if any. 105 | enum: 106 | - Optional 107 | - Required 108 | type: string 109 | key: 110 | description: Key of the label to match. 111 | type: string 112 | type: 113 | default: FromCompositeFieldPath 114 | description: Type specifies where the value for a 115 | label comes from. 116 | enum: 117 | - FromCompositeFieldPath 118 | - Value 119 | type: string 120 | value: 121 | description: Value specifies a literal label value. 122 | type: string 123 | valueFromFieldPath: 124 | description: ValueFromFieldPath specifies the field 125 | path to look for the label value. 126 | type: string 127 | required: 128 | - key 129 | type: object 130 | type: array 131 | maxMatch: 132 | description: MaxMatch specifies the number of extracted 133 | EnvironmentConfigs in Multiple mode, extracts all if nil. 134 | format: int64 135 | type: integer 136 | minMatch: 137 | description: MinMatch specifies the required minimum of 138 | extracted EnvironmentConfigs in Multiple mode. 139 | format: int64 140 | type: integer 141 | mode: 142 | default: Single 143 | description: 'Mode specifies retrieval strategy: "Single" 144 | or "Multiple".' 145 | enum: 146 | - Single 147 | - Multiple 148 | type: string 149 | sortByFieldPath: 150 | default: metadata.name 151 | description: SortByFieldPath is the path to the field based 152 | on which list of EnvironmentConfigs is alphabetically 153 | sorted. 154 | type: string 155 | type: object 156 | type: 157 | default: Reference 158 | description: |- 159 | Type specifies the way the EnvironmentConfig is selected. 160 | Default is `Reference` 161 | enum: 162 | - Reference 163 | - Selector 164 | type: string 165 | type: object 166 | type: array 167 | policy: 168 | description: |- 169 | Policy represents the Resolution policy which apply to all 170 | EnvironmentSourceReferences in EnvironmentConfigs list. 171 | properties: 172 | resolution: 173 | default: Required 174 | description: |- 175 | Resolution specifies whether resolution of this reference is required. 176 | The default is 'Required', which means the reconcile will fail if the 177 | reference cannot be resolved. 'Optional' means this reference will be 178 | a no-op if it cannot be resolved. 179 | enum: 180 | - Required 181 | - Optional 182 | type: string 183 | type: object 184 | type: object 185 | type: object 186 | served: true 187 | storage: true 188 | -------------------------------------------------------------------------------- /input/v1beta1/composition_environment.go: -------------------------------------------------------------------------------- 1 | package v1beta1 2 | 3 | /* 4 | Copyright 2022 The Crossplane Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | import ( 20 | extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | ) 24 | 25 | // An InputSpec specifies the environment for rendering composed 26 | // resources. 27 | type InputSpec struct { 28 | // DefaultData statically defines the initial state of the environment. 29 | // It has the same schema-less structure as the data field in 30 | // environment configs. 31 | // It is overwritten by the selected environment configs. 32 | DefaultData map[string]extv1.JSON `json:"defaultData,omitempty"` 33 | 34 | // EnvironmentConfigs selects a list of `EnvironmentConfig`s. The resolved 35 | // resources are stored in the composite resource at 36 | // `spec.environmentConfigRefs` and is only updated if it is null. 37 | // 38 | // The list of references is used to compute an in-memory environment at 39 | // compose time. The data of all object is merged in the order they are 40 | // listed, meaning the values of EnvironmentConfigs with a larger index take 41 | // priority over ones with smaller indices. 42 | // 43 | // The computed environment can be accessed in a composition using 44 | // `FromEnvironmentFieldPath` and `CombineFromEnvironment` patches. 45 | // +optional 46 | EnvironmentConfigs []EnvironmentSource `json:"environmentConfigs,omitempty"` 47 | 48 | // Policy represents the Resolution policy which apply to all 49 | // EnvironmentSourceReferences in EnvironmentConfigs list. 50 | // +optional 51 | Policy *Policy `json:"policy,omitempty"` 52 | } 53 | 54 | // Policy represents the Resolution policy of Reference instance. 55 | type Policy struct { 56 | // Resolve specifies when this reference should be resolved. The default 57 | // is 'IfNotPresent', which will attempt to resolve the reference only when 58 | // the corresponding field is not present. Use 'Always' to resolve the 59 | // reference on every reconcile. 60 | // +optional 61 | // +kubebuilder:validation:Enum=Always;IfNotPresent 62 | // TODO(phisco): we could consider implementing IfNotPresent, but it will 63 | // require environmentConfigRefs to be moved to the XR's status, as 64 | // Functions can not write to the XR spec. Right now we behave as if 65 | // Always was set. 66 | // Resolve *xpv1.ResolvePolicy `json:"resolve,omitempty"` 67 | 68 | // Resolution specifies whether resolution of this reference is required. 69 | // The default is 'Required', which means the reconcile will fail if the 70 | // reference cannot be resolved. 'Optional' means this reference will be 71 | // a no-op if it cannot be resolved. 72 | // +optional 73 | // +kubebuilder:default=Required 74 | // +kubebuilder:validation:Enum=Required;Optional 75 | Resolution *xpv1.ResolutionPolicy `json:"resolution,omitempty"` 76 | } 77 | 78 | // IsResolutionPolicyOptional checks whether the resolution policy of relevant 79 | // reference is Optional. 80 | func (p *Policy) IsResolutionPolicyOptional() bool { 81 | if p == nil || p.Resolution == nil { 82 | return false 83 | } 84 | 85 | return *p.Resolution == xpv1.ResolutionPolicyOptional 86 | } 87 | 88 | // EnvironmentSourceType specifies the way the EnvironmentConfig is selected. 89 | type EnvironmentSourceType string 90 | 91 | const ( 92 | // EnvironmentSourceTypeReference by name. 93 | EnvironmentSourceTypeReference EnvironmentSourceType = "Reference" 94 | // EnvironmentSourceTypeSelector by labels. 95 | EnvironmentSourceTypeSelector EnvironmentSourceType = "Selector" 96 | ) 97 | 98 | // EnvironmentSource selects a EnvironmentConfig resource. 99 | type EnvironmentSource struct { 100 | // Type specifies the way the EnvironmentConfig is selected. 101 | // Default is `Reference` 102 | // +optional 103 | // +kubebuilder:validation:Enum=Reference;Selector 104 | // +kubebuilder:default=Reference 105 | Type EnvironmentSourceType `json:"type,omitempty"` 106 | 107 | // Ref is a named reference to a single EnvironmentConfig. 108 | // Either Ref or Selector is required. 109 | // +optional 110 | Ref *EnvironmentSourceReference `json:"ref,omitempty"` 111 | 112 | // Selector selects EnvironmentConfig(s) via labels. 113 | // +optional 114 | Selector *EnvironmentSourceSelector `json:"selector,omitempty"` 115 | } 116 | 117 | // GetType returns the type of the environment source, returning the default if not set. 118 | func (e *EnvironmentSource) GetType() EnvironmentSourceType { 119 | if e == nil || e.Type == "" { 120 | return EnvironmentSourceTypeReference 121 | } 122 | return e.Type 123 | } 124 | 125 | // An EnvironmentSourceReference references an EnvironmentConfig by it's name. 126 | type EnvironmentSourceReference struct { 127 | // The name of the object. 128 | Name string `json:"name"` 129 | } 130 | 131 | // EnvironmentSourceSelectorModeType specifies amount of retrieved EnvironmentConfigs 132 | // with matching label. 133 | type EnvironmentSourceSelectorModeType string 134 | 135 | const ( 136 | // EnvironmentSourceSelectorSingleMode extracts only first EnvironmentConfig from the sorted list. 137 | EnvironmentSourceSelectorSingleMode EnvironmentSourceSelectorModeType = "Single" 138 | 139 | // EnvironmentSourceSelectorMultiMode extracts multiple EnvironmentConfigs from the sorted list. 140 | EnvironmentSourceSelectorMultiMode EnvironmentSourceSelectorModeType = "Multiple" 141 | ) 142 | 143 | // An EnvironmentSourceSelector selects an EnvironmentConfig via labels. 144 | type EnvironmentSourceSelector struct { 145 | 146 | // Mode specifies retrieval strategy: "Single" or "Multiple". 147 | // +kubebuilder:validation:Enum=Single;Multiple 148 | // +kubebuilder:default=Single 149 | Mode EnvironmentSourceSelectorModeType `json:"mode,omitempty"` 150 | 151 | // MaxMatch specifies the number of extracted EnvironmentConfigs in Multiple mode, extracts all if nil. 152 | MaxMatch *uint64 `json:"maxMatch,omitempty"` 153 | 154 | // MinMatch specifies the required minimum of extracted EnvironmentConfigs in Multiple mode. 155 | MinMatch *uint64 `json:"minMatch,omitempty"` 156 | 157 | // SortByFieldPath is the path to the field based on which list of EnvironmentConfigs is alphabetically sorted. 158 | // +kubebuilder:default="metadata.name" 159 | SortByFieldPath string `json:"sortByFieldPath,omitempty"` 160 | 161 | // MatchLabels ensures an object with matching labels is selected. 162 | MatchLabels []EnvironmentSourceSelectorLabelMatcher `json:"matchLabels,omitempty"` 163 | } 164 | 165 | // GetMode returns the mode of the EnvironmentSourceSelector, returning the default if not set. 166 | func (e *EnvironmentSourceSelector) GetMode() EnvironmentSourceSelectorModeType { 167 | if e == nil || e.Mode == "" { 168 | return EnvironmentSourceSelectorSingleMode 169 | } 170 | return e.Mode 171 | } 172 | 173 | // GetSortByFieldPath returns the field path used to sort the EnvironmentConfigs, 174 | // returning the default if not set. 175 | func (e *EnvironmentSourceSelector) GetSortByFieldPath() string { 176 | if e == nil || e.SortByFieldPath == "" { 177 | return "metadata.name" 178 | } 179 | return e.SortByFieldPath 180 | } 181 | 182 | // EnvironmentSourceSelectorLabelMatcherType specifies where the value for a 183 | // label comes from. 184 | type EnvironmentSourceSelectorLabelMatcherType string 185 | 186 | const ( 187 | // EnvironmentSourceSelectorLabelMatcherTypeFromCompositeFieldPath extracts 188 | // the label value from a composite fieldpath. 189 | EnvironmentSourceSelectorLabelMatcherTypeFromCompositeFieldPath EnvironmentSourceSelectorLabelMatcherType = "FromCompositeFieldPath" 190 | // EnvironmentSourceSelectorLabelMatcherTypeValue uses a literal as label 191 | // value. 192 | EnvironmentSourceSelectorLabelMatcherTypeValue EnvironmentSourceSelectorLabelMatcherType = "Value" 193 | ) 194 | 195 | // An EnvironmentSourceSelectorLabelMatcher acts like a k8s label selector but 196 | // can draw the label value from a different path. 197 | type EnvironmentSourceSelectorLabelMatcher struct { 198 | // Type specifies where the value for a label comes from. 199 | // +optional 200 | // +kubebuilder:validation:Enum=FromCompositeFieldPath;Value 201 | // +kubebuilder:default=FromCompositeFieldPath 202 | Type EnvironmentSourceSelectorLabelMatcherType `json:"type,omitempty"` 203 | 204 | // Key of the label to match. 205 | Key string `json:"key"` 206 | 207 | // ValueFromFieldPath specifies the field path to look for the label value. 208 | ValueFromFieldPath *string `json:"valueFromFieldPath,omitempty"` 209 | 210 | // FromFieldPathPolicy specifies the policy for the valueFromFieldPath. 211 | // The default is Required, meaning that an error will be returned if the 212 | // field is not found in the composite resource. 213 | // Optional means that if the field is not found in the composite resource, 214 | // that label pair will just be skipped. N.B. other specified label 215 | // matchers will still be used to retrieve the desired 216 | // environment config, if any. 217 | // +kubebuilder:validation:Enum=Optional;Required 218 | // +kubebuilder:default=Required 219 | FromFieldPathPolicy *FromFieldPathPolicy `json:"fromFieldPathPolicy,omitempty"` 220 | 221 | // Value specifies a literal label value. 222 | Value *string `json:"value,omitempty"` 223 | } 224 | 225 | // FromFieldPathIsOptional returns true if the FromFieldPathPolicy is set to 226 | // Optional. 227 | func (e *EnvironmentSourceSelectorLabelMatcher) FromFieldPathIsOptional() bool { 228 | return e.FromFieldPathPolicy != nil && *e.FromFieldPathPolicy == FromFieldPathPolicyOptional 229 | } 230 | 231 | // GetType returns the type of the label matcher, returning the default if not set. 232 | func (e *EnvironmentSourceSelectorLabelMatcher) GetType() EnvironmentSourceSelectorLabelMatcherType { 233 | if e == nil || e.Type == "" { 234 | return EnvironmentSourceSelectorLabelMatcherTypeFromCompositeFieldPath 235 | } 236 | return e.Type 237 | } 238 | 239 | // A FromFieldPathPolicy determines how to patch from a field path. 240 | type FromFieldPathPolicy string 241 | 242 | // FromFieldPath patch policies. 243 | const ( 244 | FromFieldPathPolicyOptional FromFieldPathPolicy = "Optional" 245 | FromFieldPathPolicyRequired FromFieldPathPolicy = "Required" 246 | ) 247 | 248 | // A PatchPolicy configures the specifics of patching behaviour. 249 | type PatchPolicy struct { 250 | // FromFieldPath specifies how to patch from a field path. The default is 251 | // 'Optional', which means the patch will be a no-op if the specified 252 | // fromFieldPath does not exist. Use 'Required' if the patch should fail if 253 | // the specified path does not exist. 254 | // +kubebuilder:validation:Enum=Optional;Required 255 | // +optional 256 | FromFieldPath *FromFieldPathPolicy `json:"fromFieldPath,omitempty"` 257 | MergeOptions *xpv1.MergeOptions `json:"mergeOptions,omitempty"` 258 | } 259 | 260 | // GetFromFieldPathPolicy returns the FromFieldPathPolicy for this PatchPolicy, defaulting to FromFieldPathPolicyOptional if not specified. 261 | func (pp *PatchPolicy) GetFromFieldPathPolicy() FromFieldPathPolicy { 262 | if pp == nil || pp.FromFieldPath == nil { 263 | return FromFieldPathPolicyOptional 264 | } 265 | return *pp.FromFieldPath 266 | } 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /fn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "sort" 9 | 10 | "google.golang.org/protobuf/types/known/structpb" 11 | extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | 15 | "github.com/crossplane/crossplane-runtime/pkg/errors" 16 | "github.com/crossplane/crossplane-runtime/pkg/fieldpath" 17 | "github.com/crossplane/crossplane-runtime/pkg/logging" 18 | fnv1 "github.com/crossplane/function-sdk-go/proto/v1" 19 | "github.com/crossplane/function-sdk-go/request" 20 | "github.com/crossplane/function-sdk-go/resource" 21 | "github.com/crossplane/function-sdk-go/response" 22 | 23 | "github.com/crossplane-contrib/function-environment-configs/input/v1beta1" 24 | ) 25 | 26 | const ( 27 | // FunctionContextKeyEnvironment is a well-known Context key where the computed Environment 28 | // will be stored, so that Crossplane v1 and other functions can access it, e.g. function-patch-and-transform. 29 | FunctionContextKeyEnvironment = "apiextensions.crossplane.io/environment" 30 | ) 31 | 32 | // Function returns whatever response you ask it to. 33 | type Function struct { 34 | fnv1.UnimplementedFunctionRunnerServiceServer 35 | 36 | log logging.Logger 37 | } 38 | 39 | // RunFunction runs the Function. 40 | func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) { //nolint:gocyclo // TODO(phisco): refactor 41 | f.log.Debug("Running function", "tag", req.GetMeta().GetTag()) 42 | 43 | rsp := response.To(req, response.DefaultTTL) 44 | 45 | in := &v1beta1.Input{} 46 | if err := request.GetInput(req, in); err != nil { 47 | response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req)) 48 | return rsp, nil 49 | } 50 | 51 | if in.Spec.EnvironmentConfigs == nil { 52 | f.log.Debug("No EnvironmentConfigs specified, exiting") 53 | return rsp, nil 54 | } 55 | 56 | oxr, err := request.GetObservedCompositeResource(req) 57 | if err != nil { 58 | response.Fatal(rsp, errors.Wrapf(err, "cannot get observed composite resource")) 59 | return rsp, nil 60 | } 61 | 62 | // Note(phisco): We need to compute the selectors even if we already 63 | // requested them already at the previous iteration. 64 | requirements, err := buildRequirements(in, oxr) 65 | if err != nil { 66 | response.Fatal(rsp, errors.Wrapf(err, "cannot build requirements")) 67 | return rsp, nil 68 | } 69 | 70 | rsp.Requirements = requirements 71 | 72 | if req.ExtraResources == nil { 73 | f.log.Debug("No extra resources specified, exiting", "requirements", rsp.GetRequirements()) 74 | return rsp, nil 75 | } 76 | 77 | var inputEnv *unstructured.Unstructured 78 | if v, ok := request.GetContextKey(req, FunctionContextKeyEnvironment); ok { 79 | inputEnv = &unstructured.Unstructured{} 80 | if err := resource.AsObject(v.GetStructValue(), inputEnv); err != nil { 81 | response.Fatal(rsp, errors.Wrapf(err, "cannot get Composition environment from %T context key %q", req, FunctionContextKeyEnvironment)) 82 | return rsp, nil 83 | } 84 | f.log.Debug("Loaded Composition environment from Function context", "context-key", FunctionContextKeyEnvironment) 85 | } 86 | 87 | extraResources, err := request.GetExtraResources(req) 88 | if err != nil { 89 | response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req)) 90 | return rsp, nil 91 | } 92 | 93 | envConfigs, err := getSelectedEnvConfigs(in, extraResources) 94 | if err != nil { 95 | response.Fatal(rsp, errors.Wrapf(err, "cannot get selected environment configs")) 96 | return rsp, nil 97 | } 98 | 99 | mergedData, err := mergeEnvConfigsData(envConfigs) 100 | if err != nil { 101 | response.Fatal(rsp, errors.Wrapf(err, "cannot merge environment data")) 102 | return rsp, nil 103 | } 104 | 105 | // merge input env if any (merged EnvironmentConfigs data > default data > input env) 106 | if inputEnv != nil { 107 | mergedData = mergeMaps(inputEnv.Object, mergedData) 108 | } 109 | 110 | // merge default data if any (merged EnvironmentConfigs data > default data > input env) 111 | if in.Spec.DefaultData != nil { 112 | defaultData, err := unmarshalData(in.Spec.DefaultData) 113 | if err != nil { 114 | response.Fatal(rsp, errors.Wrapf(err, "cannot unmarshal default data")) 115 | return rsp, nil 116 | } 117 | mergedData = mergeMaps(defaultData, mergedData) 118 | } 119 | 120 | // build environment and return it in the response as context 121 | out := &unstructured.Unstructured{Object: mergedData} 122 | if out.GroupVersionKind().Empty() { 123 | out.SetGroupVersionKind(schema.GroupVersionKind{Group: "internal.crossplane.io", Kind: "Environment", Version: "v1alpha1"}) 124 | } 125 | v, err := resource.AsStruct(out) 126 | if err != nil { 127 | response.Fatal(rsp, errors.Wrap(err, "cannot convert Composition environment to protobuf Struct well-known type")) 128 | return rsp, nil 129 | } 130 | f.log.Debug("Computed Composition environment", "environment", v) 131 | response.SetContextKey(rsp, FunctionContextKeyEnvironment, structpb.NewStructValue(v)) 132 | 133 | return rsp, nil 134 | } 135 | 136 | func getSelectedEnvConfigs(in *v1beta1.Input, extraResources map[string][]resource.Extra) (envConfigs []unstructured.Unstructured, err error) { 137 | for i, config := range in.Spec.EnvironmentConfigs { 138 | extraResName := fmt.Sprintf("environment-config-%d", i) 139 | resources, ok := extraResources[extraResName] 140 | if !ok { 141 | // Skip if the extra resource was not requested (e.g., optional selector with no matchLabels) 142 | continue 143 | } 144 | switch config.GetType() { 145 | case v1beta1.EnvironmentSourceTypeReference: 146 | out, err := processSourceByReference(in, config, resources) 147 | if err != nil { 148 | return nil, errors.Wrapf(err, "cannot process environment config %q by reference, %q", config.Ref.Name, extraResName) 149 | } 150 | if out == nil { 151 | continue 152 | } 153 | envConfigs = append(envConfigs, *out) 154 | 155 | case v1beta1.EnvironmentSourceTypeSelector: 156 | out, err := processEnvironmentSource(config, resources) 157 | if err != nil { 158 | return nil, errors.Wrapf(err, "cannot process environment config %q by selector", extraResName) 159 | } 160 | if len(out) > 0 { 161 | envConfigs = append(envConfigs, out...) 162 | } 163 | } 164 | } 165 | return envConfigs, nil 166 | } 167 | 168 | func processEnvironmentSource(config v1beta1.EnvironmentSource, resources []resource.Extra) ([]unstructured.Unstructured, error) { 169 | out := make([]unstructured.Unstructured, 0) 170 | selector := config.Selector 171 | switch selector.GetMode() { 172 | case v1beta1.EnvironmentSourceSelectorSingleMode: 173 | if len(resources) != 1 { 174 | return nil, errors.Errorf("expected exactly one extra resource, got %d", len(resources)) 175 | } 176 | out = append(out, *resources[0].Resource) 177 | case v1beta1.EnvironmentSourceSelectorMultiMode: 178 | if selector.MinMatch != nil && uint64(len(resources)) < *selector.MinMatch { 179 | return nil, errors.Errorf("expected at least %d extra resources, got %d", *selector.MinMatch, len(resources)) 180 | } 181 | if err := sortExtrasByFieldPath(resources, selector.GetSortByFieldPath()); err != nil { 182 | return nil, err 183 | } 184 | if selector.MaxMatch != nil && uint64(len(resources)) > *selector.MaxMatch { 185 | resources = resources[:*selector.MaxMatch] 186 | } 187 | for _, r := range resources { 188 | out = append(out, *r.Resource) 189 | } 190 | default: 191 | // should never happen 192 | return nil, errors.Errorf("unknown selector mode %q", selector.Mode) 193 | } 194 | return out, nil 195 | } 196 | 197 | func processSourceByReference(in *v1beta1.Input, config v1beta1.EnvironmentSource, resources []resource.Extra) (*unstructured.Unstructured, error) { 198 | envConfigName := config.Ref.Name 199 | if len(resources) == 0 { 200 | if in.Spec.Policy.IsResolutionPolicyOptional() { 201 | return nil, nil 202 | } 203 | return nil, errors.Errorf("Required environment config %q not found", envConfigName) 204 | } 205 | if len(resources) > 1 { 206 | return nil, errors.Errorf("expected exactly one extra resource %q, got %d", envConfigName, len(resources)) 207 | } 208 | return resources[0].Resource, nil 209 | } 210 | 211 | func sortExtrasByFieldPath(extras []resource.Extra, path string) error { //nolint:gocyclo // TODO(phisco): refactor 212 | if path == "" { 213 | return errors.New("cannot sort by empty field path") 214 | } 215 | p := make([]struct { 216 | ec resource.Extra 217 | val any 218 | }, len(extras)) 219 | 220 | var t reflect.Type 221 | for i := range extras { 222 | p[i].ec = extras[i] 223 | val, err := fieldpath.Pave(extras[i].Resource.Object).GetValue(path) 224 | if err != nil && !fieldpath.IsNotFound(err) { 225 | return err 226 | } 227 | p[i].val = val 228 | if val == nil { 229 | continue 230 | } 231 | vt := reflect.TypeOf(val) 232 | switch { 233 | case t == nil: 234 | t = vt 235 | case t != vt: 236 | return errors.Errorf("cannot sort values of different types %q and %q", t, vt) 237 | } 238 | } 239 | if t == nil { 240 | // we either have no values or all values are nil, we can just return 241 | return nil 242 | } 243 | 244 | var err error 245 | sort.Slice(p, func(i, j int) bool { 246 | vali, valj := p[i].val, p[j].val 247 | if vali == nil { 248 | vali = reflect.Zero(t).Interface() 249 | } 250 | if valj == nil { 251 | valj = reflect.Zero(t).Interface() 252 | } 253 | switch t.Kind() { //nolint:exhaustive // we only support these types 254 | case reflect.Float64: 255 | return vali.(float64) < valj.(float64) 256 | case reflect.Float32: 257 | return vali.(float32) < valj.(float32) 258 | case reflect.Int64: 259 | return vali.(int64) < valj.(int64) 260 | case reflect.Int32: 261 | return vali.(int32) < valj.(int32) 262 | case reflect.Int16: 263 | return vali.(int16) < valj.(int16) 264 | case reflect.Int8: 265 | return vali.(int8) < valj.(int8) 266 | case reflect.Int: 267 | return vali.(int) < valj.(int) 268 | case reflect.String: 269 | return vali.(string) < valj.(string) 270 | default: 271 | // should never happen 272 | err = errors.Errorf("unsupported type %q for sorting", t) 273 | return false 274 | } 275 | }) 276 | if err != nil { 277 | return err 278 | } 279 | 280 | for i := 0; i < len(extras); i++ { 281 | extras[i] = p[i].ec 282 | } 283 | return nil 284 | } 285 | 286 | func buildRequirements(in *v1beta1.Input, xr *resource.Composite) (*fnv1.Requirements, error) { 287 | extraResources := make(map[string]*fnv1.ResourceSelector, len(in.Spec.EnvironmentConfigs)) 288 | for i, config := range in.Spec.EnvironmentConfigs { 289 | extraResName := fmt.Sprintf("environment-config-%d", i) 290 | switch config.Type { 291 | case v1beta1.EnvironmentSourceTypeReference, "": 292 | extraResources[extraResName] = &fnv1.ResourceSelector{ 293 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 294 | Kind: "EnvironmentConfig", 295 | Match: &fnv1.ResourceSelector_MatchName{ 296 | MatchName: config.Ref.Name, 297 | }, 298 | } 299 | case v1beta1.EnvironmentSourceTypeSelector: 300 | matchLabels := map[string]string{} 301 | for _, selector := range config.Selector.MatchLabels { 302 | switch selector.GetType() { 303 | case v1beta1.EnvironmentSourceSelectorLabelMatcherTypeValue: 304 | // TODO validate value not to be nil 305 | matchLabels[selector.Key] = *selector.Value 306 | case v1beta1.EnvironmentSourceSelectorLabelMatcherTypeFromCompositeFieldPath: 307 | value, err := fieldpath.Pave(xr.Resource.Object).GetString(*selector.ValueFromFieldPath) 308 | if err != nil { 309 | if !selector.FromFieldPathIsOptional() { 310 | return nil, errors.Wrapf(err, "cannot get value from field path %q", *selector.ValueFromFieldPath) 311 | } 312 | continue 313 | } 314 | matchLabels[selector.Key] = value 315 | } 316 | } 317 | if len(matchLabels) == 0 { 318 | continue 319 | } 320 | extraResources[extraResName] = &fnv1.ResourceSelector{ 321 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 322 | Kind: "EnvironmentConfig", 323 | Match: &fnv1.ResourceSelector_MatchLabels{ 324 | MatchLabels: &fnv1.MatchLabels{Labels: matchLabels}, 325 | }, 326 | } 327 | } 328 | } 329 | return &fnv1.Requirements{ExtraResources: extraResources}, nil 330 | } 331 | 332 | func mergeEnvConfigsData(configs []unstructured.Unstructured) (map[string]interface{}, error) { 333 | merged := map[string]interface{}{} 334 | for _, c := range configs { 335 | data := map[string]interface{}{} 336 | if err := fieldpath.Pave(c.Object).GetValueInto("data", &data); err != nil { 337 | return nil, errors.Wrapf(err, "cannot get data from environment config %q", c.GetName()) 338 | } 339 | 340 | merged = mergeMaps(merged, data) 341 | } 342 | return merged, nil 343 | } 344 | 345 | func mergeMaps(a, b map[string]interface{}) map[string]interface{} { 346 | out := make(map[string]interface{}, len(a)) 347 | for k, v := range a { 348 | out[k] = v 349 | } 350 | for k, v := range b { 351 | if v, ok := v.(map[string]interface{}); ok { 352 | if bv, ok := out[k]; ok { 353 | if bv, ok := bv.(map[string]interface{}); ok { 354 | out[k] = mergeMaps(bv, v) 355 | continue 356 | } 357 | } 358 | } 359 | out[k] = v 360 | } 361 | return out 362 | } 363 | 364 | func unmarshalData(data map[string]extv1.JSON) (map[string]interface{}, error) { 365 | res := map[string]interface{}{} 366 | raw, err := json.Marshal(data) 367 | if err != nil { 368 | return nil, err 369 | } 370 | if err := json.Unmarshal(raw, &res); err != nil { 371 | return nil, err 372 | } 373 | return res, nil 374 | } 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # function-environment-configs 2 | 3 | [![CI](https://github.com/crossplane-contrib/function-environment-configs/actions/workflows/ci.yml/badge.svg)](https://github.com/crossplane-contrib/function-environment-configs/actions/workflows/ci.yml) ![GitHub release (latest SemVer)](https://img.shields.io/github/release/crossplane-contrib/function-environment-configs) 4 | 5 | > [!WARNING] 6 | > This function relies on functionalities only available in Crossplane 1.15 and 7 | > later. It will not work with earlier versions. 8 | 9 | > [!IMPORTANT] 10 | > This function is meant to replace native 11 | > [Composition Environment][upstream-docs-environment-configs] 12 | > (`--enable-environment-configs`), see 13 | > [below](#migration-from-native-composition-environment) for more details. 14 | 15 | This [composition function][docs-functions] allows you to request 16 | `EnvironmentConfigs`, merge them in the requested order and return inject the 17 | computed `environment` into the `Context` at a well-known key, 18 | `apiextensions.crossplane.io/environment`, so that other functions such as 19 | [function-patch-and-transform] can access it. 20 | 21 | ## Using this function 22 | 23 | See the [example](example) directory for examples that you can run locally using 24 | the Crossplane CLI: 25 | 26 | ```shell 27 | $ crossplane beta render \ 28 | --extra-resources example/environmentConfigs.yaml \ 29 | --include-context \ 30 | example/xr.yaml example/composition.yaml example/functions.yaml 31 | ``` 32 | 33 | To validate the rendered output, you can use the `crossplane beta validate` command: 34 | ```shell 35 | $ crossplane beta render \ 36 | --extra-resources example/environmentConfigs.yaml \ 37 | --include-full-xr \ 38 | example/xr.yaml example/composition.yaml example/functions.yaml | crossplane beta validate example - 39 | ``` 40 | 41 | See the [composition functions documentation][docs-functions] to learn more 42 | about `crossplane beta render`. 43 | 44 | ## Migration from native Composition Environment 45 | 46 | Crossplane 1.18 [dropped] native Composition Environment, meaning 47 | `spec.environment` and `*Environment` patches were removed, while 48 | `EnvironmentConfig` as a resource was promoted to `Beta`. 49 | 50 | `crossplane beta convert pipeline-composition` has been updated to 51 | automatically migrate Compositions using those fields to this function. 52 | 53 | A manual migration can be performed moving the following fields from a 54 | `Composition` spec to this function's `Input`: 55 | - `spec.environment.environmentConfigs` -> `spec.environmentConfigs` 56 | - `spec.environment.defaultData` -> `spec.defaultData` 57 | - `spec.environment.policy.resolution` -> `spec.policy.resolution` 58 | 59 | `spec.environment.policy.resolve` is not configurable at the moment, defaulting 60 | to policy `Always`. 61 | 62 | `spec.environment.patches` and resources' `*Environment` patches will have to 63 | be moved to [function-patch-and-transform]'s input. 64 | 65 | The diagram below shows what part of the usual Composition is replaced by this 66 | function and how it fits with other functions: 67 | 68 | ![diagram.png](diagram.png) 69 | 70 | ### Migration Example 71 | 72 | Given an example `Resource`-mode Composition: 73 | 74 | > [!WARNING] 75 | > The example below is using the old native Composition Environment, which was dropped in Crossplane 1.18. 76 | 77 |
Old Resource-mode Composition 78 |

79 | 80 | ```yaml 81 | apiVersion: apiextensions.crossplane.io/v1 82 | kind: Composition 83 | metadata: 84 | name: foo 85 | spec: 86 | compositeTypeRef: 87 | apiVersion: example.crossplane.io/v1 88 | kind: XR 89 | mode: Resources 90 | environment: 91 | environmentConfigs: 92 | - type: Reference 93 | ref: 94 | name: example-config 95 | patches: 96 | # So you can then use it in all your patches: 97 | # - Env -> XR 98 | - type: ToCompositeFieldPath 99 | fromFieldPath: "someFieldInTheEnvironment" 100 | toFieldPath: "status.someFieldFromTheEnvironment" 101 | # - XR -> Env 102 | - type: FromCompositeFieldPath 103 | fromFieldPath: "spec.someFieldInTheXR" 104 | toFieldPath: "someFieldFromTheXR" 105 | resources: 106 | - name: bucket 107 | base: 108 | apiVersion: s3.aws.upbound.io/v1beta1 109 | kind: Bucket 110 | spec: 111 | forProvider: 112 | region: us-east-2 113 | patches: 114 | # - Env -> Resource 115 | - type: FromEnvironmentFieldPath 116 | fromFieldPath: "someFieldInTheEnvironment" 117 | toFieldPath: "spec.forProvider.someFieldFromTheEnvironment" 118 | # - Resource -> Env 119 | - type: ToEnvironmentFieldPath 120 | fromFieldPath: "status.someOtherFieldInTheResource" 121 | toFieldPath: "someOtherFieldInTheEnvironment" 122 | 123 | ``` 124 | 125 |

126 |
127 | 128 | The above can be converted to use this function and 129 | [function-patch-and-transform] as follows: 130 | 131 | ```yaml 132 | apiVersion: apiextensions.crossplane.io/v1 133 | kind: Composition 134 | metadata: 135 | name: foo 136 | spec: 137 | compositeTypeRef: 138 | apiVersion: example.crossplane.io/v1 139 | kind: XR 140 | mode: Pipeline 141 | pipeline: 142 | - step: environmentConfigs 143 | functionRef: 144 | name: function-environment-configs 145 | input: 146 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 147 | kind: Input 148 | spec: 149 | environmentConfigs: 150 | - type: Reference 151 | ref: 152 | name: example-config 153 | # the environment is be passed to the next function in the pipeline 154 | # as part of the context 155 | - step: patch-and-transform 156 | # function-patch-and-transform knows it has to look for the environment in the 157 | # context at "apiextensions.crossplane.io/environment" 158 | functionRef: 159 | name: function-patch-and-transform 160 | input: 161 | apiVersion: pt.fn.crossplane.io/v1beta1 162 | kind: Resources 163 | environment: 164 | patches: 165 | # So you can then use it in all your patches: 166 | # - Env -> XR 167 | - type: ToCompositeFieldPath 168 | fromFieldPath: "someFieldInTheEnvironment" 169 | toFieldPath: "status.someFieldFromTheEnvironment" 170 | # - XR -> Env 171 | - type: FromCompositeFieldPath 172 | fromFieldPath: "spec.someFieldInTheXR" 173 | toFieldPath: "someFieldFromTheXR" 174 | resources: 175 | - name: bucket 176 | base: 177 | apiVersion: s3.aws.upbound.io/v1beta1 178 | kind: Bucket 179 | spec: 180 | forProvider: 181 | region: us-east-2 182 | patches: 183 | # - Env -> Resource 184 | - type: FromEnvironmentFieldPath 185 | fromFieldPath: "someFieldInTheEnvironment" 186 | toFieldPath: "spec.forProvider.someFieldFromTheEnvironment" 187 | # - Resource -> Env 188 | - type: ToEnvironmentFieldPath 189 | fromFieldPath: "status.someOtherFieldInTheResource" 190 | toFieldPath: "someOtherFieldInTheEnvironment" 191 | ``` 192 | 193 | ## Consuming environment from Context-aware functions 194 | 195 | This function just merges selected `EnvironmentConfigs` into the `Context` at a well-known key, `apiextensions.crossplane.io/environment`, therefore any `Context`-aware function can access it. 196 | 197 | For example, using [function-go-templating]: 198 | 199 | ```yaml 200 | apiVersion: apiextensions.crossplane.io/v1 201 | kind: Composition 202 | metadata: 203 | name: foo 204 | spec: 205 | compositeTypeRef: 206 | apiVersion: example.crossplane.io/v1 207 | kind: XR 208 | mode: Pipeline 209 | pipeline: 210 | - step: environmentConfigs 211 | functionRef: 212 | name: function-environment-configs 213 | input: 214 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 215 | kind: Input 216 | spec: 217 | environmentConfigs: 218 | - type: Reference 219 | ref: 220 | name: example-config 221 | # the environment is be passed to the next function in the pipeline 222 | # as part of the context 223 | - step: go-templating 224 | functionRef: 225 | name: function-go-templating 226 | input: 227 | apiVersion: gotemplating.fn.crossplane.io/v1beta1 228 | kind: GoTemplate 229 | source: Inline 230 | inline: 231 | template: | 232 | --- 233 | apiVersion: example.crossplane.io/v1 234 | kind: XR 235 | status: 236 | fromEnv: {{ index .context "apiextensions.crossplane.io/environment" "complex" "c" "d" }} 237 | ``` 238 | 239 | ### Environment Configuration Resource Sourcing 240 | 241 | #### Type Reference 242 | Source type `Reference` selects an environment configuration with the exact matching name. 243 | 244 | ```yaml 245 | < composition code removed for brevity > 246 | - step: environmentConfigs 247 | functionRef: 248 | name: function-environment-configs 249 | input: 250 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 251 | kind: Input 252 | spec: 253 | environmentConfigs: 254 | - type: Reference 255 | ref: 256 | name: example-config 257 | < composition code removed for brevity > 258 | ``` 259 | 260 | #### Type Selector 261 | Source type `Selector` selects an environment configuration with the exact matching name. Two selection modes are available; `Single` and `Multiple`. `Single` mode selects a single `environmentConfig` resource based on the labels of the `environmentConfig` and `Multiple` selects multiple `environmentConfig` resources. Mode will default to `Single` if omitted. 262 | 263 | Each label matcher accepts two types of matching; `Value` and `ValueFromFieldPath`. Type `Value` accepts a literal key/value to match and type `FromCompositeFieldPath` accepts a field input from the composite resource. 264 | 265 | 266 | Type `Selector` also supports inputs from the composite resource. 267 | ###### Single Mode 268 | ```yaml 269 | < composition code removed for brevity > 270 | - step: environmentConfigs 271 | functionRef: 272 | name: function-environment-configs 273 | input: 274 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 275 | kind: Input 276 | spec: 277 | environmentConfigs: 278 | - type: Selector 279 | selector: 280 | mode: Single 281 | matchLabels: 282 | # type 'Value' 283 | - type: Value 284 | # String literal of the label key to match 285 | key: example-label-a-key 286 | # String literal of the label value to match 287 | value: example-label-a-value 288 | - type: Selector 289 | selector: 290 | mode: Single 291 | matchLabels: 292 | # type 'FromCompositeFieldPath' 293 | # 'type' will default to 'FromCompositeFieldPath' if omitted. 294 | - type: FromCompositeFieldPath 295 | key: example-label-b-key 296 | # valueFromFieldPath expects the field path to get the key's value from within the composite resource. 297 | valueFromFieldPath: spec.example.b 298 | # FromFieldPathPolicy accepts values of 'Required' or 'Optional'. It defaults to 'Required' if omitted. 299 | # If set to 'Required', the function will throw an error if the field is not found in the field of the composite resource. 300 | # Instead if 'Optional' is set the label pair will be skipped if the field does not exist and other matchingLabels will be used if any others exist. 301 | FromFieldPathPolicy: Required 302 | < composition code removed for brevity > 303 | ``` 304 | 305 | ###### Multiple Mode 306 | Multiple mode differs from Single mode in that it will match several `environmentConfig` resources instead of a single `environmentConfig` resource. It accepts all of the same arguments as `Single` mode along with a few specific fields only available in `Multiple` mode. 307 | 308 | ```yaml 309 | < composition code removed for brevity > 310 | - step: environmentConfigs 311 | functionRef: 312 | name: function-environment-configs 313 | input: 314 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 315 | kind: Input 316 | spec: 317 | environmentConfigs: 318 | - type: Selector 319 | selector: 320 | mode: Multiple 321 | matchLabels: 322 | # type 'Value' 323 | - type: Value 324 | # String literal of the label key to match 325 | key: example-label-a-key 326 | # String literal of the label value to match 327 | value: example-label-a-value 328 | # 'maxMatch' is an optional argument that sets the maximum number of environmentConfigs to pull. Pulls all if omitted. 329 | # Only supported in 'Multiple' mode 330 | maxMatch: 3 331 | # 'minMatch' is an optional argument that the required minimum number of environmentConfigs to pull. 332 | # Only supported in 'Multiple' mode 333 | minMatch: 1 334 | # 'sortByFieldPath' is an optional argument that sets the field path on how the pulled environmentConfigs is alphabetically sorted. 335 | # Only supported in 'Multiple' mode. Defaults to "metadata.name" if omitted. 336 | sortByFieldPath: metadata.name 337 | < composition code removed for brevity > 338 | ``` 339 | 340 | ### Default data 341 | ```yaml 342 | < removed for brevity > 343 | - step: environmentConfigs 344 | functionRef: 345 | name: function-environment-configs 346 | input: 347 | apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 348 | kind: Input 349 | spec: 350 | # Default data that can be overridden by EnvironmentConfigs being pulled 351 | defaultData: 352 | a: 353 | a1: example-a1 354 | a2: example-a2 355 | b: example-b 356 | environmentConfigs: 357 | - type: Reference 358 | ref: 359 | name: example-config 360 | < removed for brevity > 361 | ``` 362 | 363 | ## Developing this function 364 | 365 | This function uses [Go][go], [Docker][docker], and the [Crossplane CLI][cli] to 366 | build functions. 367 | 368 | ```shell 369 | # Run code generation - see input/generate.go 370 | $ go generate ./... 371 | 372 | # Run tests - see fn_test.go 373 | $ go test ./... 374 | 375 | # Build the function's runtime image - see Dockerfile 376 | $ docker build . --tag=runtime 377 | 378 | # Build a function package - see package/crossplane.yaml 379 | $ crossplane xpkg build -f package --embed-runtime-image=runtime 380 | ``` 381 | 382 | [bsr]: https://buf.build/crossplane/crossplane/docs/main:apiextensions.fn.proto.v1beta1#apiextensions.fn.proto.v1beta1.RunFunctionRequest 383 | [cli]: https://docs.crossplane.io/latest/cli 384 | [docker]: https://www.docker.com 385 | [docs-functions]: https://docs.crossplane.io/latest/concepts/compositions/ 386 | [dropped]: https://github.com/crossplane/crossplane/pull/5938 387 | [function-go-templating]: https://github.com/crossplane-contrib/function-go-templating 388 | [function-patch-and-transform]: https://github.com/crossplane-contrib/function-patch-and-transform 389 | [go]: https://go.dev 390 | [upstream-docs-environment-configs]: https://docs.crossplane.io/latest/concepts/environment-configs/ 391 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= 4 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 5 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 6 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 7 | github.com/alecthomas/kong v1.4.0 h1:UL7tzGMnnY0YRMMvJyITIRX1EpO6RbBRZDNcCevy3HA= 8 | github.com/alecthomas/kong v1.4.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 9 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 10 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 11 | github.com/antchfx/htmlquery v1.2.4 h1:qLteofCMe/KGovBI6SQgmou2QNyedFUW+pE+BpeZ494= 12 | github.com/antchfx/htmlquery v1.2.4/go.mod h1:2xO6iu3EVWs7R2JYqBbp8YzG50gj/ofqs5/0VZoDZLc= 13 | github.com/antchfx/xpath v1.2.0 h1:mbwv7co+x0RwgeGAOHdrKy89GvHaGvxxBtPK0uF9Zr8= 14 | github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= 15 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 16 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 17 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 18 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 19 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 20 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 21 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 22 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 24 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 25 | github.com/crossplane/crossplane-runtime v1.18.0 h1:aAQIMNOgPbbXaqj9CUSv+gPl3QnVbn33YlzSe145//0= 26 | github.com/crossplane/crossplane-runtime v1.18.0/go.mod h1:p7nVVsLn0CWjsLvLCtr7T40ErbTgNWKRxmYnwFdfXb4= 27 | github.com/crossplane/function-sdk-go v0.4.0 h1:1jd+UIaZlVNQCUO4hLAgUqWBRnUKw2ObF9ZuMw5CpKk= 28 | github.com/crossplane/function-sdk-go v0.4.0/go.mod h1:jLnzUG8pt8tn/U6/uvtNStAhDjhIq4wCR31yECT54NM= 29 | github.com/crossplane/upjet v1.4.1-0.20240911184956-3afbb7796d46 h1:2IH1YPTBrNmBj0Z1OCjEBTrQCuRaLutZbWLaswFeCFQ= 30 | github.com/crossplane/upjet v1.4.1-0.20240911184956-3afbb7796d46/go.mod h1:wkdZf/Cvhr6PI30VdHIOjg4dX39Z5uijqnLWFk5PbGM= 31 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 36 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 37 | github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= 38 | github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 39 | github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= 40 | github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 41 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 42 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 43 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 44 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 45 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 46 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 47 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 48 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 49 | github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 h1:xcuWappghOVI8iNWoF2OKahVejd1LSVi/v4JED44Amo= 50 | github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= 51 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 52 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 53 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 54 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 55 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 56 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 57 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 58 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 59 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 60 | github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= 61 | github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 62 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 63 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 64 | github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= 65 | github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= 66 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 67 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 68 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 69 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 70 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 71 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 72 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 73 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 74 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 75 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 76 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 77 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 78 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 79 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 80 | github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= 81 | github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 82 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 83 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 84 | github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 h1:Ud/6/AdmJ1R7ibdS0Wo5MWPj0T1R0fkpaD087bBaW8I= 85 | github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= 86 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 87 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 88 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 89 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 90 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 91 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 92 | github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= 93 | github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= 94 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= 95 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 96 | github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= 97 | github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= 98 | github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc= 99 | github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= 100 | github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= 101 | github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= 102 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= 103 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 104 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= 105 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= 106 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 107 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 108 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 109 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 110 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 111 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 112 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 113 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 114 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 115 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 116 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 117 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 118 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 119 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 120 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 121 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 122 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 123 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 124 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 125 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 126 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 127 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 128 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 129 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 130 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 131 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 132 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 133 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 134 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 135 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 136 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 137 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 138 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 139 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 140 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 141 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 142 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 143 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 144 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 145 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 146 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 147 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 148 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 149 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 150 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 151 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 152 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 153 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 154 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 155 | github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= 156 | github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= 157 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 158 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 159 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 160 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 161 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 162 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 163 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 164 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 165 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 166 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 167 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 168 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 169 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 170 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 171 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 172 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 173 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 174 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 175 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 176 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 177 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 178 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 179 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 180 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 181 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 182 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 183 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 184 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 185 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 186 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 187 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 188 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 189 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 190 | github.com/tmccombs/hcl2json v0.3.3 h1:+DLNYqpWE0CsOQiEZu+OZm5ZBImake3wtITYxQ8uLFQ= 191 | github.com/tmccombs/hcl2json v0.3.3/go.mod h1:Y2chtz2x9bAeRTvSibVRVgbLJhLJXKlUeIvjeVdnm4w= 192 | github.com/upbound/provider-aws v1.14.0 h1:DDUdlMp+dNlFXXlhsGdCvQD7qFdT1AsEcaqlRU3BO14= 193 | github.com/upbound/provider-aws v1.14.0/go.mod h1:IvyvgGlhRVr737E4P75tyD/i53hxnyO7KPM8bbXH+SU= 194 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 195 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 196 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 197 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 198 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 199 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 200 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 201 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 202 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 203 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 204 | github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= 205 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 206 | github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= 207 | github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 208 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 209 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 210 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 211 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 212 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 213 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 214 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 215 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 216 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 217 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= 218 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 219 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 220 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 221 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 222 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 223 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 224 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 225 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 226 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 227 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 228 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 229 | golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= 230 | golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 231 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 232 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 233 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 234 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 235 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 236 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 237 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 240 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 241 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 242 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 243 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 244 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 245 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 246 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 247 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 248 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 249 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 250 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 251 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 252 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 253 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 254 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 255 | golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= 256 | golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= 257 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 258 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 259 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 260 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 261 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 262 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 263 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 264 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 265 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= 266 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 267 | google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= 268 | google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 269 | google.golang.org/protobuf v1.34.3-0.20240816073751-94ecbc261689 h1:hNwajDgT0MlsxZzlUajZVmUYFpts8/CYe4BSNx503ZE= 270 | google.golang.org/protobuf v1.34.3-0.20240816073751-94ecbc261689/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 271 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 272 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 273 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 274 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 275 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 276 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 277 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 278 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 279 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 280 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 281 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 282 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 283 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 284 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 285 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 286 | k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= 287 | k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= 288 | k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= 289 | k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= 290 | k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= 291 | k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= 292 | k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= 293 | k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= 294 | k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs= 295 | k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo= 296 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 297 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 298 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= 299 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= 300 | k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= 301 | k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 302 | sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= 303 | sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= 304 | sigs.k8s.io/controller-tools v0.16.0 h1:EJPB+a5Bve861SPBPPWRbP6bbKyNxqK12oYT5zEns9s= 305 | sigs.k8s.io/controller-tools v0.16.0/go.mod h1:0I0xqjR65YTfoO12iR+mZR6s6UAVcUARgXRlsu0ljB0= 306 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 307 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 308 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 309 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 310 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 311 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 312 | -------------------------------------------------------------------------------- /fn_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/google/go-cmp/cmp/cmpopts" 9 | "google.golang.org/protobuf/encoding/protojson" 10 | "google.golang.org/protobuf/types/known/durationpb" 11 | "google.golang.org/protobuf/types/known/structpb" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/utils/ptr" 14 | 15 | "github.com/crossplane/crossplane-runtime/pkg/fieldpath" 16 | "github.com/crossplane/crossplane-runtime/pkg/logging" 17 | fnv1 "github.com/crossplane/function-sdk-go/proto/v1" 18 | "github.com/crossplane/function-sdk-go/resource" 19 | "github.com/crossplane/function-sdk-go/response" 20 | ) 21 | 22 | func TestRunFunction(t *testing.T) { 23 | 24 | type args struct { 25 | ctx context.Context 26 | req *fnv1.RunFunctionRequest 27 | } 28 | type want struct { 29 | rsp *fnv1.RunFunctionResponse 30 | err error 31 | } 32 | 33 | cases := map[string]struct { 34 | reason string 35 | args args 36 | want want 37 | }{ 38 | "RequestEnvironmentConfigs": { 39 | reason: "The Function should request the necessary EnvironmentConfigs", 40 | args: args{ 41 | req: &fnv1.RunFunctionRequest{ 42 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 43 | Observed: &fnv1.State{ 44 | Composite: &fnv1.Resource{ 45 | Resource: resource.MustStructJSON(`{ 46 | "apiVersion": "test.crossplane.io/v1alpha1", 47 | "kind": "XR", 48 | "metadata": { 49 | "name": "my-xr" 50 | }, 51 | "spec": { 52 | "existingEnvSelectorLabel": "someMoreBar" 53 | } 54 | }`), 55 | }, 56 | }, 57 | Input: resource.MustStructJSON(`{ 58 | "apiVersion": "template.fn.crossplane.io/v1beta1", 59 | "kind": "Input", 60 | "spec": { 61 | "environmentConfigs": [ 62 | { 63 | "type": "Reference", 64 | "ref": { 65 | "name": "my-env-config" 66 | } 67 | }, 68 | { 69 | "type": "Reference", 70 | "ref": { 71 | "name": "my-second-env-config" 72 | } 73 | }, 74 | { 75 | "type": "Selector", 76 | "selector": { 77 | "mode": "Multiple", 78 | "matchLabels": [ 79 | { 80 | "type": "Value", 81 | "key": "foo", 82 | "value": "bar" 83 | } 84 | ] 85 | } 86 | }, 87 | { 88 | "type": "Selector", 89 | "selector": { 90 | "mode": "Single", 91 | "matchLabels": [ 92 | { 93 | "key": "someMoreFoo", 94 | "valueFromFieldPath": "spec.missingEnvSelectorLabel", 95 | "fromFieldPathPolicy": "Optional" 96 | } 97 | ] 98 | } 99 | }, 100 | { 101 | "type": "Selector", 102 | "selector": { 103 | "mode": "Single", 104 | "matchLabels": [ 105 | { 106 | "key": "someMoreFoo", 107 | "valueFromFieldPath": "spec.existingEnvSelectorLabel", 108 | "fromFieldPathPolicy": "Required" 109 | } 110 | ] 111 | } 112 | } 113 | ] 114 | } 115 | }`), 116 | }, 117 | }, 118 | want: want{ 119 | rsp: &fnv1.RunFunctionResponse{ 120 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 121 | Results: []*fnv1.Result{}, 122 | Requirements: &fnv1.Requirements{ 123 | ExtraResources: map[string]*fnv1.ResourceSelector{ 124 | "environment-config-0": { 125 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 126 | Kind: "EnvironmentConfig", 127 | Match: &fnv1.ResourceSelector_MatchName{ 128 | MatchName: "my-env-config", 129 | }, 130 | }, 131 | "environment-config-1": { 132 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 133 | Kind: "EnvironmentConfig", 134 | Match: &fnv1.ResourceSelector_MatchName{ 135 | MatchName: "my-second-env-config", 136 | }, 137 | }, 138 | "environment-config-2": { 139 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 140 | Kind: "EnvironmentConfig", 141 | Match: &fnv1.ResourceSelector_MatchLabels{ 142 | MatchLabels: &fnv1.MatchLabels{ 143 | Labels: map[string]string{ 144 | "foo": "bar", 145 | }, 146 | }, 147 | }, 148 | }, 149 | // environment-config-3 is not requested because it was optional 150 | "environment-config-4": { 151 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 152 | Kind: "EnvironmentConfig", 153 | Match: &fnv1.ResourceSelector_MatchLabels{ 154 | MatchLabels: &fnv1.MatchLabels{ 155 | Labels: map[string]string{ 156 | "someMoreFoo": "someMoreBar", 157 | }, 158 | }, 159 | }, 160 | }, 161 | }, 162 | }, 163 | }, 164 | }, 165 | }, 166 | "RequestEnvironmentConfigsFound": { 167 | reason: "The Function should request the necessary EnvironmentConfigs even if they are already present in the request", 168 | args: args{ 169 | req: &fnv1.RunFunctionRequest{ 170 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 171 | Observed: &fnv1.State{ 172 | Composite: &fnv1.Resource{ 173 | Resource: resource.MustStructJSON(`{ 174 | "apiVersion": "test.crossplane.io/v1alpha1", 175 | "kind": "XR", 176 | "metadata": { 177 | "name": "my-xr" 178 | }, 179 | "spec": { 180 | "existingEnvSelectorLabel": "someMoreBar" 181 | } 182 | }`), 183 | }, 184 | }, 185 | ExtraResources: map[string]*fnv1.Resources{ 186 | "environment-config-0": { 187 | Items: []*fnv1.Resource{ 188 | { 189 | Resource: resource.MustStructJSON(`{ 190 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 191 | "kind": "EnvironmentConfig", 192 | "metadata": { 193 | "name": "my-env-config" 194 | }, 195 | "data": { 196 | "firstKey": "firstVal", 197 | "secondKey": "secondVal" 198 | } 199 | }`), 200 | }, 201 | }, 202 | }, 203 | "environment-config-1": { 204 | Items: []*fnv1.Resource{ 205 | { 206 | Resource: resource.MustStructJSON(`{ 207 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 208 | "kind": "EnvironmentConfig", 209 | "metadata": { 210 | "name": "my-second-env-config" 211 | }, 212 | "data": { 213 | "secondKey": "secondVal-ok", 214 | "thirdKey": "thirdVal" 215 | } 216 | }`), 217 | }, 218 | }, 219 | }, 220 | "environment-config-2": { 221 | Items: []*fnv1.Resource{ 222 | { 223 | Resource: resource.MustStructJSON(`{ 224 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 225 | "kind": "EnvironmentConfig", 226 | "metadata": { 227 | "name": "my-third-env-config-b" 228 | }, 229 | "data": { 230 | "fourthKey": "fourthVal-b" 231 | } 232 | }`), 233 | }, 234 | { 235 | Resource: resource.MustStructJSON(`{ 236 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 237 | "kind": "EnvironmentConfig", 238 | "metadata": { 239 | "name": "my-third-env-config-a" 240 | }, 241 | "data": { 242 | "fourthKey": "fourthVal-a" 243 | } 244 | }`), 245 | }, 246 | }, 247 | }, 248 | "environment-config-3": { 249 | Items: []*fnv1.Resource{ 250 | { 251 | Resource: resource.MustStructJSON(`{ 252 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 253 | "kind": "EnvironmentConfig", 254 | "metadata": { 255 | "name": "my-third-env-config" 256 | }, 257 | "data": { 258 | "fifthKey": "fifthVal" 259 | } 260 | }`), 261 | }, 262 | }, 263 | }, 264 | "environment-config-4": { 265 | Items: []*fnv1.Resource{ 266 | { 267 | Resource: resource.MustStructJSON(`{ 268 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 269 | "kind": "EnvironmentConfig", 270 | "metadata": { 271 | "name": "my-fourth-env-config" 272 | }, 273 | "data": { 274 | "sixthKey": "sixthVal" 275 | } 276 | }`), 277 | }, 278 | }, 279 | }, 280 | }, 281 | Input: resource.MustStructJSON(`{ 282 | "apiVersion": "template.fn.crossplane.io/v1beta1", 283 | "kind": "Input", 284 | "spec": { 285 | "environmentConfigs": [ 286 | { 287 | "type": "Reference", 288 | "ref": { 289 | "name": "my-env-config" 290 | } 291 | }, 292 | { 293 | "type": "Reference", 294 | "ref": { 295 | "name": "my-second-env-config" 296 | } 297 | }, 298 | { 299 | "type": "Selector", 300 | "selector": { 301 | "mode": "Multiple", 302 | "matchLabels": [ 303 | { 304 | "type": "Value", 305 | "key": "foo", 306 | "value": "bar" 307 | } 308 | ] 309 | } 310 | }, 311 | { 312 | "type": "Selector", 313 | "selector": { 314 | "mode": "Single", 315 | "matchLabels": [ 316 | { 317 | "key": "someMoreFoo", 318 | "valueFromFieldPath": "spec.missingEnvSelectorLabel", 319 | "fromFieldPathPolicy": "Optional" 320 | } 321 | ] 322 | } 323 | }, 324 | { 325 | "type": "Selector", 326 | "selector": { 327 | "mode": "Single", 328 | "matchLabels": [ 329 | { 330 | "key": "someMoreFoo", 331 | "valueFromFieldPath": "spec.existingEnvSelectorLabel", 332 | "fromFieldPathPolicy": "Required" 333 | } 334 | ] 335 | } 336 | } 337 | ] 338 | } 339 | }`), 340 | }, 341 | }, 342 | want: want{ 343 | rsp: &fnv1.RunFunctionResponse{ 344 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 345 | Results: []*fnv1.Result{}, 346 | Requirements: &fnv1.Requirements{ 347 | ExtraResources: map[string]*fnv1.ResourceSelector{ 348 | "environment-config-0": { 349 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 350 | Kind: "EnvironmentConfig", 351 | Match: &fnv1.ResourceSelector_MatchName{ 352 | MatchName: "my-env-config", 353 | }, 354 | }, 355 | "environment-config-1": { 356 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 357 | Kind: "EnvironmentConfig", 358 | Match: &fnv1.ResourceSelector_MatchName{ 359 | MatchName: "my-second-env-config", 360 | }, 361 | }, 362 | "environment-config-2": { 363 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 364 | Kind: "EnvironmentConfig", 365 | Match: &fnv1.ResourceSelector_MatchLabels{ 366 | MatchLabels: &fnv1.MatchLabels{ 367 | Labels: map[string]string{ 368 | "foo": "bar", 369 | }, 370 | }, 371 | }, 372 | }, 373 | // environment-config-3 is not requested because it was optional 374 | "environment-config-4": { 375 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 376 | Kind: "EnvironmentConfig", 377 | Match: &fnv1.ResourceSelector_MatchLabels{ 378 | MatchLabels: &fnv1.MatchLabels{ 379 | Labels: map[string]string{ 380 | "someMoreFoo": "someMoreBar", 381 | }, 382 | }, 383 | }, 384 | }, 385 | }, 386 | }, 387 | Context: &structpb.Struct{ 388 | Fields: map[string]*structpb.Value{ 389 | FunctionContextKeyEnvironment: structpb.NewStructValue(resource.MustStructJSON(`{ 390 | "apiVersion": "internal.crossplane.io/v1alpha1", 391 | "kind": "Environment", 392 | "firstKey": "firstVal", 393 | "secondKey": "secondVal-ok", 394 | "thirdKey": "thirdVal", 395 | "fourthKey": "fourthVal-b", 396 | "fifthKey": "fifthVal", 397 | "sixthKey": "sixthVal" 398 | }`)), 399 | }, 400 | }, 401 | }, 402 | }, 403 | }, 404 | "RequestEnvironmentConfigsNotFoundRequired": { 405 | reason: "The Function should return fatal if a required EnvironmentConfig is not found", 406 | args: args{ 407 | req: &fnv1.RunFunctionRequest{ 408 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 409 | Observed: &fnv1.State{ 410 | Composite: &fnv1.Resource{ 411 | Resource: resource.MustStructJSON(`{ 412 | "apiVersion": "test.crossplane.io/v1alpha1", 413 | "kind": "XR", 414 | "metadata": { 415 | "name": "my-xr" 416 | } 417 | }`), 418 | }, 419 | }, 420 | ExtraResources: map[string]*fnv1.Resources{ 421 | "environment-config-0": { 422 | Items: []*fnv1.Resource{}, 423 | }, 424 | }, 425 | Input: resource.MustStructJSON(`{ 426 | "apiVersion": "template.fn.crossplane.io/v1beta1", 427 | "kind": "Input", 428 | "spec": { 429 | "environmentConfigs": [ 430 | { 431 | "type": "Reference", 432 | "ref": { 433 | "name": "my-env-config" 434 | } 435 | } 436 | ] 437 | } 438 | }`), 439 | }, 440 | }, 441 | want: want{ 442 | rsp: &fnv1.RunFunctionResponse{ 443 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 444 | Results: []*fnv1.Result{ 445 | { 446 | Severity: fnv1.Severity_SEVERITY_FATAL, 447 | Target: ptr.To(fnv1.Target_TARGET_COMPOSITE), 448 | }, 449 | }, 450 | Requirements: &fnv1.Requirements{ 451 | ExtraResources: map[string]*fnv1.ResourceSelector{ 452 | "environment-config-0": { 453 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 454 | Kind: "EnvironmentConfig", 455 | Match: &fnv1.ResourceSelector_MatchName{ 456 | MatchName: "my-env-config", 457 | }, 458 | }, 459 | }, 460 | }, 461 | }, 462 | }, 463 | }, 464 | "SelectorWithOptionalFieldPathNotProvided": { 465 | reason: "The Function should gracefully skip selectors with optional field paths when the environment config is not provided in extraResources", 466 | args: args{ 467 | req: &fnv1.RunFunctionRequest{ 468 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 469 | Observed: &fnv1.State{ 470 | Composite: &fnv1.Resource{ 471 | Resource: resource.MustStructJSON(`{ 472 | "apiVersion": "test.crossplane.io/v1alpha1", 473 | "kind": "XR", 474 | "metadata": { 475 | "name": "my-xr" 476 | }, 477 | "spec": { 478 | "presentField": "value" 479 | } 480 | }`), 481 | }, 482 | }, 483 | ExtraResources: map[string]*fnv1.Resources{ 484 | "environment-config-0": { 485 | Items: []*fnv1.Resource{ 486 | { 487 | Resource: resource.MustStructJSON(`{ 488 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 489 | "kind": "EnvironmentConfig", 490 | "metadata": { 491 | "name": "base-env-config" 492 | }, 493 | "data": { 494 | "baseKey": "baseVal" 495 | } 496 | }`), 497 | }, 498 | }, 499 | }, 500 | // environment-config-1 is NOT provided (optional field doesn't exist) 501 | }, 502 | Input: resource.MustStructJSON(`{ 503 | "apiVersion": "template.fn.crossplane.io/v1beta1", 504 | "kind": "Input", 505 | "spec": { 506 | "environmentConfigs": [ 507 | { 508 | "type": "Reference", 509 | "ref": { 510 | "name": "base-env-config" 511 | } 512 | }, 513 | { 514 | "type": "Selector", 515 | "selector": { 516 | "mode": "Multiple", 517 | "minMatch": 0, 518 | "maxMatch": 1, 519 | "matchLabels": [ 520 | { 521 | "key": "epd", 522 | "valueFromFieldPath": "spec.epd.name", 523 | "fromFieldPathPolicy": "Optional" 524 | } 525 | ] 526 | } 527 | } 528 | ] 529 | } 530 | }`), 531 | }, 532 | }, 533 | want: want{ 534 | rsp: &fnv1.RunFunctionResponse{ 535 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 536 | Results: []*fnv1.Result{}, 537 | Requirements: &fnv1.Requirements{ 538 | ExtraResources: map[string]*fnv1.ResourceSelector{ 539 | "environment-config-0": { 540 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 541 | Kind: "EnvironmentConfig", 542 | Match: &fnv1.ResourceSelector_MatchName{ 543 | MatchName: "base-env-config", 544 | }, 545 | }, 546 | // environment-config-1 is not in requirements because optional field doesn't exist 547 | }, 548 | }, 549 | Context: &structpb.Struct{ 550 | Fields: map[string]*structpb.Value{ 551 | FunctionContextKeyEnvironment: structpb.NewStructValue(resource.MustStructJSON(`{ 552 | "apiVersion": "internal.crossplane.io/v1alpha1", 553 | "kind": "Environment", 554 | "baseKey": "baseVal" 555 | }`)), 556 | }, 557 | }, 558 | }, 559 | }, 560 | }, 561 | "SelectorSingleModeWithOptionalFieldPathNotProvided": { 562 | reason: "Single mode should gracefully skip when optional field path doesn't exist (per documentation: 'if any others exist')", 563 | args: args{ 564 | req: &fnv1.RunFunctionRequest{ 565 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 566 | Observed: &fnv1.State{ 567 | Composite: &fnv1.Resource{ 568 | Resource: resource.MustStructJSON(`{ 569 | "apiVersion": "test.crossplane.io/v1alpha1", 570 | "kind": "XR", 571 | "metadata": { 572 | "name": "my-xr" 573 | }, 574 | "spec": { 575 | "presentField": "value" 576 | } 577 | }`), 578 | }, 579 | }, 580 | ExtraResources: map[string]*fnv1.Resources{ 581 | "environment-config-0": { 582 | Items: []*fnv1.Resource{ 583 | { 584 | Resource: resource.MustStructJSON(`{ 585 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 586 | "kind": "EnvironmentConfig", 587 | "metadata": { 588 | "name": "base-env-config" 589 | }, 590 | "data": { 591 | "baseKey": "baseVal" 592 | } 593 | }`), 594 | }, 595 | }, 596 | }, 597 | // environment-config-1 is NOT provided (optional field doesn't exist) 598 | }, 599 | Input: resource.MustStructJSON(`{ 600 | "apiVersion": "template.fn.crossplane.io/v1beta1", 601 | "kind": "Input", 602 | "spec": { 603 | "environmentConfigs": [ 604 | { 605 | "type": "Reference", 606 | "ref": { 607 | "name": "base-env-config" 608 | } 609 | }, 610 | { 611 | "type": "Selector", 612 | "selector": { 613 | "mode": "Single", 614 | "matchLabels": [ 615 | { 616 | "key": "epd", 617 | "valueFromFieldPath": "spec.epd.name", 618 | "fromFieldPathPolicy": "Optional" 619 | } 620 | ] 621 | } 622 | } 623 | ] 624 | } 625 | }`), 626 | }, 627 | }, 628 | want: want{ 629 | rsp: &fnv1.RunFunctionResponse{ 630 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 631 | Results: []*fnv1.Result{}, 632 | Requirements: &fnv1.Requirements{ 633 | ExtraResources: map[string]*fnv1.ResourceSelector{ 634 | "environment-config-0": { 635 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 636 | Kind: "EnvironmentConfig", 637 | Match: &fnv1.ResourceSelector_MatchName{ 638 | MatchName: "base-env-config", 639 | }, 640 | }, 641 | // environment-config-1 is not in requirements because optional field doesn't exist 642 | }, 643 | }, 644 | Context: &structpb.Struct{ 645 | Fields: map[string]*structpb.Value{ 646 | FunctionContextKeyEnvironment: structpb.NewStructValue(resource.MustStructJSON(`{ 647 | "apiVersion": "internal.crossplane.io/v1alpha1", 648 | "kind": "Environment", 649 | "baseKey": "baseVal" 650 | }`)), 651 | }, 652 | }, 653 | }, 654 | }, 655 | }, 656 | "MergeEnvironmentConfigs": { 657 | reason: "The Function should merge the provided EnvironmentConfigs", 658 | args: args{ 659 | req: &fnv1.RunFunctionRequest{ 660 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 661 | Context: resource.MustStructJSON(`{ 662 | "` + FunctionContextKeyEnvironment + `": { 663 | "apiVersion": "internal.crossplane.io/v1alpha1", 664 | "kind": "Environment", 665 | "a": "only-from-input", 666 | "e": "overridden-from-input-ok", 667 | "f": "overridden-from-env-config-1", 668 | "g": "overridden-from-env-config-2" 669 | } 670 | }`), 671 | Input: resource.MustStructJSON(`{ 672 | "apiVersion": "template.fn.crossplane.io/v1beta1", 673 | "kind": "Input", 674 | "spec": { 675 | "defaultData": { 676 | "b": "only-from-default", 677 | "e": "overridden-from-input", 678 | "f": "overridden-from-env-config-2" 679 | }, 680 | "environmentConfigs": [ 681 | { 682 | "type": "Reference", 683 | "ref": { 684 | "name": "my-env-config" 685 | } 686 | }, 687 | { 688 | "type": "Reference", 689 | "ref": { 690 | "name": "my-second-env-config" 691 | } 692 | } 693 | ] 694 | } 695 | }`), 696 | ExtraResources: map[string]*fnv1.Resources{ 697 | "environment-config-0": { 698 | Items: []*fnv1.Resource{ 699 | { 700 | Resource: resource.MustStructJSON(`{ 701 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 702 | "kind": "EnvironmentConfig", 703 | "metadata": { 704 | "name": "my-env-config" 705 | }, 706 | "data": { 707 | "c": "only-from-env-config-1", 708 | "f": "overridden-from-env-config-1-ok", 709 | "h": "override-from-env-config-1" 710 | } 711 | }`), 712 | }, 713 | }, 714 | }, 715 | "environment-config-1": { 716 | Items: []*fnv1.Resource{ 717 | { 718 | Resource: resource.MustStructJSON(`{ 719 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 720 | "kind": "EnvironmentConfig", 721 | "metadata": { 722 | "name": "my-second-env-config" 723 | }, 724 | "data": { 725 | "d": "only-from-env-config-1", 726 | "g": "overridden-from-env-config-2-ok", 727 | "h": "override-from-env-config-1-ok" 728 | } 729 | }`), 730 | }, 731 | }, 732 | }, 733 | }, 734 | }, 735 | }, 736 | want: want{ 737 | rsp: &fnv1.RunFunctionResponse{ 738 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 739 | Results: []*fnv1.Result{}, 740 | Requirements: &fnv1.Requirements{ 741 | ExtraResources: map[string]*fnv1.ResourceSelector{ 742 | "environment-config-0": { 743 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 744 | Kind: "EnvironmentConfig", 745 | Match: &fnv1.ResourceSelector_MatchName{ 746 | MatchName: "my-env-config", 747 | }, 748 | }, 749 | "environment-config-1": { 750 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 751 | Kind: "EnvironmentConfig", 752 | Match: &fnv1.ResourceSelector_MatchName{ 753 | MatchName: "my-second-env-config", 754 | }, 755 | }, 756 | }, 757 | }, 758 | Context: &structpb.Struct{ 759 | Fields: map[string]*structpb.Value{ 760 | FunctionContextKeyEnvironment: structpb.NewStructValue(resource.MustStructJSON(`{ 761 | "apiVersion": "internal.crossplane.io/v1alpha1", 762 | "kind": "Environment", 763 | "a": "only-from-input", 764 | "b": "only-from-default", 765 | "c": "only-from-env-config-1", 766 | "d": "only-from-env-config-1", 767 | "e": "overridden-from-input-ok", 768 | "f": "overridden-from-env-config-1-ok", 769 | "g": "overridden-from-env-config-2-ok", 770 | "h": "override-from-env-config-1-ok" 771 | }`)), 772 | }, 773 | }, 774 | }, 775 | }, 776 | }, 777 | } 778 | 779 | for name, tc := range cases { 780 | t.Run(name, func(t *testing.T) { 781 | f := &Function{log: logging.NewNopLogger()} 782 | rsp, err := f.RunFunction(tc.args.ctx, tc.args.req) 783 | 784 | diff := cmp.Diff(tc.want.rsp, rsp, cmpopts.AcyclicTransformer("toJsonWithoutResultMessages", func(r *fnv1.RunFunctionResponse) []byte { 785 | // We don't care about messages. 786 | // cmptopts.IgnoreField wasn't working with protocmp.Transform 787 | // We can't split this to another transformer as 788 | // transformers are applied not in order but as soon as they 789 | // match the type, which are walked from the root (RunFunctionResponse). 790 | for _, result := range r.GetResults() { 791 | result.Message = "" 792 | } 793 | out, err := protojson.Marshal(r) 794 | if err != nil { 795 | t.Fatalf("cannot marshal %T to JSON: %s", r, err) 796 | } 797 | return out 798 | })) 799 | if diff != "" { 800 | t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff) 801 | } 802 | 803 | if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { 804 | t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff) 805 | } 806 | }) 807 | } 808 | } 809 | 810 | func resourceWithFieldPathValue(path string, value any) resource.Extra { 811 | u := unstructured.Unstructured{ 812 | Object: map[string]interface{}{}, 813 | } 814 | err := fieldpath.Pave(u.Object).SetValue(path, value) 815 | if err != nil { 816 | panic(err) 817 | } 818 | return resource.Extra{ 819 | Resource: &u, 820 | } 821 | } 822 | 823 | func TestSortExtrasByFieldPath(t *testing.T) { 824 | type args struct { 825 | extras []resource.Extra 826 | path string 827 | } 828 | type want struct { 829 | extras []resource.Extra 830 | err error 831 | } 832 | 833 | cases := map[string]struct { 834 | reason string 835 | args args 836 | want want 837 | }{ 838 | "SortByString": { 839 | reason: "The Function should sort the Extras by the string value at the specified field path", 840 | args: args{ 841 | extras: []resource.Extra{ 842 | resourceWithFieldPathValue("metadata.name", "c"), 843 | resourceWithFieldPathValue("metadata.name", "a"), 844 | resourceWithFieldPathValue("metadata.name", "b"), 845 | }, 846 | path: "metadata.name", 847 | }, 848 | want: want{ 849 | extras: []resource.Extra{ 850 | resourceWithFieldPathValue("metadata.name", "a"), 851 | resourceWithFieldPathValue("metadata.name", "b"), 852 | resourceWithFieldPathValue("metadata.name", "c"), 853 | }, 854 | }, 855 | }, 856 | "SortByInt": { 857 | reason: "The Function should sort the Extras by the int value at the specified field path", 858 | args: args{ 859 | extras: []resource.Extra{ 860 | resourceWithFieldPathValue("data.someInt", 3), 861 | resourceWithFieldPathValue("data.someInt", 1), 862 | resourceWithFieldPathValue("data.someInt", 2), 863 | }, 864 | path: "data.someInt", 865 | }, 866 | want: want{ 867 | extras: []resource.Extra{ 868 | resourceWithFieldPathValue("data.someInt", 1), 869 | resourceWithFieldPathValue("data.someInt", 2), 870 | resourceWithFieldPathValue("data.someInt", 3), 871 | }, 872 | }, 873 | }, 874 | "SortByFloat": { 875 | reason: "The Function should sort the Extras by the float value at the specified field path", 876 | args: args{ 877 | extras: []resource.Extra{ 878 | resourceWithFieldPathValue("data.someFloat", 1.3), 879 | resourceWithFieldPathValue("data.someFloat", 1.1), 880 | resourceWithFieldPathValue("data.someFloat", 1.2), 881 | resourceWithFieldPathValue("data.someFloat", 1.4), 882 | }, 883 | path: "data.someFloat", 884 | }, 885 | want: want{ 886 | extras: []resource.Extra{ 887 | resourceWithFieldPathValue("data.someFloat", 1.1), 888 | resourceWithFieldPathValue("data.someFloat", 1.2), 889 | resourceWithFieldPathValue("data.someFloat", 1.3), 890 | resourceWithFieldPathValue("data.someFloat", 1.4), 891 | }, 892 | }, 893 | }, 894 | "InconsistentTypeSortByInt": { 895 | reason: "The Function should sort the Extras by the int value at the specified field path", 896 | args: args{ 897 | extras: []resource.Extra{ 898 | resourceWithFieldPathValue("data.someInt", 3), 899 | resourceWithFieldPathValue("data.someInt", 1), 900 | resourceWithFieldPathValue("data.someInt", "2"), 901 | }, 902 | path: "data.someInt", 903 | }, 904 | want: want{ 905 | err: cmpopts.AnyError, 906 | }, 907 | }, 908 | "EmptyPath": { 909 | reason: "The Function should return an error if the path is empty", 910 | args: args{ 911 | extras: []resource.Extra{ 912 | resourceWithFieldPathValue("metadata.name", "c"), 913 | resourceWithFieldPathValue("metadata.name", "a"), 914 | resourceWithFieldPathValue("metadata.name", "b"), 915 | }, 916 | path: "", 917 | }, 918 | want: want{ 919 | err: cmpopts.AnyError, 920 | }, 921 | }, 922 | "InvalidPathAll": { 923 | reason: "The Function should return no error if the path is invalid for all resources", 924 | args: args{ 925 | extras: []resource.Extra{ 926 | resourceWithFieldPathValue("metadata.name", "c"), 927 | resourceWithFieldPathValue("metadata.name", "a"), 928 | resourceWithFieldPathValue("metadata.name", "b"), 929 | }, 930 | path: "metadata.invalid", 931 | }, 932 | want: want{ 933 | extras: []resource.Extra{ 934 | resourceWithFieldPathValue("metadata.name", "c"), 935 | resourceWithFieldPathValue("metadata.name", "a"), 936 | resourceWithFieldPathValue("metadata.name", "b"), 937 | }, 938 | }, 939 | }, 940 | "InvalidPathSome": { 941 | reason: "The Function should return no error if the path is invalid for some resources, just use the rest of the resources zero value", 942 | args: args{ 943 | extras: []resource.Extra{ 944 | resourceWithFieldPathValue("metadata.name", "c"), 945 | resourceWithFieldPathValue("metadata.invalid", "a"), 946 | resourceWithFieldPathValue("metadata.name", "b"), 947 | }, 948 | path: "metadata.name", 949 | }, 950 | want: want{ 951 | extras: []resource.Extra{ 952 | resourceWithFieldPathValue("metadata.invalid", "a"), 953 | resourceWithFieldPathValue("metadata.name", "b"), 954 | resourceWithFieldPathValue("metadata.name", "c"), 955 | }, 956 | }, 957 | }, 958 | } 959 | 960 | for name, tc := range cases { 961 | t.Run(name, func(t *testing.T) { 962 | got := sortExtrasByFieldPath(tc.args.extras, tc.args.path) 963 | if diff := cmp.Diff(tc.want.err, got, cmpopts.EquateErrors()); diff != "" { 964 | t.Errorf("%s\n(...): -want err, +got err:\n%s", tc.reason, diff) 965 | } 966 | if tc.want.err != nil { 967 | return 968 | } 969 | if diff := cmp.Diff(tc.want.extras, tc.args.extras); diff != "" { 970 | t.Errorf("%s\n(...): -want, +got:\n%s", tc.reason, diff) 971 | } 972 | }) 973 | } 974 | } 975 | --------------------------------------------------------------------------------