├── 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 | [](https://github.com/crossplane-contrib/function-environment-configs/actions/workflows/ci.yml) 
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 | 
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 |
--------------------------------------------------------------------------------