├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── tag.yml ├── .gitignore ├── .golangci.yml ├── CODEOWNERS ├── Dockerfile ├── LICENSE ├── NOTES.txt ├── OWNERS.md ├── README.md ├── air.toml ├── example ├── README.md ├── composition.yaml ├── extraResources.yaml ├── functions.yaml └── xr.yaml ├── fn.go ├── fn_test.go ├── go.mod ├── go.sum ├── input ├── generate.go └── v1beta1 │ ├── input.go │ ├── resource_select.go │ └── zz_generated.deepcopy.go ├── main.go ├── package ├── crossplane.yaml └── input │ └── extra-resources.fn.crossplane.io_inputs.yaml ├── renovate.json └── run.sh /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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.3' 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 | # The package to push, without a version tag. The default matches GitHub. For 27 | # example xpkg.crossplane.io/crossplane/function-template-go. Note that 28 | # xpkg.crossplane.io is just an alias for ghcr.io, so we upload to ghcr.io but 29 | # this'll be pulled from xpkg.crossplane.io. 30 | XPKG: ghcr.io/${{ github.repository}} 31 | 32 | # The package version to push. The default is 0.0.0-gitsha. 33 | XPKG_VERSION: ${{ inputs.version }} 34 | 35 | jobs: 36 | lint: 37 | runs-on: ubuntu-24.04 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v4 41 | 42 | - name: Setup Go 43 | uses: actions/setup-go@v5 44 | with: 45 | go-version: ${{ env.GO_VERSION }} 46 | cache: false # The golangci-lint action does its own caching. 47 | 48 | - name: Check go mod tidy 49 | run: go mod tidy && git diff --exit-code go.mod go.sum 50 | 51 | - name: Lint 52 | uses: golangci/golangci-lint-action@v8 53 | with: 54 | version: ${{ env.GOLANGCI_VERSION }} 55 | 56 | unit-test: 57 | runs-on: ubuntu-24.04 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v4 61 | 62 | - name: Setup Go 63 | uses: actions/setup-go@v5 64 | with: 65 | go-version: ${{ env.GO_VERSION }} 66 | 67 | - name: Run Unit Tests 68 | run: go test -v -cover ./... 69 | 70 | # We want to build most packages for the amd64 and arm64 architectures. To 71 | # speed this up we build single-platform packages in parallel. We then upload 72 | # those packages to GitHub as a build artifact. The push job downloads those 73 | # artifacts and pushes them as a single multi-platform package. 74 | build: 75 | runs-on: ubuntu-24.04 76 | strategy: 77 | fail-fast: true 78 | matrix: 79 | arch: 80 | - amd64 81 | - arm64 82 | steps: 83 | - name: Setup QEMU 84 | uses: docker/setup-qemu-action@v3 85 | with: 86 | platforms: all 87 | 88 | - name: Setup Docker Buildx 89 | uses: docker/setup-buildx-action@v3 90 | with: 91 | version: ${{ env.DOCKER_BUILDX_VERSION }} 92 | install: true 93 | 94 | - name: Checkout 95 | uses: actions/checkout@v4 96 | 97 | # We ask Docker to use GitHub Action's native caching support to speed up 98 | # the build, per https://docs.docker.com/build/cache/backends/gha/. 99 | - name: Build Runtime 100 | id: image 101 | uses: docker/build-push-action@v6 102 | with: 103 | context: . 104 | platforms: linux/${{ matrix.arch }} 105 | cache-from: type=gha 106 | cache-to: type=gha,mode=max 107 | target: image 108 | build-args: 109 | GO_VERSION=${{ env.GO_VERSION }} 110 | outputs: type=docker,dest=runtime-${{ matrix.arch }}.tar 111 | 112 | - name: Setup the Crossplane CLI 113 | run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh" 114 | 115 | - name: Build Package 116 | run: ./crossplane xpkg build --package-file=${{ matrix.arch }}.xpkg --package-root=package/ --embed-runtime-image-tarball=runtime-${{ matrix.arch }}.tar 117 | 118 | - name: Upload Single-Platform Package 119 | uses: actions/upload-artifact@v4 120 | with: 121 | name: package-${{ matrix.arch }} 122 | path: "*.xpkg" 123 | if-no-files-found: error 124 | retention-days: 1 125 | 126 | # This job downloads the single-platform packages built by the build job, and 127 | # pushes them as a multi-platform package. 128 | push: 129 | runs-on: ubuntu-24.04 130 | permissions: 131 | contents: read 132 | packages: write 133 | needs: 134 | - build 135 | steps: 136 | - name: Checkout 137 | uses: actions/checkout@v4 138 | 139 | - name: Download Single-Platform Packages 140 | uses: actions/download-artifact@v4 141 | with: 142 | # See https://github.com/docker/build-push-action/blob/263435/README.md#summaries 143 | pattern: "!*.dockerbuild" 144 | path: . 145 | merge-multiple: true 146 | 147 | - name: Setup the Crossplane CLI 148 | run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh" 149 | 150 | - name: Login to GitHub Container Registry 151 | uses: docker/login-action@v3 152 | with: 153 | registry: ghcr.io 154 | username: ${{ github.repository_owner }} 155 | password: ${{ secrets.GITHUB_TOKEN }} 156 | 157 | # If a version wasn't explicitly passed as a workflow_dispatch input we 158 | # default to version v0.0.0--, for example 159 | # v0.0.0-20231101115142-1091066df799. This is a simple implementation of 160 | # Go's pseudo-versions: https://go.dev/ref/mod#pseudo-versions. 161 | - name: Set Default Multi-Platform Package Version 162 | if: env.XPKG_VERSION == '' 163 | 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 164 | 165 | - name: Push Multi-Platform Package to GitHub Container Registry 166 | # XPKG repo name can't contain uppercase characters like UpboundCare, we need to lowercase if the GithubOrg contains them. 167 | # See https://github.com/orgs/community/discussions/25768#discussioncomment-8057564 for XPKG@L lowercase explanation 168 | run: "./crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${XPKG@L}:${{ env.XPKG_VERSION }}" 169 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | package/*.xpkg 20 | 21 | # Go workspace file 22 | go.work 23 | tmp 24 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 @reedjosh 23 | -------------------------------------------------------------------------------- /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/base-debian11 AS image 42 | WORKDIR / 43 | COPY --from=build /function /function 44 | EXPOSE 9443 45 | USER nonroot:nonroot 46 | ENTRYPOINT ["/function"] 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /NOTES.txt: -------------------------------------------------------------------------------- 1 | To get started: 2 | 3 | 1. Replace `function-template-go` with your function in `go.mod`, 4 | `package/crossplane.yaml`, and any Go imports. (You can also do this 5 | automatically by running the `./init.sh ` script.) 6 | 2. Update `input/v1beta1/` to reflect your desired input (and run `go generate`) 7 | 3. Add your logic to `RunFunction` in `fn.go` 8 | 4. Add tests for your logic in `fn_test.go` 9 | 5. Update `README.md`, to be about your function! -------------------------------------------------------------------------------- /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 | * Joshua Reed ([reedjosh](https://github.com/reedjosh)) 16 | 17 | See [CODEOWNERS](./CODEOWNERS) for automatic PR assignment. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # function-extra-resources 2 | [![CI](https://github.com/crossplane/function-template-go/actions/workflows/ci.yml/badge.svg)](https://github.com/crossplane/function-template-go/actions/workflows/ci.yml) 3 | 4 | A function for selecting extra resources via [composition function][functions]s in [Go][go]. 5 | 6 | ## Using `function-extra-resources` 7 | 8 | Please see the example in `./examples` 9 | 10 | `function-extra-resources` is generally most useful in tandem with a function that can reference the many resources like 11 | `function-go-templating`. 12 | 13 | ### Creating objects from other's found in the local cluster. 14 | ``` yaml 15 | apiVersion: apiextensions.crossplane.io/v1 16 | kind: Composition 17 | metadata: 18 | name: function-environment-configs 19 | spec: 20 | compositeTypeRef: 21 | apiVersion: example.crossplane.io/v1 22 | kind: XR 23 | mode: Pipeline 24 | pipeline: 25 | - step: pull-extra-resources 26 | functionRef: 27 | name: function-extra-resources 28 | input: 29 | apiVersion: extra-resources.fn.crossplane.io/v1beta1 30 | kind: Input 31 | spec: 32 | extraResources: 33 | - kind: XCluster 34 | into: XCluster 35 | apiVersion: example.crossplane.io/v1 36 | type: Selector 37 | selector: 38 | maxMatch: 2 39 | minMatch: 1 40 | matchLabels: 41 | - key: type 42 | type: Value 43 | value: cluster 44 | - step: go-templating 45 | functionRef: 46 | name: function-go-templating 47 | input: 48 | apiVersion: gotemplating.fn.crossplane.io/v1beta1 49 | kind: GoTemplate 50 | source: Inline 51 | inline: 52 | template: | 53 | {{- $XClusters := index (index .context "apiextensions.crossplane.io/extra-resources") "XCluster" }} 54 | {{- range $i, $A := $XClusters }} 55 | --- 56 | apiVersion: vault.upbound.io/v1beta1 57 | kind: VaultRole 58 | metadata: 59 | annotations: 60 | gotemplating.fn.crossplane.io/composition-resource-name: {{index (index $A "metadata") "name"}} 61 | spec: 62 | forProvider: 63 | {{- end}} 64 | ``` 65 | 66 | ## Local dev. 67 | 68 | ### Air 69 | 70 | `air` is not strictly necessary, but helpful. 71 | 72 | Installing [air](https://github.com/cosmtrek/air) allows quick iterative local development. 73 | 74 | `air` is a live reloader that watches for local file changes. 75 | 76 | Once installed, running 77 | 78 | `air -- --insecure --debug --address localhost:9443`. 79 | 80 | Shoud get the function process/server build and running to serve CLI function requests. 81 | 82 | ### After locally serving function-extra-resources 83 | 84 | `./run.sh` will use the crossplane CLI to run our basic example in `./examples` 85 | 86 | ### Crossplane Function Basics 87 | 88 | This function uses [Go][go], [Docker][docker], and the [Crossplane CLI][cli]. 89 | 90 | ```shell 91 | # Run code generation - see input/generate.go 92 | $ go generate ./... 93 | 94 | # Run tests - see fn_test.go 95 | $ go test ./... 96 | 97 | # Build the function's runtime image - see Dockerfile 98 | $ docker build . --tag=runtime 99 | 100 | # Build a function package - see package/crossplane.yaml 101 | $ crossplane xpkg build -f package --embed-runtime-image=runtime 102 | ``` 103 | 104 | [functions]: https://docs.crossplane.io/latest/concepts/composition-functions 105 | [go]: https://go.dev 106 | [function guide]: https://docs.crossplane.io/knowledge-base/guides/write-a-composition-function-in-go 107 | [package docs]: https://pkg.go.dev/github.com/crossplane/function-sdk-go 108 | [docker]: https://www.docker.com 109 | [cli]: https://docs.crossplane.io/latest/cli 110 | -------------------------------------------------------------------------------- /air.toml: -------------------------------------------------------------------------------- 1 | # air.toml 2 | root = "." 3 | tmp_dir = "tmp" 4 | build_cmd = "go build -o ./tmp/main ." 5 | run_cmd = "./tmp/main" 6 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example manifests 2 | 3 | You can run your function locally and test it using `crossplane 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 xr.yaml composition.yaml functions.yaml -r 14 | --- 15 | apiVersion: example.crossplane.io/v1 16 | kind: XR 17 | metadata: 18 | name: example-xr 19 | --- 20 | apiVersion: render.crossplane.io/v1beta1 21 | kind: Result 22 | message: I was run with input "Hello world"! 23 | severity: SEVERITY_NORMAL 24 | step: run-the-template 25 | ``` 26 | -------------------------------------------------------------------------------- /example/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: pull-extra-resources 12 | functionRef: 13 | name: function-extra-resources 14 | input: 15 | apiVersion: extra-resources.fn.crossplane.io/v1beta1 16 | kind: Input 17 | spec: 18 | extraResources: 19 | - kind: EnvironmentConfig 20 | into: envConfs 21 | apiVersion: apiextensions.crossplane.io/v1alpha1 22 | type: Selector 23 | selector: 24 | maxMatch: 2 25 | minMatch: 1 26 | matchLabels: 27 | - key: type 28 | type: Value 29 | value: cluster 30 | - kind: XCluster 31 | into: XCluster 32 | apiVersion: example.crossplane.io/v1 33 | type: Selector 34 | selector: 35 | maxMatch: 2 36 | minMatch: 1 37 | matchLabels: 38 | - key: type 39 | type: Value 40 | value: cluster 41 | - step: go-templating 42 | functionRef: 43 | name: function-go-templating 44 | input: 45 | apiVersion: gotemplating.fn.crossplane.io/v1beta1 46 | kind: GoTemplate 47 | source: Inline 48 | inline: 49 | template: | 50 | {{- $XClusters := index (index .context "apiextensions.crossplane.io/extra-resources") "XCluster" }} 51 | {{- range $i, $A := $XClusters }} 52 | --- 53 | apiVersion: vault.upbound.io/v1beta1 54 | kind: VaultRole 55 | metadata: 56 | annotations: 57 | gotemplating.fn.crossplane.io/composition-resource-name: {{index (index $A "metadata") "name"}} 58 | spec: 59 | forProvider: 60 | {{- end}} 61 | -------------------------------------------------------------------------------- /example/extraResources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1alpha1 2 | kind: EnvironmentConfig 3 | metadata: 4 | name: test-x1 5 | labels: 6 | type: cluster 7 | metaName: test 8 | data: 9 | realName: test-x1 10 | --- 11 | apiVersion: apiextensions.crossplane.io/v1alpha1 12 | kind: EnvironmentConfig 13 | metadata: 14 | name: test-y1 15 | labels: 16 | type: cluster 17 | metaName: test 18 | data: 19 | realName: test-y1 20 | --- 21 | apiVersion: example.crossplane.io/v1 22 | kind: XCluster 23 | metadata: 24 | name: net-staging-blue 25 | labels: 26 | type: cluster 27 | spec: 28 | compositionRef: 29 | name: compositecluster.example.crossplane.io 30 | compositionSelector: 31 | matchLabels: 32 | provider: aws 33 | compositionUpdatePolicy: Automatic 34 | --- 35 | apiVersion: example.crossplane.io/v1 36 | kind: XCluster 37 | metadata: 38 | name: net-staging-green 39 | labels: 40 | type: cluster 41 | spec: 42 | compositionRef: 43 | name: compositecluster.example.crossplane.io 44 | compositionSelector: 45 | matchLabels: 46 | provider: aws 47 | compositionUpdatePolicy: Automatic 48 | -------------------------------------------------------------------------------- /example/functions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1beta1 3 | kind: Function 4 | metadata: 5 | name: function-extra-resources 6 | annotations: 7 | # This tells crossplane 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-extra-resources:v0.1.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 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /fn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "reflect" 7 | "sort" 8 | 9 | "google.golang.org/protobuf/encoding/protojson" 10 | "google.golang.org/protobuf/types/known/structpb" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | 13 | "github.com/crossplane/crossplane-runtime/pkg/errors" 14 | "github.com/crossplane/crossplane-runtime/pkg/fieldpath" 15 | "github.com/crossplane/crossplane-runtime/pkg/logging" 16 | fnv1 "github.com/crossplane/function-sdk-go/proto/v1" 17 | "github.com/crossplane/function-sdk-go/request" 18 | "github.com/crossplane/function-sdk-go/resource" 19 | "github.com/crossplane/function-sdk-go/response" 20 | 21 | "github.com/crossplane-contrib/function-extra-resources/input/v1beta1" 22 | ) 23 | 24 | // Key to retrieve extras at. 25 | const ( 26 | FunctionContextKeyExtraResources = "apiextensions.crossplane.io/extra-resources" 27 | ) 28 | 29 | // Function returns whatever response you ask it to. 30 | type Function struct { 31 | fnv1.UnimplementedFunctionRunnerServiceServer 32 | 33 | log logging.Logger 34 | } 35 | 36 | // RunFunction runs the Function. 37 | func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) { 38 | f.log.Info("Running function", "tag", req.GetMeta().GetTag()) 39 | 40 | rsp := response.To(req, response.DefaultTTL) 41 | 42 | // Get function input. 43 | in := &v1beta1.Input{} 44 | if err := request.GetInput(req, in); err != nil { 45 | response.Fatal(rsp, errors.Errorf("cannot get Function input from %T: %w", req, err)) 46 | return rsp, nil 47 | } 48 | 49 | // Get XR the pipeline targets. 50 | oxr, err := request.GetObservedCompositeResource(req) 51 | if err != nil { 52 | response.Fatal(rsp, errors.Errorf("cannot get observed composite resource: %w", err)) 53 | return rsp, nil 54 | } 55 | 56 | // Build extraResource Requests. 57 | requirements, err := buildRequirements(in, oxr) 58 | if err != nil { 59 | response.Fatal(rsp, errors.Errorf("could not build extra resource requirements: %w", err)) 60 | return rsp, nil 61 | } 62 | rsp.Requirements = requirements 63 | 64 | // The request response cycle for the Crossplane ExtraResources API requires that function-extra-resources 65 | // tells Crossplane what it wants. 66 | // Then a new rquest is sent to function-extra-resources with those resources present at the ExtraResources field. 67 | // 68 | // function-extra-resources does not know if it has requested the resources already or not. 69 | // 70 | // If it has and these resources are now present, proceed with verification and conversion. 71 | if req.ExtraResources == nil { 72 | f.log.Debug("No extra resources present, exiting", "requirements", rsp.GetRequirements()) 73 | return rsp, nil 74 | } 75 | 76 | // Pull extra resources from the ExtraResources request field. 77 | extraResources, err := request.GetExtraResources(req) 78 | if err != nil { 79 | response.Fatal(rsp, errors.Errorf("fetching extra resources %T: %w", req, err)) 80 | return rsp, nil 81 | } 82 | 83 | // Sort and verify min/max selected. 84 | // Sorting is required for determinism. 85 | verifiedExtras, err := verifyAndSortExtras(in, extraResources) 86 | if err != nil { 87 | response.Fatal(rsp, errors.Errorf("verifying and sorting extra resources: %w", err)) 88 | return rsp, nil 89 | } 90 | 91 | // For now cheaply convert to JSON for serializing. 92 | // 93 | // TODO(reedjosh): look into resources.AsStruct or simlar since unsturctured k8s objects are already almost json. 94 | // structpb.NewList(v []interface{}) should create an array like. 95 | // Combining this and similar structures from the structpb lib should should be done to create 96 | // a map[string][object] container into which the found extra resources can be dumped. 97 | // 98 | // The found extra resources should then be directly marhsal-able via: 99 | // obj := &unstructured.Unstructured{} 100 | // obj.MarshalJSON() 101 | b, err := json.Marshal(verifiedExtras) 102 | if err != nil { 103 | response.Fatal(rsp, errors.Errorf("cannot marshal %T: %w", verifiedExtras, err)) 104 | return rsp, nil 105 | } 106 | s := &structpb.Struct{} 107 | err = protojson.Unmarshal(b, s) 108 | if err != nil { 109 | response.Fatal(rsp, errors.Errorf("cannot unmarshal %T into %T: %w", extraResources, s, err)) 110 | return rsp, nil 111 | } 112 | response.SetContextKey(rsp, FunctionContextKeyExtraResources, structpb.NewStructValue(s)) 113 | 114 | return rsp, nil 115 | } 116 | 117 | // Build requirements takes input and outputs an array of external resoruce requirements to request 118 | // from Crossplane's external resource API. 119 | func buildRequirements(in *v1beta1.Input, xr *resource.Composite) (*fnv1.Requirements, error) { //nolint:gocyclo // Adding non-nil validations increases function complexity. 120 | extraResources := make(map[string]*fnv1.ResourceSelector, len(in.Spec.ExtraResources)) 121 | for _, extraResource := range in.Spec.ExtraResources { 122 | extraResName := extraResource.Into 123 | switch extraResource.Type { 124 | case v1beta1.ResourceSourceTypeReference, "": 125 | extraResources[extraResName] = &fnv1.ResourceSelector{ 126 | ApiVersion: extraResource.APIVersion, 127 | Kind: extraResource.Kind, 128 | Match: &fnv1.ResourceSelector_MatchName{ 129 | MatchName: extraResource.Ref.Name, 130 | }, 131 | Namespace: extraResource.Namespace, 132 | } 133 | case v1beta1.ResourceSourceTypeSelector: 134 | matchLabels := map[string]string{} 135 | for _, selector := range extraResource.Selector.MatchLabels { 136 | switch selector.GetType() { 137 | case v1beta1.ResourceSourceSelectorLabelMatcherTypeValue: 138 | if selector.Value == nil { 139 | return nil, errors.New("Value cannot be nil for type 'Value'") 140 | } 141 | matchLabels[selector.Key] = *selector.Value 142 | case v1beta1.ResourceSourceSelectorLabelMatcherTypeFromCompositeFieldPath: 143 | if selector.ValueFromFieldPath == nil { 144 | return nil, errors.New("ValueFromFieldPath cannot be nil for type 'FromCompositeFieldPath'") 145 | } 146 | value, err := fieldpath.Pave(xr.Resource.Object).GetString(*selector.ValueFromFieldPath) 147 | if err != nil { 148 | if !selector.FromFieldPathIsOptional() { 149 | return nil, errors.Wrapf(err, "cannot get value from field path %q", *selector.ValueFromFieldPath) 150 | } 151 | continue 152 | } 153 | matchLabels[selector.Key] = value 154 | } 155 | } 156 | if len(matchLabels) == 0 { 157 | continue 158 | } 159 | extraResources[extraResName] = &fnv1.ResourceSelector{ 160 | ApiVersion: extraResource.APIVersion, 161 | Kind: extraResource.Kind, 162 | Match: &fnv1.ResourceSelector_MatchLabels{ 163 | MatchLabels: &fnv1.MatchLabels{Labels: matchLabels}, 164 | }, 165 | Namespace: extraResource.Namespace, 166 | } 167 | } 168 | } 169 | return &fnv1.Requirements{ExtraResources: extraResources}, nil 170 | } 171 | 172 | // Verify Min/Max and sort extra resources by field path within a single kind. 173 | func verifyAndSortExtras(in *v1beta1.Input, extraResources map[string][]resource.Extra, //nolint:gocyclo // TODO(reedjosh): refactor 174 | ) (cleanedExtras map[string][]unstructured.Unstructured, err error) { 175 | cleanedExtras = make(map[string][]unstructured.Unstructured) 176 | for _, extraResource := range in.Spec.ExtraResources { 177 | extraResName := extraResource.Into 178 | resources, ok := extraResources[extraResName] 179 | if !ok { 180 | return nil, errors.Errorf("cannot find expected extra resource %q", extraResName) 181 | } 182 | switch extraResource.GetType() { 183 | case v1beta1.ResourceSourceTypeReference: 184 | if len(resources) == 0 { 185 | if in.Spec.Policy.IsResolutionPolicyOptional() { 186 | continue 187 | } 188 | return nil, errors.Errorf("Required extra resource %q not found", extraResName) 189 | } 190 | if len(resources) > 1 { 191 | return nil, errors.Errorf("expected exactly one extra resource %q, got %d", extraResName, len(resources)) 192 | } 193 | cleanedExtras[extraResName] = append(cleanedExtras[extraResName], *resources[0].Resource) 194 | 195 | case v1beta1.ResourceSourceTypeSelector: 196 | selector := extraResource.Selector 197 | if selector.MinMatch != nil && uint64(len(resources)) < *selector.MinMatch { 198 | return nil, errors.Errorf("expected at least %d extra resources %q, got %d", *selector.MinMatch, extraResName, len(resources)) 199 | } 200 | if err := sortExtrasByFieldPath(resources, selector.GetSortByFieldPath()); err != nil { 201 | return nil, err 202 | } 203 | if selector.MaxMatch != nil && uint64(len(resources)) > *selector.MaxMatch { 204 | resources = resources[:*selector.MaxMatch] 205 | } 206 | for _, r := range resources { 207 | cleanedExtras[extraResName] = append(cleanedExtras[extraResName], *r.Resource) 208 | } 209 | } 210 | } 211 | return cleanedExtras, nil 212 | } 213 | 214 | // Sort extra resources by field path within a single kind. 215 | func sortExtrasByFieldPath(extras []resource.Extra, path string) error { //nolint:gocyclo // TODO(phisco): refactor 216 | if path == "" { 217 | return errors.New("cannot sort by empty field path") 218 | } 219 | p := make([]struct { 220 | ec resource.Extra 221 | val any 222 | }, len(extras)) 223 | 224 | var t reflect.Type 225 | for i := range extras { 226 | p[i].ec = extras[i] 227 | val, err := fieldpath.Pave(extras[i].Resource.Object).GetValue(path) 228 | if err != nil && !fieldpath.IsNotFound(err) { 229 | return err 230 | } 231 | p[i].val = val 232 | if val == nil { 233 | continue 234 | } 235 | vt := reflect.TypeOf(val) 236 | switch { 237 | case t == nil: 238 | t = vt 239 | case t != vt: 240 | return errors.Errorf("cannot sort values of different types %q and %q", t, vt) 241 | } 242 | } 243 | if t == nil { 244 | // we either have no values or all values are nil, we can just return 245 | return nil 246 | } 247 | 248 | var err error 249 | sort.Slice(p, func(i, j int) bool { 250 | vali, valj := p[i].val, p[j].val 251 | if vali == nil { 252 | vali = reflect.Zero(t).Interface() 253 | } 254 | if valj == nil { 255 | valj = reflect.Zero(t).Interface() 256 | } 257 | switch t.Kind() { //nolint:exhaustive // we only support these types 258 | case reflect.Float64: 259 | return vali.(float64) < valj.(float64) 260 | case reflect.Float32: 261 | return vali.(float32) < valj.(float32) 262 | case reflect.Int64: 263 | return vali.(int64) < valj.(int64) 264 | case reflect.Int32: 265 | return vali.(int32) < valj.(int32) 266 | case reflect.Int16: 267 | return vali.(int16) < valj.(int16) 268 | case reflect.Int8: 269 | return vali.(int8) < valj.(int8) 270 | case reflect.Int: 271 | return vali.(int) < valj.(int) 272 | case reflect.String: 273 | return vali.(string) < valj.(string) 274 | default: 275 | // should never happen 276 | err = errors.Errorf("unsupported type %q for sorting", t) 277 | return false 278 | } 279 | }) 280 | if err != nil { 281 | return err 282 | } 283 | 284 | for i := 0; i < len(extras); i++ { 285 | extras[i] = p[i].ec 286 | } 287 | return nil 288 | } 289 | -------------------------------------------------------------------------------- /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 | type args struct { 24 | ctx context.Context 25 | req *fnv1.RunFunctionRequest 26 | } 27 | type want struct { 28 | rsp *fnv1.RunFunctionResponse 29 | err error 30 | } 31 | 32 | cases := map[string]struct { 33 | reason string 34 | args args 35 | want want 36 | }{ 37 | "RequestExtraResources": { 38 | reason: "The Function should request ExtraResources", 39 | args: args{ 40 | req: &fnv1.RunFunctionRequest{ 41 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 42 | Observed: &fnv1.State{ 43 | Composite: &fnv1.Resource{ 44 | Resource: resource.MustStructJSON(`{ 45 | "apiVersion": "test.crossplane.io/v1alpha1", 46 | "kind": "XR", 47 | "metadata": { 48 | "name": "my-xr" 49 | }, 50 | "spec": { 51 | "existingEnvSelectorLabel": "someMoreBar", 52 | "existingBazLabel": "someMoreBar" 53 | } 54 | }`), 55 | }, 56 | }, 57 | Input: resource.MustStructJSON(`{ 58 | "apiVersion": "extra-resources.fn.crossplane.io/v1beta1", 59 | "kind": "Input", 60 | "spec": { 61 | "extraResources": [ 62 | { 63 | "kind": "EnvironmentConfig", 64 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 65 | "type": "Reference", 66 | "into": "obj-0", 67 | "ref": { 68 | "name": "my-env-config" 69 | } 70 | }, 71 | { 72 | "type": "Reference", 73 | "kind": "EnvironmentConfig", 74 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 75 | "into": "obj-1", 76 | "ref": { 77 | "name": "my-second-env-config" 78 | } 79 | }, 80 | { 81 | "type": "Selector", 82 | "kind": "EnvironmentConfig", 83 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 84 | "into": "obj-2", 85 | "selector": { 86 | "matchLabels": [ 87 | { 88 | "type": "Value", 89 | "key": "foo", 90 | "value": "bar" 91 | } 92 | ] 93 | } 94 | }, 95 | { 96 | "type": "Selector", 97 | "kind": "EnvironmentConfig", 98 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 99 | "into": "obj-3", 100 | "selector": { 101 | "matchLabels": [ 102 | { 103 | "key": "someMoreFoo", 104 | "valueFromFieldPath": "spec.missingEnvSelectorLabel", 105 | "fromFieldPathPolicy": "Optional" 106 | } 107 | ] 108 | } 109 | }, 110 | { 111 | "type": "Selector", 112 | "kind": "EnvironmentConfig", 113 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 114 | "into": "obj-4", 115 | "selector": { 116 | "matchLabels": [ 117 | { 118 | "key": "someMoreFoo", 119 | "valueFromFieldPath": "spec.existingEnvSelectorLabel", 120 | "fromFieldPathPolicy": "Required" 121 | } 122 | ] 123 | } 124 | }, 125 | { 126 | "type": "Reference", 127 | "kind": "Foo", 128 | "apiVersion": "test.crossplane.io/v1alpha1", 129 | "namespace": "my-namespace", 130 | "into": "obj-5", 131 | "ref": { 132 | "name": "my-foo" 133 | } 134 | }, 135 | { 136 | "type": "Selector", 137 | "kind": "Bar", 138 | "apiVersion": "test.crossplane.io/v1alpha1", 139 | "namespace": "my-namespace", 140 | "into": "obj-6", 141 | "selector": { 142 | "matchLabels": [ 143 | { 144 | "key": "someMoreBar", 145 | "valueFromFieldPath": "spec.existingBazLabel", 146 | "fromFieldPathPolicy": "Required" 147 | } 148 | ] 149 | } 150 | } 151 | ] 152 | } 153 | }`), 154 | }, 155 | }, 156 | want: want{ 157 | rsp: &fnv1.RunFunctionResponse{ 158 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 159 | Results: []*fnv1.Result{}, 160 | Requirements: &fnv1.Requirements{ 161 | ExtraResources: map[string]*fnv1.ResourceSelector{ 162 | "obj-0": { 163 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 164 | Kind: "EnvironmentConfig", 165 | Match: &fnv1.ResourceSelector_MatchName{ 166 | MatchName: "my-env-config", 167 | }, 168 | }, 169 | "obj-1": { 170 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 171 | Kind: "EnvironmentConfig", 172 | Match: &fnv1.ResourceSelector_MatchName{ 173 | MatchName: "my-second-env-config", 174 | }, 175 | }, 176 | "obj-2": { 177 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 178 | Kind: "EnvironmentConfig", 179 | Match: &fnv1.ResourceSelector_MatchLabels{ 180 | MatchLabels: &fnv1.MatchLabels{ 181 | Labels: map[string]string{ 182 | "foo": "bar", 183 | }, 184 | }, 185 | }, 186 | }, 187 | // 188 | // environment-config-3 is not requested because it was optional 189 | // 190 | "obj-4": { 191 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 192 | Kind: "EnvironmentConfig", 193 | Match: &fnv1.ResourceSelector_MatchLabels{ 194 | MatchLabels: &fnv1.MatchLabels{ 195 | Labels: map[string]string{ 196 | "someMoreFoo": "someMoreBar", 197 | }, 198 | }, 199 | }, 200 | }, 201 | "obj-5": { 202 | ApiVersion: "test.crossplane.io/v1alpha1", 203 | Kind: "Foo", 204 | Match: &fnv1.ResourceSelector_MatchName{ 205 | MatchName: "my-foo", 206 | }, 207 | Namespace: ptr.To("my-namespace"), 208 | }, 209 | "obj-6": { 210 | ApiVersion: "test.crossplane.io/v1alpha1", 211 | Kind: "Bar", 212 | Match: &fnv1.ResourceSelector_MatchLabels{ 213 | MatchLabels: &fnv1.MatchLabels{ 214 | Labels: map[string]string{ 215 | "someMoreBar": "someMoreBar", 216 | }, 217 | }, 218 | }, 219 | Namespace: ptr.To("my-namespace"), 220 | }, 221 | }, 222 | }, 223 | }, 224 | }, 225 | }, 226 | "RequestEnvironmentConfigsFound": { 227 | reason: "The Function should request the necessary EnvironmentConfigs even if they are already present in the request", 228 | args: args{ 229 | req: &fnv1.RunFunctionRequest{ 230 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 231 | Observed: &fnv1.State{ 232 | Composite: &fnv1.Resource{ 233 | Resource: resource.MustStructJSON(`{ 234 | "apiVersion": "test.crossplane.io/v1alpha1", 235 | "kind": "XR", 236 | "metadata": { 237 | "name": "my-xr" 238 | }, 239 | "spec": { 240 | "existingEnvSelectorLabel": "someMoreBar" 241 | } 242 | }`), 243 | }, 244 | }, 245 | ExtraResources: map[string]*fnv1.Resources{ 246 | "obj-0": { 247 | Items: []*fnv1.Resource{ 248 | { 249 | Resource: resource.MustStructJSON(`{ 250 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 251 | "kind": "EnvironmentConfig", 252 | "metadata": { 253 | "name": "my-env-config" 254 | }, 255 | "data": { 256 | "firstKey": "firstVal", 257 | "secondKey": "secondVal" 258 | } 259 | }`), 260 | }, 261 | }, 262 | }, 263 | "obj-1": { 264 | Items: []*fnv1.Resource{ 265 | { 266 | Resource: resource.MustStructJSON(`{ 267 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 268 | "kind": "EnvironmentConfig", 269 | "metadata": { 270 | "name": "my-second-env-config" 271 | }, 272 | "data": { 273 | "secondKey": "secondVal-ok", 274 | "thirdKey": "thirdVal" 275 | } 276 | }`), 277 | }, 278 | }, 279 | }, 280 | "obj-2": { 281 | Items: []*fnv1.Resource{ 282 | { 283 | Resource: resource.MustStructJSON(`{ 284 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 285 | "kind": "EnvironmentConfig", 286 | "metadata": { 287 | "name": "my-third-env-config-b" 288 | }, 289 | "data": { 290 | "fourthKey": "fourthVal-b" 291 | } 292 | }`), 293 | }, 294 | { 295 | Resource: resource.MustStructJSON(`{ 296 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 297 | "kind": "EnvironmentConfig", 298 | "metadata": { 299 | "name": "my-third-env-config-a" 300 | }, 301 | "data": { 302 | "fourthKey": "fourthVal-a" 303 | } 304 | }`), 305 | }, 306 | }, 307 | }, 308 | "obj-3": { 309 | Items: []*fnv1.Resource{ 310 | { 311 | Resource: resource.MustStructJSON(`{ 312 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 313 | "kind": "EnvironmentConfig", 314 | "metadata": { 315 | "name": "my-third-env-config" 316 | }, 317 | "data": { 318 | "fifthKey": "fifthVal" 319 | } 320 | }`), 321 | }, 322 | }, 323 | }, 324 | "obj-4": { 325 | Items: []*fnv1.Resource{ 326 | { 327 | Resource: resource.MustStructJSON(`{ 328 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 329 | "kind": "EnvironmentConfig", 330 | "metadata": { 331 | "name": "my-fourth-env-config" 332 | }, 333 | "data": { 334 | "sixthKey": "sixthVal" 335 | } 336 | }`), 337 | }, 338 | }, 339 | }, 340 | }, 341 | Input: resource.MustStructJSON(`{ 342 | "apiVersion": "extra-resources.fn.crossplane.io/v1beta1", 343 | "kind": "Input", 344 | "spec": { 345 | "extraResources": [ 346 | { 347 | "type": "Reference", 348 | "into": "obj-0", 349 | "kind": "EnvironmentConfig", 350 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 351 | "ref": { 352 | "name": "my-env-config" 353 | } 354 | }, 355 | { 356 | "type": "Reference", 357 | "into": "obj-1", 358 | "kind": "EnvironmentConfig", 359 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 360 | "ref": { 361 | "name": "my-second-env-config" 362 | } 363 | }, 364 | { 365 | "type": "Selector", 366 | "into": "obj-2", 367 | "kind": "EnvironmentConfig", 368 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 369 | "selector": { 370 | "matchLabels": [ 371 | { 372 | "type": "Value", 373 | "key": "foo", 374 | "value": "bar" 375 | } 376 | ] 377 | } 378 | }, 379 | { 380 | "type": "Selector", 381 | "into": "obj-3", 382 | "kind": "EnvironmentConfig", 383 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 384 | "selector": { 385 | "matchLabels": [ 386 | { 387 | "key": "someMoreFoo", 388 | "valueFromFieldPath": "spec.missingEnvSelectorLabel", 389 | "fromFieldPathPolicy": "Optional" 390 | } 391 | ] 392 | } 393 | }, 394 | { 395 | "type": "Selector", 396 | "into": "obj-4", 397 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 398 | "kind": "EnvironmentConfig", 399 | "selector": { 400 | "matchLabels": [ 401 | { 402 | "key": "someMoreFoo", 403 | "valueFromFieldPath": "spec.existingEnvSelectorLabel", 404 | "fromFieldPathPolicy": "Required" 405 | } 406 | ] 407 | } 408 | } 409 | ] 410 | } 411 | }`), 412 | }, 413 | }, 414 | want: want{ 415 | rsp: &fnv1.RunFunctionResponse{ 416 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 417 | Results: []*fnv1.Result{}, 418 | Requirements: &fnv1.Requirements{ 419 | ExtraResources: map[string]*fnv1.ResourceSelector{ 420 | "obj-0": { 421 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 422 | Kind: "EnvironmentConfig", 423 | Match: &fnv1.ResourceSelector_MatchName{ 424 | MatchName: "my-env-config", 425 | }, 426 | }, 427 | "obj-1": { 428 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 429 | Kind: "EnvironmentConfig", 430 | Match: &fnv1.ResourceSelector_MatchName{ 431 | MatchName: "my-second-env-config", 432 | }, 433 | }, 434 | "obj-2": { 435 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 436 | Kind: "EnvironmentConfig", 437 | Match: &fnv1.ResourceSelector_MatchLabels{ 438 | MatchLabels: &fnv1.MatchLabels{ 439 | Labels: map[string]string{ 440 | "foo": "bar", 441 | }, 442 | }, 443 | }, 444 | }, 445 | // environment-config-3 is not requested because it was optional 446 | "obj-4": { 447 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 448 | Kind: "EnvironmentConfig", 449 | Match: &fnv1.ResourceSelector_MatchLabels{ 450 | MatchLabels: &fnv1.MatchLabels{ 451 | Labels: map[string]string{ 452 | "someMoreFoo": "someMoreBar", 453 | }, 454 | }, 455 | }, 456 | }, 457 | }, 458 | }, 459 | Context: &structpb.Struct{ 460 | Fields: map[string]*structpb.Value{ 461 | FunctionContextKeyExtraResources: structpb.NewStructValue(resource.MustStructJSON(`{ 462 | "obj-0": [ 463 | { 464 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 465 | "data": { 466 | "firstKey": "firstVal", 467 | "secondKey": "secondVal" 468 | }, 469 | "kind": "EnvironmentConfig", 470 | "metadata": { 471 | "name": "my-env-config" 472 | } 473 | } 474 | ], 475 | "obj-1": [ 476 | { 477 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 478 | "data": { 479 | "secondKey": "secondVal-ok", 480 | "thirdKey": "thirdVal" 481 | }, 482 | "kind": "EnvironmentConfig", 483 | "metadata": { 484 | "name": "my-second-env-config" 485 | } 486 | } 487 | ], 488 | "obj-2": [ 489 | { 490 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 491 | "data": { 492 | "fourthKey": "fourthVal-a" 493 | }, 494 | "kind": "EnvironmentConfig", 495 | "metadata": { 496 | "name": "my-third-env-config-a" 497 | } 498 | }, 499 | { 500 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 501 | "data": { 502 | "fourthKey": "fourthVal-b" 503 | }, 504 | "kind": "EnvironmentConfig", 505 | "metadata": { 506 | "name": "my-third-env-config-b" 507 | } 508 | } 509 | ], 510 | "obj-3": [ 511 | { 512 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 513 | "data": { 514 | "fifthKey": "fifthVal" 515 | }, 516 | "kind": "EnvironmentConfig", 517 | "metadata": { 518 | "name": "my-third-env-config" 519 | } 520 | } 521 | ], 522 | "obj-4": [ 523 | { 524 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 525 | "data": { 526 | "sixthKey": "sixthVal" 527 | }, 528 | "kind": "EnvironmentConfig", 529 | "metadata": { 530 | "name": "my-fourth-env-config" 531 | } 532 | } 533 | ] 534 | }`)), 535 | }, 536 | }, 537 | }, 538 | }, 539 | }, 540 | "RequestEnvironmentConfigsNotFoundRequired": { 541 | reason: "The Function should return fatal if a required EnvironmentConfig is not found", 542 | args: args{ 543 | req: &fnv1.RunFunctionRequest{ 544 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 545 | Observed: &fnv1.State{ 546 | Composite: &fnv1.Resource{ 547 | Resource: resource.MustStructJSON(`{ 548 | "apiVersion": "test.crossplane.io/v1alpha1", 549 | "kind": "XR", 550 | "metadata": { 551 | "name": "my-xr" 552 | } 553 | }`), 554 | }, 555 | }, 556 | ExtraResources: map[string]*fnv1.Resources{ 557 | "environment-config-0": { 558 | Items: []*fnv1.Resource{}, 559 | }, 560 | }, 561 | Input: resource.MustStructJSON(`{ 562 | "apiVersion": "extra-resources.fn.crossplane.io/v1beta1", 563 | "kind": "Input", 564 | "spec": { 565 | "extraResources": [ 566 | { 567 | "type": "Reference", 568 | "into": "obj-0", 569 | "kind": "EnvironmentConfig", 570 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 571 | "ref": { 572 | "name": "my-env-config" 573 | } 574 | } 575 | ] 576 | } 577 | }`), 578 | }, 579 | }, 580 | want: want{ 581 | rsp: &fnv1.RunFunctionResponse{ 582 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 583 | Results: []*fnv1.Result{ 584 | { 585 | Severity: fnv1.Severity_SEVERITY_FATAL, 586 | Target: fnv1.Target_TARGET_COMPOSITE.Enum(), 587 | }, 588 | }, 589 | Requirements: &fnv1.Requirements{ 590 | ExtraResources: map[string]*fnv1.ResourceSelector{ 591 | "obj-0": { 592 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 593 | Kind: "EnvironmentConfig", 594 | Match: &fnv1.ResourceSelector_MatchName{ 595 | MatchName: "my-env-config", 596 | }, 597 | }, 598 | }, 599 | }, 600 | }, 601 | }, 602 | }, 603 | } 604 | 605 | for name, tc := range cases { 606 | t.Run(name, func(t *testing.T) { 607 | f := &Function{log: logging.NewNopLogger()} 608 | rsp, err := f.RunFunction(tc.args.ctx, tc.args.req) 609 | 610 | diff := cmp.Diff(tc.want.rsp, rsp, cmpopts.AcyclicTransformer("toJsonWithoutResultMessages", func(r *fnv1.RunFunctionResponse) []byte { 611 | // We don't care about messages. 612 | // cmptopts.IgnoreField wasn't working with protocmp.Transform 613 | // We can't split this to another transformer as 614 | // transformers are applied not in order but as soon as they 615 | // match the type, which are walked from the root (RunFunctionResponse). 616 | for _, result := range r.GetResults() { 617 | result.Message = "" 618 | } 619 | out, err := protojson.Marshal(r) 620 | if err != nil { 621 | t.Fatalf("cannot marshal %T to JSON: %s", r, err) 622 | } 623 | return out 624 | })) 625 | if diff != "" { 626 | t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff) 627 | } 628 | 629 | if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { 630 | t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff) 631 | } 632 | }) 633 | } 634 | } 635 | 636 | func resourceWithFieldPathValue(path string, value any) resource.Extra { 637 | u := unstructured.Unstructured{ 638 | Object: map[string]interface{}{}, 639 | } 640 | err := fieldpath.Pave(u.Object).SetValue(path, value) 641 | if err != nil { 642 | panic(err) 643 | } 644 | return resource.Extra{ 645 | Resource: &u, 646 | } 647 | } 648 | 649 | func TestSortExtrasByFieldPath(t *testing.T) { 650 | type args struct { 651 | extras []resource.Extra 652 | path string 653 | } 654 | type want struct { 655 | extras []resource.Extra 656 | err error 657 | } 658 | 659 | cases := map[string]struct { 660 | reason string 661 | args args 662 | want want 663 | }{ 664 | "SortByString": { 665 | reason: "The Function should sort the Extras by the string value at the specified field path", 666 | args: args{ 667 | extras: []resource.Extra{ 668 | resourceWithFieldPathValue("metadata.name", "c"), 669 | resourceWithFieldPathValue("metadata.name", "a"), 670 | resourceWithFieldPathValue("metadata.name", "b"), 671 | }, 672 | path: "metadata.name", 673 | }, 674 | want: want{ 675 | extras: []resource.Extra{ 676 | resourceWithFieldPathValue("metadata.name", "a"), 677 | resourceWithFieldPathValue("metadata.name", "b"), 678 | resourceWithFieldPathValue("metadata.name", "c"), 679 | }, 680 | }, 681 | }, 682 | "SortByInt": { 683 | reason: "The Function should sort the Extras by the int value at the specified field path", 684 | args: args{ 685 | extras: []resource.Extra{ 686 | resourceWithFieldPathValue("data.someInt", 3), 687 | resourceWithFieldPathValue("data.someInt", 1), 688 | resourceWithFieldPathValue("data.someInt", 2), 689 | }, 690 | path: "data.someInt", 691 | }, 692 | want: want{ 693 | extras: []resource.Extra{ 694 | resourceWithFieldPathValue("data.someInt", 1), 695 | resourceWithFieldPathValue("data.someInt", 2), 696 | resourceWithFieldPathValue("data.someInt", 3), 697 | }, 698 | }, 699 | }, 700 | "SortByFloat": { 701 | reason: "The Function should sort the Extras by the float value at the specified field path", 702 | args: args{ 703 | extras: []resource.Extra{ 704 | resourceWithFieldPathValue("data.someFloat", 1.3), 705 | resourceWithFieldPathValue("data.someFloat", 1.1), 706 | resourceWithFieldPathValue("data.someFloat", 1.2), 707 | resourceWithFieldPathValue("data.someFloat", 1.4), 708 | }, 709 | path: "data.someFloat", 710 | }, 711 | want: want{ 712 | extras: []resource.Extra{ 713 | resourceWithFieldPathValue("data.someFloat", 1.1), 714 | resourceWithFieldPathValue("data.someFloat", 1.2), 715 | resourceWithFieldPathValue("data.someFloat", 1.3), 716 | resourceWithFieldPathValue("data.someFloat", 1.4), 717 | }, 718 | }, 719 | }, 720 | "InconsistentTypeSortByInt": { 721 | reason: "The Function should sort the Extras by the int value at the specified field path", 722 | args: args{ 723 | extras: []resource.Extra{ 724 | resourceWithFieldPathValue("data.someInt", 3), 725 | resourceWithFieldPathValue("data.someInt", 1), 726 | resourceWithFieldPathValue("data.someInt", "2"), 727 | }, 728 | path: "data.someInt", 729 | }, 730 | want: want{ 731 | err: cmpopts.AnyError, 732 | }, 733 | }, 734 | "EmptyPath": { 735 | reason: "The Function should return an error if the path is empty", 736 | args: args{ 737 | extras: []resource.Extra{ 738 | resourceWithFieldPathValue("metadata.name", "c"), 739 | resourceWithFieldPathValue("metadata.name", "a"), 740 | resourceWithFieldPathValue("metadata.name", "b"), 741 | }, 742 | path: "", 743 | }, 744 | want: want{ 745 | err: cmpopts.AnyError, 746 | }, 747 | }, 748 | "InvalidPathAll": { 749 | reason: "The Function should return no error if the path is invalid for all resources", 750 | args: args{ 751 | extras: []resource.Extra{ 752 | resourceWithFieldPathValue("metadata.name", "c"), 753 | resourceWithFieldPathValue("metadata.name", "a"), 754 | resourceWithFieldPathValue("metadata.name", "b"), 755 | }, 756 | path: "metadata.invalid", 757 | }, 758 | want: want{ 759 | extras: []resource.Extra{ 760 | resourceWithFieldPathValue("metadata.name", "c"), 761 | resourceWithFieldPathValue("metadata.name", "a"), 762 | resourceWithFieldPathValue("metadata.name", "b"), 763 | }, 764 | }, 765 | }, 766 | "InvalidPathSome": { 767 | reason: "The Function should return no error if the path is invalid for some resources, just use the rest of the resources zero value", 768 | args: args{ 769 | extras: []resource.Extra{ 770 | resourceWithFieldPathValue("metadata.name", "c"), 771 | resourceWithFieldPathValue("metadata.invalid", "a"), 772 | resourceWithFieldPathValue("metadata.name", "b"), 773 | }, 774 | path: "metadata.name", 775 | }, 776 | want: want{ 777 | extras: []resource.Extra{ 778 | resourceWithFieldPathValue("metadata.invalid", "a"), 779 | resourceWithFieldPathValue("metadata.name", "b"), 780 | resourceWithFieldPathValue("metadata.name", "c"), 781 | }, 782 | }, 783 | }, 784 | } 785 | 786 | for name, tc := range cases { 787 | t.Run(name, func(t *testing.T) { 788 | got := sortExtrasByFieldPath(tc.args.extras, tc.args.path) 789 | if diff := cmp.Diff(tc.want.err, got, cmpopts.EquateErrors()); diff != "" { 790 | t.Errorf("%s\n(...): -want err, +got err:\n%s", tc.reason, diff) 791 | } 792 | if tc.want.err != nil { 793 | return 794 | } 795 | if diff := cmp.Diff(tc.want.extras, tc.args.extras); diff != "" { 796 | t.Errorf("%s\n(...): -want, +got:\n%s", tc.reason, diff) 797 | } 798 | }) 799 | } 800 | } 801 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crossplane-contrib/function-extra-resources 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/alecthomas/kong v0.8.1 9 | github.com/crossplane/crossplane-runtime v1.20.0 10 | github.com/crossplane/function-sdk-go v0.5.0-rc.0.0.20250608193828-e1a38586f605 11 | github.com/google/go-cmp v0.7.0 12 | google.golang.org/protobuf v1.36.6 13 | k8s.io/apimachinery v0.33.1 14 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 15 | sigs.k8s.io/controller-tools v0.16.0 16 | ) 17 | 18 | require ( 19 | dario.cat/mergo v1.0.1 // indirect 20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 21 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 22 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 23 | github.com/fatih/color v1.17.0 // indirect 24 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 25 | github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 // indirect 26 | github.com/go-logr/logr v1.4.3 // indirect 27 | github.com/go-logr/zapr v1.3.0 // indirect 28 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 29 | github.com/go-openapi/jsonreference v0.20.2 // indirect 30 | github.com/go-openapi/swag v0.23.0 // indirect 31 | github.com/gobuffalo/flect v1.0.2 // indirect 32 | github.com/gogo/protobuf v1.3.2 // indirect 33 | github.com/golang/protobuf v1.5.4 // indirect 34 | github.com/google/gnostic-models v0.6.9 // indirect 35 | github.com/google/uuid v1.6.0 // indirect 36 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 37 | github.com/josharian/intern v1.0.0 // indirect 38 | github.com/json-iterator/go v1.1.12 // indirect 39 | github.com/mailru/easyjson v0.7.7 // indirect 40 | github.com/mattn/go-colorable v0.1.13 // indirect 41 | github.com/mattn/go-isatty v0.0.20 // indirect 42 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 44 | github.com/modern-go/reflect2 v1.0.2 // indirect 45 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 46 | github.com/pkg/errors v0.9.1 // indirect 47 | github.com/spf13/afero v1.11.0 // indirect 48 | github.com/spf13/cobra v1.8.1 // indirect 49 | github.com/spf13/pflag v1.0.5 // indirect 50 | github.com/x448/float16 v0.8.4 // indirect 51 | github.com/yuin/goldmark v1.7.0 // 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.25.0 // indirect 55 | golang.org/x/net v0.41.0 // indirect 56 | golang.org/x/oauth2 v0.28.0 // indirect 57 | golang.org/x/sync v0.15.0 // indirect 58 | golang.org/x/sys v0.33.0 // indirect 59 | golang.org/x/term v0.32.0 // indirect 60 | golang.org/x/text v0.26.0 // indirect 61 | golang.org/x/time v0.9.0 // indirect 62 | golang.org/x/tools v0.33.0 // indirect 63 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect 64 | google.golang.org/grpc v1.73.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/apiextensions-apiserver v0.31.0 // indirect 70 | k8s.io/client-go v0.31.0 // indirect 71 | k8s.io/klog/v2 v2.130.1 // indirect 72 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 73 | sigs.k8s.io/controller-runtime v0.19.0 // indirect 74 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 75 | sigs.k8s.io/randfill v1.0.0 // indirect 76 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 77 | sigs.k8s.io/yaml v1.4.0 // indirect 78 | ) 79 | -------------------------------------------------------------------------------- /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.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= 6 | github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= 7 | github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= 8 | github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= 9 | github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= 10 | github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 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.20.0 h1:I54uipRIecqZyms+vz1J/l62yjVQ7HV5w+Nh3RMrUtc= 26 | github.com/crossplane/crossplane-runtime v1.20.0/go.mod h1:lfV1VJenDc9PNVLxDC80YjPoTm+JdSZ13xlS2h37Dvg= 27 | github.com/crossplane/function-sdk-go v0.5.0-rc.0.0.20250608193828-e1a38586f605 h1:IxBrOWvnxvdH4Tg35aGwdYPMUKYuUIWCTzBgF+Je6nE= 28 | github.com/crossplane/function-sdk-go v0.5.0-rc.0.0.20250608193828-e1a38586f605/go.mod h1:k04xrVQb4yIhc+9+AK1dcyshuynA49eSRCYM9RF5m70= 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.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= 38 | github.com/evanphx/json-patch v5.9.11+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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 52 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 53 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 54 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 55 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 56 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 57 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 58 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 59 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 60 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 61 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 62 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 63 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 64 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 65 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 66 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 67 | github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= 68 | github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= 69 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 70 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 71 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 72 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 73 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 74 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 75 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 76 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 77 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 78 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 79 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 80 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 81 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 82 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 83 | github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= 84 | github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 85 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 86 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 87 | github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 h1:Ud/6/AdmJ1R7ibdS0Wo5MWPj0T1R0fkpaD087bBaW8I= 88 | github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= 89 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 90 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 91 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 92 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 93 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 94 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 95 | github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= 96 | github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= 97 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= 98 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 99 | github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= 100 | github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= 101 | github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc= 102 | github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= 103 | github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= 104 | github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= 105 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= 106 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 107 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= 108 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= 109 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 110 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 111 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 112 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 113 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 114 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 115 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 116 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 117 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 118 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 119 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 120 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 121 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 122 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 123 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 124 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 125 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 126 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 127 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 128 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 129 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 130 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 131 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 132 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 133 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 134 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 135 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 136 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 137 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 138 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 139 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 140 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 141 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 142 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 143 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= 144 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 145 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 146 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 147 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 148 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 149 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 150 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 151 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 152 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 153 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 154 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 155 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 156 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 157 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 158 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 159 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 160 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 161 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 162 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 163 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 164 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 165 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 166 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 167 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 168 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 169 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 170 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 171 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 172 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 173 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 174 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 175 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 176 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 177 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 178 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 179 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 180 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 181 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 182 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 183 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 184 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 185 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 186 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 187 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 188 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 189 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 190 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 191 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 192 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 193 | github.com/tmccombs/hcl2json v0.3.3 h1:+DLNYqpWE0CsOQiEZu+OZm5ZBImake3wtITYxQ8uLFQ= 194 | github.com/tmccombs/hcl2json v0.3.3/go.mod h1:Y2chtz2x9bAeRTvSibVRVgbLJhLJXKlUeIvjeVdnm4w= 195 | github.com/upbound/provider-aws v1.14.0 h1:DDUdlMp+dNlFXXlhsGdCvQD7qFdT1AsEcaqlRU3BO14= 196 | github.com/upbound/provider-aws v1.14.0/go.mod h1:IvyvgGlhRVr737E4P75tyD/i53hxnyO7KPM8bbXH+SU= 197 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 198 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 199 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 200 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 201 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 202 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 203 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 204 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 205 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 206 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 207 | github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= 208 | github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 209 | github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= 210 | github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 211 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 212 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 213 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 214 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 215 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 216 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 217 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 218 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 219 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 220 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 221 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 222 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 223 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 224 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 225 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 226 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 227 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 228 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 229 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 230 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 231 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 232 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= 233 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 234 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 235 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 236 | golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= 237 | golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 238 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 239 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 240 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 241 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 242 | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= 243 | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 244 | golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 245 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 246 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 247 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 248 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 249 | golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= 250 | golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 251 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 252 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 255 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 256 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 257 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 258 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 259 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 260 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 261 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 262 | golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= 263 | golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 264 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 265 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 266 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 267 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 268 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 269 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 270 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 271 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 272 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 273 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 274 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 275 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 276 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 277 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 278 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 279 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 280 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= 281 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 282 | google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= 283 | google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 284 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 285 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 286 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 287 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 288 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 289 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 290 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 291 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 292 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 293 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 294 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 295 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 296 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 297 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 298 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 299 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 300 | k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= 301 | k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= 302 | k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= 303 | k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= 304 | k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= 305 | k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 306 | k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= 307 | k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= 308 | k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs= 309 | k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo= 310 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 311 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 312 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 313 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 314 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 315 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 316 | sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= 317 | sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= 318 | sigs.k8s.io/controller-tools v0.16.0 h1:EJPB+a5Bve861SPBPPWRbP6bbKyNxqK12oYT5zEns9s= 319 | sigs.k8s.io/controller-tools v0.16.0/go.mod h1:0I0xqjR65YTfoO12iR+mZR6s6UAVcUARgXRlsu0ljB0= 320 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 321 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 322 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 323 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 324 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 325 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 326 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 327 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 328 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 329 | -------------------------------------------------------------------------------- /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://go.dev/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 | -------------------------------------------------------------------------------- /input/v1beta1/input.go: -------------------------------------------------------------------------------- 1 | // Package v1beta1 contains the input type for this Function 2 | // +kubebuilder:object:generate=true 3 | // +groupName=extra-resources.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 is the input to this function. 26 | Spec InputSpec `json:"spec,omitempty"` 27 | } 28 | -------------------------------------------------------------------------------- /input/v1beta1/resource_select.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 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 21 | ) 22 | 23 | // An InputSpec specifies extra resource(s) for rendering composed resources. 24 | type InputSpec struct { 25 | // ExtraResources selects a list of `ExtraResource`s. The resolved 26 | // resources are stored in the composite resource at 27 | // `spec.extraResourceRefs` and is only updated if it is null. 28 | ExtraResources []ResourceSource `json:"extraResources"` 29 | 30 | // Policy represents the Resolution policies which apply to all 31 | // ResourceSourceReferences in ExtraResources list. 32 | // +optional 33 | Policy *Policy `json:"policy,omitempty"` 34 | } 35 | 36 | // Policy represents the Resolution policy of Reference instance. 37 | type Policy struct { 38 | // Resolution specifies whether resolution of this reference is required. 39 | // The default is 'Required', which means the reconcile will fail if the 40 | // reference cannot be resolved. 'Optional' means this reference will be 41 | // a no-op if it cannot be resolved. 42 | // +optional 43 | // +kubebuilder:default=Required 44 | // +kubebuilder:validation:Enum=Required;Optional 45 | Resolution *xpv1.ResolutionPolicy `json:"resolution,omitempty"` 46 | } 47 | 48 | // IsResolutionPolicyOptional checks whether the resolution policy of relevant 49 | // reference is Optional. 50 | func (p *Policy) IsResolutionPolicyOptional() bool { 51 | if p == nil || p.Resolution == nil { 52 | return false 53 | } 54 | 55 | return *p.Resolution == xpv1.ResolutionPolicyOptional 56 | } 57 | 58 | // ResourceSourceType specifies the way the ExtraResource is selected. 59 | type ResourceSourceType string 60 | 61 | const ( 62 | // ResourceSourceTypeReference by name. 63 | ResourceSourceTypeReference ResourceSourceType = "Reference" 64 | // ResourceSourceTypeSelector by labels. 65 | ResourceSourceTypeSelector ResourceSourceType = "Selector" 66 | ) 67 | 68 | // ResourceSource selects a ExtraResource. 69 | type ResourceSource struct { 70 | // Type specifies the way the ExtraResource is selected. 71 | // Default is `Reference` 72 | // +optional 73 | // +kubebuilder:validation:Enum=Reference;Selector 74 | // +kubebuilder:default=Reference 75 | Type ResourceSourceType `json:"type,omitempty"` 76 | 77 | // Ref is a named reference to a single ExtraResource. 78 | // Either Ref or Selector is required. 79 | // +optional 80 | Ref *ResourceSourceReference `json:"ref,omitempty"` 81 | 82 | // Selector selects ExtraResource(s) via labels. 83 | // +optional 84 | Selector *ResourceSourceSelector `json:"selector,omitempty"` 85 | 86 | // Kind is the kubernetes kind of the target extra resource(s). 87 | Kind string `json:"kind,omitempty"` 88 | 89 | // APIVersion is the kubernetes API Version of the target extra resource(s). 90 | APIVersion string `json:"apiVersion,omitempty"` 91 | 92 | // Namespace is the namespace in which to look for the ExtraResource. 93 | // If not set, the resource is assumed to be cluster-scoped. 94 | // +optional 95 | Namespace *string `json:"namespace,omitempty"` 96 | 97 | // Into is the key into which extra resources for this selector will be placed. 98 | Into string `json:"into"` 99 | } 100 | 101 | // GetType returns the type of the resource source, returning the default if not set. 102 | func (e *ResourceSource) GetType() ResourceSourceType { 103 | if e == nil || e.Type == "" { 104 | return ResourceSourceTypeReference 105 | } 106 | return e.Type 107 | } 108 | 109 | // An ResourceSourceReference references an ExtraResource by it's name. 110 | type ResourceSourceReference struct { 111 | // The name of the object. 112 | Name string `json:"name"` 113 | } 114 | 115 | // An ResourceSourceSelector selects an ExtraResource via labels. 116 | type ResourceSourceSelector struct { 117 | // MaxMatch specifies the number of extracted ExtraResources in Multiple mode, extracts all if nil. 118 | MaxMatch *uint64 `json:"maxMatch,omitempty"` 119 | 120 | // MinMatch specifies the required minimum of extracted ExtraResources in Multiple mode. 121 | MinMatch *uint64 `json:"minMatch,omitempty"` 122 | 123 | // SortByFieldPath is the path to the field based on which list of ExtraResources is alphabetically sorted. 124 | // +kubebuilder:default="metadata.name" 125 | SortByFieldPath string `json:"sortByFieldPath,omitempty"` 126 | 127 | // MatchLabels ensures an object with matching labels is selected. 128 | MatchLabels []ResourceSourceSelectorLabelMatcher `json:"matchLabels,omitempty"` 129 | } 130 | 131 | // GetSortByFieldPath returns the sort by path if set or a sane default. 132 | func (e *ResourceSourceSelector) GetSortByFieldPath() string { 133 | if e == nil || e.SortByFieldPath == "" { 134 | return "metadata.name" 135 | } 136 | return e.SortByFieldPath 137 | } 138 | 139 | // ResourceSourceSelectorLabelMatcherType specifies where the value for a label comes from. 140 | type ResourceSourceSelectorLabelMatcherType string 141 | 142 | const ( 143 | // ResourceSourceSelectorLabelMatcherTypeFromCompositeFieldPath extracts 144 | // the label value from a composite fieldpath. 145 | ResourceSourceSelectorLabelMatcherTypeFromCompositeFieldPath ResourceSourceSelectorLabelMatcherType = "FromCompositeFieldPath" 146 | // ResourceSourceSelectorLabelMatcherTypeValue uses a literal as label 147 | // value. 148 | ResourceSourceSelectorLabelMatcherTypeValue ResourceSourceSelectorLabelMatcherType = "Value" 149 | ) 150 | 151 | // An ResourceSourceSelectorLabelMatcher acts like a k8s label selector but 152 | // can draw the label value from a different path. 153 | type ResourceSourceSelectorLabelMatcher struct { 154 | // Type specifies where the value for a label comes from. 155 | // +optional 156 | // +kubebuilder:validation:Enum=FromCompositeFieldPath;Value 157 | // +kubebuilder:default=FromCompositeFieldPath 158 | Type ResourceSourceSelectorLabelMatcherType `json:"type,omitempty"` 159 | 160 | // Key of the label to match. 161 | Key string `json:"key"` 162 | 163 | // ValueFromFieldPath specifies the field path to look for the label value. 164 | ValueFromFieldPath *string `json:"valueFromFieldPath,omitempty"` 165 | 166 | // FromFieldPathPolicy specifies the policy for the valueFromFieldPath. 167 | // The default is Required, meaning that an error will be returned if the 168 | // field is not found in the composite resource. 169 | // Optional means that if the field is not found in the composite resource, 170 | // that label pair will just be skipped. N.B. other specified label 171 | // matchers will still be used to retrieve the desired 172 | // resource config, if any. 173 | // +kubebuilder:validation:Enum=Optional;Required 174 | // +kubebuilder:default=Required 175 | FromFieldPathPolicy *FromFieldPathPolicy `json:"fromFieldPathPolicy,omitempty"` 176 | 177 | // Value specifies a literal label value. 178 | Value *string `json:"value,omitempty"` 179 | } 180 | 181 | // FromFieldPathIsOptional returns true if the FromFieldPathPolicy is set to 182 | // +optional 183 | func (e *ResourceSourceSelectorLabelMatcher) FromFieldPathIsOptional() bool { 184 | return e.FromFieldPathPolicy != nil && *e.FromFieldPathPolicy == FromFieldPathPolicyOptional 185 | } 186 | 187 | // GetType returns the type of the label matcher, returning the default if not set. 188 | func (e *ResourceSourceSelectorLabelMatcher) GetType() ResourceSourceSelectorLabelMatcherType { 189 | if e == nil || e.Type == "" { 190 | return ResourceSourceSelectorLabelMatcherTypeFromCompositeFieldPath 191 | } 192 | return e.Type 193 | } 194 | 195 | // A FromFieldPathPolicy determines how to patch from a field path. 196 | type FromFieldPathPolicy string 197 | 198 | // FromFieldPath patch policies. 199 | const ( 200 | FromFieldPathPolicyOptional FromFieldPathPolicy = "Optional" 201 | FromFieldPathPolicyRequired FromFieldPathPolicy = "Required" 202 | ) 203 | 204 | // A PatchPolicy configures the specifics of patching behaviour. 205 | type PatchPolicy struct { 206 | // FromFieldPath specifies how to patch from a field path. The default is 207 | // 'Optional', which means the patch will be a no-op if the specified 208 | // fromFieldPath does not exist. Use 'Required' if the patch should fail if 209 | // the specified path does not exist. 210 | // +kubebuilder:validation:Enum=Optional;Required 211 | // +optional 212 | FromFieldPath *FromFieldPathPolicy `json:"fromFieldPath,omitempty"` 213 | MergeOptions *xpv1.MergeOptions `json:"mergeOptions,omitempty"` 214 | } 215 | 216 | // GetFromFieldPathPolicy returns the FromFieldPathPolicy for this PatchPolicy, defaulting to FromFieldPathPolicyOptional if not specified. 217 | func (pp *PatchPolicy) GetFromFieldPathPolicy() FromFieldPathPolicy { 218 | if pp == nil || pp.FromFieldPath == nil { 219 | return FromFieldPathPolicyOptional 220 | } 221 | return *pp.FromFieldPath 222 | } 223 | -------------------------------------------------------------------------------- /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 | "github.com/crossplane/crossplane-runtime/apis/common/v1" 9 | runtime "k8s.io/apimachinery/pkg/runtime" 10 | ) 11 | 12 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 13 | func (in *Input) DeepCopyInto(out *Input) { 14 | *out = *in 15 | out.TypeMeta = in.TypeMeta 16 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 17 | in.Spec.DeepCopyInto(&out.Spec) 18 | } 19 | 20 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Input. 21 | func (in *Input) DeepCopy() *Input { 22 | if in == nil { 23 | return nil 24 | } 25 | out := new(Input) 26 | in.DeepCopyInto(out) 27 | return out 28 | } 29 | 30 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 31 | func (in *Input) DeepCopyObject() runtime.Object { 32 | if c := in.DeepCopy(); c != nil { 33 | return c 34 | } 35 | return nil 36 | } 37 | 38 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 39 | func (in *InputSpec) DeepCopyInto(out *InputSpec) { 40 | *out = *in 41 | if in.ExtraResources != nil { 42 | in, out := &in.ExtraResources, &out.ExtraResources 43 | *out = make([]ResourceSource, len(*in)) 44 | for i := range *in { 45 | (*in)[i].DeepCopyInto(&(*out)[i]) 46 | } 47 | } 48 | if in.Policy != nil { 49 | in, out := &in.Policy, &out.Policy 50 | *out = new(Policy) 51 | (*in).DeepCopyInto(*out) 52 | } 53 | } 54 | 55 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InputSpec. 56 | func (in *InputSpec) DeepCopy() *InputSpec { 57 | if in == nil { 58 | return nil 59 | } 60 | out := new(InputSpec) 61 | in.DeepCopyInto(out) 62 | return out 63 | } 64 | 65 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 66 | func (in *PatchPolicy) DeepCopyInto(out *PatchPolicy) { 67 | *out = *in 68 | if in.FromFieldPath != nil { 69 | in, out := &in.FromFieldPath, &out.FromFieldPath 70 | *out = new(FromFieldPathPolicy) 71 | **out = **in 72 | } 73 | if in.MergeOptions != nil { 74 | in, out := &in.MergeOptions, &out.MergeOptions 75 | *out = new(v1.MergeOptions) 76 | (*in).DeepCopyInto(*out) 77 | } 78 | } 79 | 80 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchPolicy. 81 | func (in *PatchPolicy) DeepCopy() *PatchPolicy { 82 | if in == nil { 83 | return nil 84 | } 85 | out := new(PatchPolicy) 86 | in.DeepCopyInto(out) 87 | return out 88 | } 89 | 90 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 91 | func (in *Policy) DeepCopyInto(out *Policy) { 92 | *out = *in 93 | if in.Resolution != nil { 94 | in, out := &in.Resolution, &out.Resolution 95 | *out = new(v1.ResolutionPolicy) 96 | **out = **in 97 | } 98 | } 99 | 100 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy. 101 | func (in *Policy) DeepCopy() *Policy { 102 | if in == nil { 103 | return nil 104 | } 105 | out := new(Policy) 106 | in.DeepCopyInto(out) 107 | return out 108 | } 109 | 110 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 111 | func (in *ResourceSource) DeepCopyInto(out *ResourceSource) { 112 | *out = *in 113 | if in.Ref != nil { 114 | in, out := &in.Ref, &out.Ref 115 | *out = new(ResourceSourceReference) 116 | **out = **in 117 | } 118 | if in.Selector != nil { 119 | in, out := &in.Selector, &out.Selector 120 | *out = new(ResourceSourceSelector) 121 | (*in).DeepCopyInto(*out) 122 | } 123 | if in.Namespace != nil { 124 | in, out := &in.Namespace, &out.Namespace 125 | *out = new(string) 126 | **out = **in 127 | } 128 | } 129 | 130 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSource. 131 | func (in *ResourceSource) DeepCopy() *ResourceSource { 132 | if in == nil { 133 | return nil 134 | } 135 | out := new(ResourceSource) 136 | in.DeepCopyInto(out) 137 | return out 138 | } 139 | 140 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 141 | func (in *ResourceSourceReference) DeepCopyInto(out *ResourceSourceReference) { 142 | *out = *in 143 | } 144 | 145 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSourceReference. 146 | func (in *ResourceSourceReference) DeepCopy() *ResourceSourceReference { 147 | if in == nil { 148 | return nil 149 | } 150 | out := new(ResourceSourceReference) 151 | in.DeepCopyInto(out) 152 | return out 153 | } 154 | 155 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 156 | func (in *ResourceSourceSelector) DeepCopyInto(out *ResourceSourceSelector) { 157 | *out = *in 158 | if in.MaxMatch != nil { 159 | in, out := &in.MaxMatch, &out.MaxMatch 160 | *out = new(uint64) 161 | **out = **in 162 | } 163 | if in.MinMatch != nil { 164 | in, out := &in.MinMatch, &out.MinMatch 165 | *out = new(uint64) 166 | **out = **in 167 | } 168 | if in.MatchLabels != nil { 169 | in, out := &in.MatchLabels, &out.MatchLabels 170 | *out = make([]ResourceSourceSelectorLabelMatcher, len(*in)) 171 | for i := range *in { 172 | (*in)[i].DeepCopyInto(&(*out)[i]) 173 | } 174 | } 175 | } 176 | 177 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSourceSelector. 178 | func (in *ResourceSourceSelector) DeepCopy() *ResourceSourceSelector { 179 | if in == nil { 180 | return nil 181 | } 182 | out := new(ResourceSourceSelector) 183 | in.DeepCopyInto(out) 184 | return out 185 | } 186 | 187 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 188 | func (in *ResourceSourceSelectorLabelMatcher) DeepCopyInto(out *ResourceSourceSelectorLabelMatcher) { 189 | *out = *in 190 | if in.ValueFromFieldPath != nil { 191 | in, out := &in.ValueFromFieldPath, &out.ValueFromFieldPath 192 | *out = new(string) 193 | **out = **in 194 | } 195 | if in.FromFieldPathPolicy != nil { 196 | in, out := &in.FromFieldPathPolicy, &out.FromFieldPathPolicy 197 | *out = new(FromFieldPathPolicy) 198 | **out = **in 199 | } 200 | if in.Value != nil { 201 | in, out := &in.Value, &out.Value 202 | *out = new(string) 203 | **out = **in 204 | } 205 | } 206 | 207 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSourceSelectorLabelMatcher. 208 | func (in *ResourceSourceSelectorLabelMatcher) DeepCopy() *ResourceSourceSelectorLabelMatcher { 209 | if in == nil { 210 | return nil 211 | } 212 | out := new(ResourceSourceSelectorLabelMatcher) 213 | in.DeepCopyInto(out) 214 | return out 215 | } 216 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main implements a Composition Function. 2 | package main 3 | 4 | import ( 5 | "github.com/alecthomas/kong" 6 | 7 | function "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 | } 19 | 20 | // Run this Function. 21 | func (c *CLI) Run() error { 22 | log, err := function.NewLogger(c.Debug) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | return function.Serve(&Function{log: log}, 28 | function.Listen(c.Network, c.Address), 29 | function.MTLSCertificates(c.TLSCertsDir), 30 | function.Insecure(c.Insecure)) 31 | } 32 | 33 | func main() { 34 | ctx := kong.Parse(&CLI{}, kong.Description("A Crossplane Composition Function to retrieve extra resources to the context.")) 35 | ctx.FatalIfErrorf(ctx.Run()) 36 | } 37 | -------------------------------------------------------------------------------- /package/crossplane.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: meta.pkg.crossplane.io/v1beta1 3 | kind: Function 4 | metadata: 5 | name: function-extra-resources 6 | annotations: 7 | meta.crossplane.io/maintainer: Crossplane Maintainers 8 | meta.crossplane.io/source: github.com/crossplane-contrib/function-extra-resources 9 | meta.crossplane.io/license: Apache-2.0 10 | meta.crossplane.io/description: | 11 | A composition function that can retrieve arbitrary cluster-scoped extra resources and add them to the context. 12 | meta.crossplane.io/readme: | 13 | This composition function retrives arbitrary cluster-scoped extra 14 | resources and add them in the specified order into the Context at a 15 | well-known key, apiextensions.crossplane.io/extra-resources, so that they 16 | can be used by other functions. 17 | spec: 18 | crossplane: 19 | version: ">=v2.0.0-0" 20 | -------------------------------------------------------------------------------- /package/input/extra-resources.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.16.0 7 | name: inputs.extra-resources.fn.crossplane.io 8 | spec: 9 | group: extra-resources.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: Spec is the input to this function. 43 | properties: 44 | extraResources: 45 | description: |- 46 | ExtraResources selects a list of `ExtraResource`s. The resolved 47 | resources are stored in the composite resource at 48 | `spec.extraResourceRefs` and is only updated if it is null. 49 | items: 50 | description: ResourceSource selects a ExtraResource. 51 | properties: 52 | apiVersion: 53 | description: APIVersion is the kubernetes API Version of the 54 | target extra resource(s). 55 | type: string 56 | into: 57 | description: Into is the key into which extra resources for 58 | this selector will be placed. 59 | type: string 60 | kind: 61 | description: Kind is the kubernetes kind of the target extra 62 | resource(s). 63 | type: string 64 | namespace: 65 | description: |- 66 | Namespace is the namespace in which to look for the ExtraResource. 67 | If not set, the resource is assumed to be cluster-scoped. 68 | type: string 69 | ref: 70 | description: |- 71 | Ref is a named reference to a single ExtraResource. 72 | Either Ref or Selector is required. 73 | properties: 74 | name: 75 | description: The name of the object. 76 | type: string 77 | required: 78 | - name 79 | type: object 80 | selector: 81 | description: Selector selects ExtraResource(s) via labels. 82 | properties: 83 | matchLabels: 84 | description: MatchLabels ensures an object with matching 85 | labels is selected. 86 | items: 87 | description: |- 88 | An ResourceSourceSelectorLabelMatcher acts like a k8s label selector but 89 | can draw the label value from a different path. 90 | properties: 91 | fromFieldPathPolicy: 92 | default: Required 93 | description: |- 94 | FromFieldPathPolicy specifies the policy for the valueFromFieldPath. 95 | The default is Required, meaning that an error will be returned if the 96 | field is not found in the composite resource. 97 | Optional means that if the field is not found in the composite resource, 98 | that label pair will just be skipped. N.B. other specified label 99 | matchers will still be used to retrieve the desired 100 | resource config, if any. 101 | enum: 102 | - Optional 103 | - Required 104 | type: string 105 | key: 106 | description: Key of the label to match. 107 | type: string 108 | type: 109 | default: FromCompositeFieldPath 110 | description: Type specifies where the value for a 111 | label comes from. 112 | enum: 113 | - FromCompositeFieldPath 114 | - Value 115 | type: string 116 | value: 117 | description: Value specifies a literal label value. 118 | type: string 119 | valueFromFieldPath: 120 | description: ValueFromFieldPath specifies the field 121 | path to look for the label value. 122 | type: string 123 | required: 124 | - key 125 | type: object 126 | type: array 127 | maxMatch: 128 | description: MaxMatch specifies the number of extracted 129 | ExtraResources in Multiple mode, extracts all if nil. 130 | format: int64 131 | type: integer 132 | minMatch: 133 | description: MinMatch specifies the required minimum of 134 | extracted ExtraResources in Multiple mode. 135 | format: int64 136 | type: integer 137 | sortByFieldPath: 138 | default: metadata.name 139 | description: SortByFieldPath is the path to the field based 140 | on which list of ExtraResources is alphabetically sorted. 141 | type: string 142 | type: object 143 | type: 144 | default: Reference 145 | description: |- 146 | Type specifies the way the ExtraResource is selected. 147 | Default is `Reference` 148 | enum: 149 | - Reference 150 | - Selector 151 | type: string 152 | required: 153 | - into 154 | type: object 155 | type: array 156 | policy: 157 | description: |- 158 | Policy represents the Resolution policies which apply to all 159 | ResourceSourceReferences in ExtraResources list. 160 | properties: 161 | resolution: 162 | default: Required 163 | description: |- 164 | Resolution specifies whether resolution of this reference is required. 165 | The default is 'Required', which means the reconcile will fail if the 166 | reference cannot be resolved. 'Optional' means this reference will be 167 | a no-op if it cannot be resolved. 168 | enum: 169 | - Required 170 | - Optional 171 | type: string 172 | type: object 173 | required: 174 | - extraResources 175 | type: object 176 | type: object 177 | served: true 178 | storage: true 179 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "crossplane": { 7 | "fileMatch": ["(^|/)example/.*\\.ya?ml$"] 8 | }, 9 | "packageRules": [ 10 | { 11 | "matchManagers": ["crossplane"], 12 | "matchFileNames": ["example/**"], 13 | "groupName": "examples" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # It seems extra-resources must be in only one file. 3 | # Supplying the argument a second time excludes any input from the 4 | # first --extra-resources argument 5 | crossplane render \ 6 | --extra-resources example/extraResources.yaml \ 7 | --include-context \ 8 | example/xr.yaml example/composition.yaml example/functions.yaml 9 | --------------------------------------------------------------------------------