├── .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.23.6' 18 | GOLANGCI_VERSION: 'v1.62.0' 19 | DOCKER_BUILDX_VERSION: 'v0.23.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: master # TODO(negz): Pin to stable once v1.14 is released. 24 | XP_VERSION: current # TODO(negz): Pin to a version once v1.14 is released. 25 | 26 | # This CI job will automatically push new builds to xpkg.upbound.io if the 27 | # XPKG_ACCESS_ID and XPKG_TOKEN secrets are set in the GitHub respository (or 28 | # organization) settings. Create a token at https://accounts.upbound.io. 29 | XPKG_ACCESS_ID: ${{ secrets.XPKG_ACCESS_ID }} 30 | 31 | # The package to push, without a version tag. The default matches GitHub. For 32 | # example xpkg.upbound.io/crossplane/function-template-go. 33 | XPKG: xpkg.upbound.io/${{ github.repository}} 34 | CROSSPLANE_REGORG: ghcr.io/${{ github.repository}} # xpkg.crossplane.io/crossplane-contrib 35 | 36 | # The package version to push. The default is 0.0.0-gitsha. 37 | XPKG_VERSION: ${{ inputs.version }} 38 | 39 | jobs: 40 | lint: 41 | runs-on: ubuntu-24.04 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | 46 | - name: Setup Go 47 | uses: actions/setup-go@v5 48 | with: 49 | go-version: ${{ env.GO_VERSION }} 50 | cache: false # The golangci-lint action does its own caching. 51 | 52 | - name: Check go mod tidy 53 | run: go mod tidy && git diff --exit-code go.mod go.sum 54 | 55 | - name: Lint 56 | uses: golangci/golangci-lint-action@v6 57 | with: 58 | version: ${{ env.GOLANGCI_VERSION }} 59 | 60 | unit-test: 61 | runs-on: ubuntu-24.04 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | 66 | - name: Setup Go 67 | uses: actions/setup-go@v5 68 | with: 69 | go-version: ${{ env.GO_VERSION }} 70 | 71 | - name: Run Unit Tests 72 | run: go test -v -cover ./... 73 | 74 | # We want to build most packages for the amd64 and arm64 architectures. To 75 | # speed this up we build single-platform packages in parallel. We then upload 76 | # those packages to GitHub as a build artifact. The push job downloads those 77 | # artifacts and pushes them as a single multi-platform package. 78 | build: 79 | runs-on: ubuntu-24.04 80 | strategy: 81 | fail-fast: true 82 | matrix: 83 | arch: 84 | - amd64 85 | - arm64 86 | steps: 87 | - name: Setup QEMU 88 | uses: docker/setup-qemu-action@v3 89 | with: 90 | platforms: all 91 | 92 | - name: Setup Docker Buildx 93 | uses: docker/setup-buildx-action@v3 94 | with: 95 | version: ${{ env.DOCKER_BUILDX_VERSION }} 96 | install: true 97 | 98 | - name: Checkout 99 | uses: actions/checkout@v4 100 | 101 | # We ask Docker to use GitHub Action's native caching support to speed up 102 | # the build, per https://docs.docker.com/build/cache/backends/gha/. 103 | - name: Build Runtime 104 | id: image 105 | uses: docker/build-push-action@v6 106 | with: 107 | context: . 108 | platforms: linux/${{ matrix.arch }} 109 | cache-from: type=gha 110 | cache-to: type=gha,mode=max 111 | target: image 112 | build-args: 113 | GO_VERSION=${{ env.GO_VERSION }} 114 | outputs: type=docker,dest=runtime-${{ matrix.arch }}.tar 115 | 116 | - name: Setup the Crossplane CLI 117 | run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh" 118 | 119 | - name: Build Package 120 | run: ./crossplane xpkg build --package-file=${{ matrix.arch }}.xpkg --package-root=package/ --embed-runtime-image-tarball=runtime-${{ matrix.arch }}.tar 121 | 122 | - name: Upload Single-Platform Package 123 | uses: actions/upload-artifact@v4 124 | with: 125 | name: package-${{ matrix.arch }} 126 | path: "*.xpkg" 127 | if-no-files-found: error 128 | retention-days: 1 129 | 130 | # This job downloads the single-platform packages built by the build job, and 131 | # pushes them as a multi-platform package. We only push the package it the 132 | # XPKG_ACCESS_ID and XPKG_TOKEN secrets were provided. 133 | push: 134 | runs-on: ubuntu-24.04 135 | needs: 136 | - build 137 | steps: 138 | - name: Checkout 139 | uses: actions/checkout@v4 140 | 141 | - name: Download Single-Platform Packages 142 | uses: actions/download-artifact@v4 143 | with: 144 | path: . 145 | merge-multiple: true 146 | pattern: "!*.dockerbuild" # This gets uploaded by docker/build-push-action but must be skipped: https://github.com/actions/toolkit/pull/1874 147 | 148 | - name: Setup the Crossplane CLI 149 | run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh" 150 | 151 | - name: Login to Upbound 152 | uses: docker/login-action@v3 153 | if: env.XPKG_ACCESS_ID != '' 154 | with: 155 | registry: xpkg.upbound.io 156 | username: ${{ secrets.XPKG_ACCESS_ID }} 157 | password: ${{ secrets.XPKG_TOKEN }} 158 | 159 | # If a version wasn't explicitly passed as a workflow_dispatch input we 160 | # default to version v0.0.0--, for example 161 | # v0.0.0-20231101115142-1091066df799. This is a simple implementation of 162 | # Go's pseudo-versions: https://go.dev/ref/mod#pseudo-versions. 163 | - name: Set Default Multi-Platform Package Version 164 | if: env.XPKG_VERSION == '' 165 | 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 166 | 167 | - name: Push Multi-Platform Package to Upbound 168 | if: env.XPKG_ACCESS_ID != '' 169 | run: "./crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${{ env.XPKG }}:${{ env.XPKG_VERSION }}" 170 | 171 | - name: Login to GHCR 172 | uses: docker/login-action@v3.3.0 173 | with: 174 | registry: ghcr.io 175 | username: ${{ github.repository_owner }} 176 | password: ${{ secrets.GITHUB_TOKEN }} 177 | 178 | - name: Push Multi-Platform Package to GHCR 179 | if: env.XPKG_ACCESS_ID != '' 180 | run: "./crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${{ env.CROSSPLANE_REGORG }}:${{ env.XPKG_VERSION }}" 181 | -------------------------------------------------------------------------------- /.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 | 20 | # Go workspace file 21 | go.work 22 | tmp 23 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 10m 3 | 4 | output: 5 | # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" 6 | formats: 7 | - format: colored-line-number 8 | 9 | linters-settings: 10 | errcheck: 11 | # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 12 | # default is false: such cases aren't reported by default. 13 | check-type-assertions: false 14 | 15 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 16 | # default is false: such cases aren't reported by default. 17 | check-blank: false 18 | 19 | # List of functions to exclude from checking, where each entry is a single function to exclude. 20 | # See https://github.com/kisielk/errcheck#excluding-functions for details. 21 | exclude-functions: 22 | - io/ioutil.ReadFile 23 | 24 | govet: 25 | # report about shadowed variables 26 | disable: 27 | - shadow 28 | 29 | gofmt: 30 | # simplify code: gofmt with `-s` option, true by default 31 | simplify: true 32 | 33 | gci: 34 | custom-order: true 35 | sections: 36 | - standard 37 | - default 38 | - prefix(github.com/crossplane) 39 | - prefix(github.com/crossplane-contrib) 40 | - blank 41 | - dot 42 | 43 | gocyclo: 44 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 45 | min-complexity: 10 46 | 47 | dupl: 48 | # tokens count to trigger issue, 150 by default 49 | threshold: 100 50 | 51 | goconst: 52 | # minimal length of string constant, 3 by default 53 | min-len: 3 54 | # minimal occurrences count to trigger, 3 by default 55 | min-occurrences: 5 56 | 57 | lll: 58 | # tab width in spaces. Default to 1. 59 | tab-width: 1 60 | 61 | unparam: 62 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 63 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 64 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 65 | # with golangci-lint call it on a directory with the changed file. 66 | check-exported: false 67 | 68 | nakedret: 69 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 70 | max-func-lines: 30 71 | 72 | prealloc: 73 | # XXX: we don't recommend using this linter before doing performance profiling. 74 | # For most programs usage of prealloc will be a premature optimization. 75 | 76 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 77 | # True by default. 78 | simple: true 79 | range-loops: true # Report preallocation suggestions on range loops, true by default 80 | for-loops: false # Report preallocation suggestions on for loops, false by default 81 | 82 | gocritic: 83 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks. 84 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 85 | enabled-tags: 86 | - performance 87 | 88 | settings: # settings passed to gocritic 89 | captLocal: # must be valid enabled check name 90 | paramsOnly: true 91 | rangeValCopy: 92 | sizeThreshold: 32 93 | 94 | nolintlint: 95 | require-explanation: true 96 | require-specific: true 97 | 98 | 99 | linters: 100 | enable: 101 | - megacheck 102 | - govet 103 | - gocyclo 104 | - gocritic 105 | - goconst 106 | - gci 107 | - gofmt # We enable this as well as goimports for its simplify mode. 108 | - prealloc 109 | - revive 110 | - unconvert 111 | - misspell 112 | - nakedret 113 | - nolintlint 114 | 115 | disable: 116 | # These linters are all deprecated as of golangci-lint v1.49.0. We disable 117 | # them explicitly to avoid the linter logging deprecation warnings. 118 | - deadcode 119 | - varcheck 120 | - scopelint 121 | - structcheck 122 | - interfacer 123 | 124 | presets: 125 | - bugs 126 | - unused 127 | fast: false 128 | 129 | 130 | issues: 131 | # Excluding configuration per-path and per-linter 132 | exclude-rules: 133 | - path: "zz_generated\\..+\\.go$" 134 | linters: ["all"] 135 | 136 | # Exclude some linters from running on tests files. 137 | - path: _test(ing)?\.go 138 | linters: 139 | - gocyclo 140 | - errcheck 141 | - dupl 142 | - gosec 143 | - scopelint 144 | - unparam 145 | 146 | # Ease some gocritic warnings on test files. 147 | - path: _test\.go 148 | text: "(unnamedResult|exitAfterDefer)" 149 | linters: 150 | - gocritic 151 | 152 | # These are performance optimisations rather than style issues per se. 153 | # They warn when function arguments or range values copy a lot of memory 154 | # rather than using a pointer. 155 | - text: "(hugeParam|rangeValCopy):" 156 | linters: 157 | - gocritic 158 | 159 | # This "TestMain should call os.Exit to set exit code" warning is not clever 160 | # enough to notice that we call a helper method that calls os.Exit. 161 | - text: "SA3000:" 162 | linters: 163 | - staticcheck 164 | 165 | - text: "k8s.io/api/core/v1" 166 | linters: 167 | - goimports 168 | 169 | # This is a "potential hardcoded credentials" warning. It's triggered by 170 | # any variable with 'secret' in the same, and thus hits a lot of false 171 | # positives in Kubernetes land where a Secret is an object type. 172 | - text: "G101:" 173 | linters: 174 | - gosec 175 | - gas 176 | 177 | # This is an 'errors unhandled' warning that duplicates errcheck. 178 | - text: "G104:" 179 | linters: 180 | - gosec 181 | - gas 182 | 183 | # Some k8s dependencies do not have JSON tags on all fields in structs. 184 | - path: k8s.io/ 185 | linters: 186 | - musttag 187 | 188 | # Independently from option `exclude` we use default exclude patterns, 189 | # it can be disabled by this option. To list all 190 | # excluded by default patterns execute `golangci-lint run --help`. 191 | # Default value for this option is true. 192 | exclude-use-default: false 193 | 194 | # Show only new issues: if there are unstaged changes or untracked files, 195 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 196 | # It's a super-useful option for integration of golangci-lint into existing 197 | # large codebase. It's not practical to fix all existing issues at the moment 198 | # of integration: much better don't allow issues in new code. 199 | # Default is false. 200 | new: false 201 | 202 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 203 | max-issues-per-linter: 0 204 | 205 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 206 | max-same-issues: 0 207 | 208 | -------------------------------------------------------------------------------- /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 beta render` 4 | with these example manifests. 5 | 6 | ```shell 7 | # Run the function locally 8 | $ go run . --insecure --debug 9 | ``` 10 | 11 | ```shell 12 | # Then, in another terminal, call it with these example manifests 13 | $ crossplane beta 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 beta render to connect to the function locally. 8 | #render.crossplane.io/runtime: Development 9 | spec: 10 | # This is ignored when using the Development runtime. 11 | package: xpkg.upbound.io/crossplane-contrib/function-extra-resources:v0.0.3 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.4.1 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 | } 132 | case v1beta1.ResourceSourceTypeSelector: 133 | matchLabels := map[string]string{} 134 | for _, selector := range extraResource.Selector.MatchLabels { 135 | switch selector.GetType() { 136 | case v1beta1.ResourceSourceSelectorLabelMatcherTypeValue: 137 | if selector.Value == nil { 138 | return nil, errors.New("Value cannot be nil for type 'Value'") 139 | } 140 | matchLabels[selector.Key] = *selector.Value 141 | case v1beta1.ResourceSourceSelectorLabelMatcherTypeFromCompositeFieldPath: 142 | if selector.ValueFromFieldPath == nil { 143 | return nil, errors.New("ValueFromFieldPath cannot be nil for type 'FromCompositeFieldPath'") 144 | } 145 | value, err := fieldpath.Pave(xr.Resource.Object).GetString(*selector.ValueFromFieldPath) 146 | if err != nil { 147 | if !selector.FromFieldPathIsOptional() { 148 | return nil, errors.Wrapf(err, "cannot get value from field path %q", *selector.ValueFromFieldPath) 149 | } 150 | continue 151 | } 152 | matchLabels[selector.Key] = value 153 | } 154 | } 155 | if len(matchLabels) == 0 { 156 | continue 157 | } 158 | extraResources[extraResName] = &fnv1.ResourceSelector{ 159 | ApiVersion: extraResource.APIVersion, 160 | Kind: extraResource.Kind, 161 | Match: &fnv1.ResourceSelector_MatchLabels{ 162 | MatchLabels: &fnv1.MatchLabels{Labels: matchLabels}, 163 | }, 164 | } 165 | } 166 | } 167 | return &fnv1.Requirements{ExtraResources: extraResources}, nil 168 | } 169 | 170 | // Verify Min/Max and sort extra resources by field path within a single kind. 171 | func verifyAndSortExtras(in *v1beta1.Input, extraResources map[string][]resource.Extra, //nolint:gocyclo // TODO(reedjosh): refactor 172 | ) (cleanedExtras map[string][]unstructured.Unstructured, err error) { 173 | cleanedExtras = make(map[string][]unstructured.Unstructured) 174 | for _, extraResource := range in.Spec.ExtraResources { 175 | extraResName := extraResource.Into 176 | resources, ok := extraResources[extraResName] 177 | if !ok { 178 | return nil, errors.Errorf("cannot find expected extra resource %q", extraResName) 179 | } 180 | switch extraResource.GetType() { 181 | case v1beta1.ResourceSourceTypeReference: 182 | if len(resources) == 0 { 183 | if in.Spec.Policy.IsResolutionPolicyOptional() { 184 | continue 185 | } 186 | return nil, errors.Errorf("Required extra resource %q not found", extraResName) 187 | } 188 | if len(resources) > 1 { 189 | return nil, errors.Errorf("expected exactly one extra resource %q, got %d", extraResName, len(resources)) 190 | } 191 | cleanedExtras[extraResName] = append(cleanedExtras[extraResName], *resources[0].Resource) 192 | 193 | case v1beta1.ResourceSourceTypeSelector: 194 | selector := extraResource.Selector 195 | if selector.MinMatch != nil && uint64(len(resources)) < *selector.MinMatch { 196 | return nil, errors.Errorf("expected at least %d extra resources %q, got %d", *selector.MinMatch, extraResName, len(resources)) 197 | } 198 | if err := sortExtrasByFieldPath(resources, selector.GetSortByFieldPath()); err != nil { 199 | return nil, err 200 | } 201 | if selector.MaxMatch != nil && uint64(len(resources)) > *selector.MaxMatch { 202 | resources = resources[:*selector.MaxMatch] 203 | } 204 | for _, r := range resources { 205 | cleanedExtras[extraResName] = append(cleanedExtras[extraResName], *r.Resource) 206 | } 207 | } 208 | } 209 | return cleanedExtras, nil 210 | } 211 | 212 | // Sort extra resources by field path within a single kind. 213 | func sortExtrasByFieldPath(extras []resource.Extra, path string) error { //nolint:gocyclo // TODO(phisco): refactor 214 | if path == "" { 215 | return errors.New("cannot sort by empty field path") 216 | } 217 | p := make([]struct { 218 | ec resource.Extra 219 | val any 220 | }, len(extras)) 221 | 222 | var t reflect.Type 223 | for i := range extras { 224 | p[i].ec = extras[i] 225 | val, err := fieldpath.Pave(extras[i].Resource.Object).GetValue(path) 226 | if err != nil && !fieldpath.IsNotFound(err) { 227 | return err 228 | } 229 | p[i].val = val 230 | if val == nil { 231 | continue 232 | } 233 | vt := reflect.TypeOf(val) 234 | switch { 235 | case t == nil: 236 | t = vt 237 | case t != vt: 238 | return errors.Errorf("cannot sort values of different types %q and %q", t, vt) 239 | } 240 | } 241 | if t == nil { 242 | // we either have no values or all values are nil, we can just return 243 | return nil 244 | } 245 | 246 | var err error 247 | sort.Slice(p, func(i, j int) bool { 248 | vali, valj := p[i].val, p[j].val 249 | if vali == nil { 250 | vali = reflect.Zero(t).Interface() 251 | } 252 | if valj == nil { 253 | valj = reflect.Zero(t).Interface() 254 | } 255 | switch t.Kind() { //nolint:exhaustive // we only support these types 256 | case reflect.Float64: 257 | return vali.(float64) < valj.(float64) 258 | case reflect.Float32: 259 | return vali.(float32) < valj.(float32) 260 | case reflect.Int64: 261 | return vali.(int64) < valj.(int64) 262 | case reflect.Int32: 263 | return vali.(int32) < valj.(int32) 264 | case reflect.Int16: 265 | return vali.(int16) < valj.(int16) 266 | case reflect.Int8: 267 | return vali.(int8) < valj.(int8) 268 | case reflect.Int: 269 | return vali.(int) < valj.(int) 270 | case reflect.String: 271 | return vali.(string) < valj.(string) 272 | default: 273 | // should never happen 274 | err = errors.Errorf("unsupported type %q for sorting", t) 275 | return false 276 | } 277 | }) 278 | if err != nil { 279 | return err 280 | } 281 | 282 | for i := 0; i < len(extras); i++ { 283 | extras[i] = p[i].ec 284 | } 285 | return nil 286 | } 287 | -------------------------------------------------------------------------------- /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 | 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/resource" 18 | "github.com/crossplane/function-sdk-go/response" 19 | ) 20 | 21 | func TestRunFunction(t *testing.T) { 22 | type args struct { 23 | ctx context.Context 24 | req *fnv1.RunFunctionRequest 25 | } 26 | type want struct { 27 | rsp *fnv1.RunFunctionResponse 28 | err error 29 | } 30 | 31 | cases := map[string]struct { 32 | reason string 33 | args args 34 | want want 35 | }{ 36 | "RequestExtraResources": { 37 | reason: "The Function should request ExtraResources", 38 | args: args{ 39 | req: &fnv1.RunFunctionRequest{ 40 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 41 | Observed: &fnv1.State{ 42 | Composite: &fnv1.Resource{ 43 | Resource: resource.MustStructJSON(`{ 44 | "apiVersion": "test.crossplane.io/v1alpha1", 45 | "kind": "XR", 46 | "metadata": { 47 | "name": "my-xr" 48 | }, 49 | "spec": { 50 | "existingEnvSelectorLabel": "someMoreBar" 51 | } 52 | }`), 53 | }, 54 | }, 55 | Input: resource.MustStructJSON(`{ 56 | "apiVersion": "extra-resources.fn.crossplane.io/v1beta1", 57 | "kind": "Input", 58 | "spec": { 59 | "extraResources": [ 60 | { 61 | "kind": "EnvironmentConfig", 62 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 63 | "type": "Reference", 64 | "into": "obj-0", 65 | "ref": { 66 | "name": "my-env-config" 67 | } 68 | }, 69 | { 70 | "type": "Reference", 71 | "kind": "EnvironmentConfig", 72 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 73 | "into": "obj-1", 74 | "ref": { 75 | "name": "my-second-env-config" 76 | } 77 | }, 78 | { 79 | "type": "Selector", 80 | "kind": "EnvironmentConfig", 81 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 82 | "into": "obj-2", 83 | "selector": { 84 | "matchLabels": [ 85 | { 86 | "type": "Value", 87 | "key": "foo", 88 | "value": "bar" 89 | } 90 | ] 91 | } 92 | }, 93 | { 94 | "type": "Selector", 95 | "kind": "EnvironmentConfig", 96 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 97 | "into": "obj-3", 98 | "selector": { 99 | "matchLabels": [ 100 | { 101 | "key": "someMoreFoo", 102 | "valueFromFieldPath": "spec.missingEnvSelectorLabel", 103 | "fromFieldPathPolicy": "Optional" 104 | } 105 | ] 106 | } 107 | }, 108 | { 109 | "type": "Selector", 110 | "kind": "EnvironmentConfig", 111 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 112 | "into": "obj-4", 113 | "selector": { 114 | "matchLabels": [ 115 | { 116 | "key": "someMoreFoo", 117 | "valueFromFieldPath": "spec.existingEnvSelectorLabel", 118 | "fromFieldPathPolicy": "Required" 119 | } 120 | ] 121 | } 122 | } 123 | ] 124 | } 125 | }`), 126 | }, 127 | }, 128 | want: want{ 129 | rsp: &fnv1.RunFunctionResponse{ 130 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 131 | Results: []*fnv1.Result{}, 132 | Requirements: &fnv1.Requirements{ 133 | ExtraResources: map[string]*fnv1.ResourceSelector{ 134 | "obj-0": { 135 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 136 | Kind: "EnvironmentConfig", 137 | Match: &fnv1.ResourceSelector_MatchName{ 138 | MatchName: "my-env-config", 139 | }, 140 | }, 141 | "obj-1": { 142 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 143 | Kind: "EnvironmentConfig", 144 | Match: &fnv1.ResourceSelector_MatchName{ 145 | MatchName: "my-second-env-config", 146 | }, 147 | }, 148 | "obj-2": { 149 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 150 | Kind: "EnvironmentConfig", 151 | Match: &fnv1.ResourceSelector_MatchLabels{ 152 | MatchLabels: &fnv1.MatchLabels{ 153 | Labels: map[string]string{ 154 | "foo": "bar", 155 | }, 156 | }, 157 | }, 158 | }, 159 | // 160 | // environment-config-3 is not requested because it was optional 161 | // 162 | "obj-4": { 163 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 164 | Kind: "EnvironmentConfig", 165 | Match: &fnv1.ResourceSelector_MatchLabels{ 166 | MatchLabels: &fnv1.MatchLabels{ 167 | Labels: map[string]string{ 168 | "someMoreFoo": "someMoreBar", 169 | }, 170 | }, 171 | }, 172 | }, 173 | }, 174 | }, 175 | }, 176 | }, 177 | }, 178 | "RequestEnvironmentConfigsFound": { 179 | reason: "The Function should request the necessary EnvironmentConfigs even if they are already present in the request", 180 | args: args{ 181 | req: &fnv1.RunFunctionRequest{ 182 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 183 | Observed: &fnv1.State{ 184 | Composite: &fnv1.Resource{ 185 | Resource: resource.MustStructJSON(`{ 186 | "apiVersion": "test.crossplane.io/v1alpha1", 187 | "kind": "XR", 188 | "metadata": { 189 | "name": "my-xr" 190 | }, 191 | "spec": { 192 | "existingEnvSelectorLabel": "someMoreBar" 193 | } 194 | }`), 195 | }, 196 | }, 197 | ExtraResources: map[string]*fnv1.Resources{ 198 | "obj-0": { 199 | Items: []*fnv1.Resource{ 200 | { 201 | Resource: resource.MustStructJSON(`{ 202 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 203 | "kind": "EnvironmentConfig", 204 | "metadata": { 205 | "name": "my-env-config" 206 | }, 207 | "data": { 208 | "firstKey": "firstVal", 209 | "secondKey": "secondVal" 210 | } 211 | }`), 212 | }, 213 | }, 214 | }, 215 | "obj-1": { 216 | Items: []*fnv1.Resource{ 217 | { 218 | Resource: resource.MustStructJSON(`{ 219 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 220 | "kind": "EnvironmentConfig", 221 | "metadata": { 222 | "name": "my-second-env-config" 223 | }, 224 | "data": { 225 | "secondKey": "secondVal-ok", 226 | "thirdKey": "thirdVal" 227 | } 228 | }`), 229 | }, 230 | }, 231 | }, 232 | "obj-2": { 233 | Items: []*fnv1.Resource{ 234 | { 235 | Resource: resource.MustStructJSON(`{ 236 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 237 | "kind": "EnvironmentConfig", 238 | "metadata": { 239 | "name": "my-third-env-config-b" 240 | }, 241 | "data": { 242 | "fourthKey": "fourthVal-b" 243 | } 244 | }`), 245 | }, 246 | { 247 | Resource: resource.MustStructJSON(`{ 248 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 249 | "kind": "EnvironmentConfig", 250 | "metadata": { 251 | "name": "my-third-env-config-a" 252 | }, 253 | "data": { 254 | "fourthKey": "fourthVal-a" 255 | } 256 | }`), 257 | }, 258 | }, 259 | }, 260 | "obj-3": { 261 | Items: []*fnv1.Resource{ 262 | { 263 | Resource: resource.MustStructJSON(`{ 264 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 265 | "kind": "EnvironmentConfig", 266 | "metadata": { 267 | "name": "my-third-env-config" 268 | }, 269 | "data": { 270 | "fifthKey": "fifthVal" 271 | } 272 | }`), 273 | }, 274 | }, 275 | }, 276 | "obj-4": { 277 | Items: []*fnv1.Resource{ 278 | { 279 | Resource: resource.MustStructJSON(`{ 280 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 281 | "kind": "EnvironmentConfig", 282 | "metadata": { 283 | "name": "my-fourth-env-config" 284 | }, 285 | "data": { 286 | "sixthKey": "sixthVal" 287 | } 288 | }`), 289 | }, 290 | }, 291 | }, 292 | }, 293 | Input: resource.MustStructJSON(`{ 294 | "apiVersion": "extra-resources.fn.crossplane.io/v1beta1", 295 | "kind": "Input", 296 | "spec": { 297 | "extraResources": [ 298 | { 299 | "type": "Reference", 300 | "into": "obj-0", 301 | "kind": "EnvironmentConfig", 302 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 303 | "ref": { 304 | "name": "my-env-config" 305 | } 306 | }, 307 | { 308 | "type": "Reference", 309 | "into": "obj-1", 310 | "kind": "EnvironmentConfig", 311 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 312 | "ref": { 313 | "name": "my-second-env-config" 314 | } 315 | }, 316 | { 317 | "type": "Selector", 318 | "into": "obj-2", 319 | "kind": "EnvironmentConfig", 320 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 321 | "selector": { 322 | "matchLabels": [ 323 | { 324 | "type": "Value", 325 | "key": "foo", 326 | "value": "bar" 327 | } 328 | ] 329 | } 330 | }, 331 | { 332 | "type": "Selector", 333 | "into": "obj-3", 334 | "kind": "EnvironmentConfig", 335 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 336 | "selector": { 337 | "matchLabels": [ 338 | { 339 | "key": "someMoreFoo", 340 | "valueFromFieldPath": "spec.missingEnvSelectorLabel", 341 | "fromFieldPathPolicy": "Optional" 342 | } 343 | ] 344 | } 345 | }, 346 | { 347 | "type": "Selector", 348 | "into": "obj-4", 349 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 350 | "kind": "EnvironmentConfig", 351 | "selector": { 352 | "matchLabels": [ 353 | { 354 | "key": "someMoreFoo", 355 | "valueFromFieldPath": "spec.existingEnvSelectorLabel", 356 | "fromFieldPathPolicy": "Required" 357 | } 358 | ] 359 | } 360 | } 361 | ] 362 | } 363 | }`), 364 | }, 365 | }, 366 | want: want{ 367 | rsp: &fnv1.RunFunctionResponse{ 368 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 369 | Results: []*fnv1.Result{}, 370 | Requirements: &fnv1.Requirements{ 371 | ExtraResources: map[string]*fnv1.ResourceSelector{ 372 | "obj-0": { 373 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 374 | Kind: "EnvironmentConfig", 375 | Match: &fnv1.ResourceSelector_MatchName{ 376 | MatchName: "my-env-config", 377 | }, 378 | }, 379 | "obj-1": { 380 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 381 | Kind: "EnvironmentConfig", 382 | Match: &fnv1.ResourceSelector_MatchName{ 383 | MatchName: "my-second-env-config", 384 | }, 385 | }, 386 | "obj-2": { 387 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 388 | Kind: "EnvironmentConfig", 389 | Match: &fnv1.ResourceSelector_MatchLabels{ 390 | MatchLabels: &fnv1.MatchLabels{ 391 | Labels: map[string]string{ 392 | "foo": "bar", 393 | }, 394 | }, 395 | }, 396 | }, 397 | // environment-config-3 is not requested because it was optional 398 | "obj-4": { 399 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 400 | Kind: "EnvironmentConfig", 401 | Match: &fnv1.ResourceSelector_MatchLabels{ 402 | MatchLabels: &fnv1.MatchLabels{ 403 | Labels: map[string]string{ 404 | "someMoreFoo": "someMoreBar", 405 | }, 406 | }, 407 | }, 408 | }, 409 | }, 410 | }, 411 | Context: &structpb.Struct{ 412 | Fields: map[string]*structpb.Value{ 413 | FunctionContextKeyExtraResources: structpb.NewStructValue(resource.MustStructJSON(`{ 414 | "obj-0": [ 415 | { 416 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 417 | "data": { 418 | "firstKey": "firstVal", 419 | "secondKey": "secondVal" 420 | }, 421 | "kind": "EnvironmentConfig", 422 | "metadata": { 423 | "name": "my-env-config" 424 | } 425 | } 426 | ], 427 | "obj-1": [ 428 | { 429 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 430 | "data": { 431 | "secondKey": "secondVal-ok", 432 | "thirdKey": "thirdVal" 433 | }, 434 | "kind": "EnvironmentConfig", 435 | "metadata": { 436 | "name": "my-second-env-config" 437 | } 438 | } 439 | ], 440 | "obj-2": [ 441 | { 442 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 443 | "data": { 444 | "fourthKey": "fourthVal-a" 445 | }, 446 | "kind": "EnvironmentConfig", 447 | "metadata": { 448 | "name": "my-third-env-config-a" 449 | } 450 | }, 451 | { 452 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 453 | "data": { 454 | "fourthKey": "fourthVal-b" 455 | }, 456 | "kind": "EnvironmentConfig", 457 | "metadata": { 458 | "name": "my-third-env-config-b" 459 | } 460 | } 461 | ], 462 | "obj-3": [ 463 | { 464 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 465 | "data": { 466 | "fifthKey": "fifthVal" 467 | }, 468 | "kind": "EnvironmentConfig", 469 | "metadata": { 470 | "name": "my-third-env-config" 471 | } 472 | } 473 | ], 474 | "obj-4": [ 475 | { 476 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 477 | "data": { 478 | "sixthKey": "sixthVal" 479 | }, 480 | "kind": "EnvironmentConfig", 481 | "metadata": { 482 | "name": "my-fourth-env-config" 483 | } 484 | } 485 | ] 486 | }`)), 487 | }, 488 | }, 489 | }, 490 | }, 491 | }, 492 | "RequestEnvironmentConfigsNotFoundRequired": { 493 | reason: "The Function should return fatal if a required EnvironmentConfig is not found", 494 | args: args{ 495 | req: &fnv1.RunFunctionRequest{ 496 | Meta: &fnv1.RequestMeta{Tag: "hello"}, 497 | Observed: &fnv1.State{ 498 | Composite: &fnv1.Resource{ 499 | Resource: resource.MustStructJSON(`{ 500 | "apiVersion": "test.crossplane.io/v1alpha1", 501 | "kind": "XR", 502 | "metadata": { 503 | "name": "my-xr" 504 | } 505 | }`), 506 | }, 507 | }, 508 | ExtraResources: map[string]*fnv1.Resources{ 509 | "environment-config-0": { 510 | Items: []*fnv1.Resource{}, 511 | }, 512 | }, 513 | Input: resource.MustStructJSON(`{ 514 | "apiVersion": "extra-resources.fn.crossplane.io/v1beta1", 515 | "kind": "Input", 516 | "spec": { 517 | "extraResources": [ 518 | { 519 | "type": "Reference", 520 | "into": "obj-0", 521 | "kind": "EnvironmentConfig", 522 | "apiVersion": "apiextensions.crossplane.io/v1beta1", 523 | "ref": { 524 | "name": "my-env-config" 525 | } 526 | } 527 | ] 528 | } 529 | }`), 530 | }, 531 | }, 532 | want: want{ 533 | rsp: &fnv1.RunFunctionResponse{ 534 | Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, 535 | Results: []*fnv1.Result{ 536 | { 537 | Severity: fnv1.Severity_SEVERITY_FATAL, 538 | Target: fnv1.Target_TARGET_COMPOSITE.Enum(), 539 | }, 540 | }, 541 | Requirements: &fnv1.Requirements{ 542 | ExtraResources: map[string]*fnv1.ResourceSelector{ 543 | "obj-0": { 544 | ApiVersion: "apiextensions.crossplane.io/v1beta1", 545 | Kind: "EnvironmentConfig", 546 | Match: &fnv1.ResourceSelector_MatchName{ 547 | MatchName: "my-env-config", 548 | }, 549 | }, 550 | }, 551 | }, 552 | }, 553 | }, 554 | }, 555 | } 556 | 557 | for name, tc := range cases { 558 | t.Run(name, func(t *testing.T) { 559 | f := &Function{log: logging.NewNopLogger()} 560 | rsp, err := f.RunFunction(tc.args.ctx, tc.args.req) 561 | 562 | diff := cmp.Diff(tc.want.rsp, rsp, cmpopts.AcyclicTransformer("toJsonWithoutResultMessages", func(r *fnv1.RunFunctionResponse) []byte { 563 | // We don't care about messages. 564 | // cmptopts.IgnoreField wasn't working with protocmp.Transform 565 | // We can't split this to another transformer as 566 | // transformers are applied not in order but as soon as they 567 | // match the type, which are walked from the root (RunFunctionResponse). 568 | for _, result := range r.GetResults() { 569 | result.Message = "" 570 | } 571 | out, err := protojson.Marshal(r) 572 | if err != nil { 573 | t.Fatalf("cannot marshal %T to JSON: %s", r, err) 574 | } 575 | return out 576 | })) 577 | if diff != "" { 578 | t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff) 579 | } 580 | 581 | if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { 582 | t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff) 583 | } 584 | }) 585 | } 586 | } 587 | 588 | func resourceWithFieldPathValue(path string, value any) resource.Extra { 589 | u := unstructured.Unstructured{ 590 | Object: map[string]interface{}{}, 591 | } 592 | err := fieldpath.Pave(u.Object).SetValue(path, value) 593 | if err != nil { 594 | panic(err) 595 | } 596 | return resource.Extra{ 597 | Resource: &u, 598 | } 599 | } 600 | 601 | func TestSortExtrasByFieldPath(t *testing.T) { 602 | type args struct { 603 | extras []resource.Extra 604 | path string 605 | } 606 | type want struct { 607 | extras []resource.Extra 608 | err error 609 | } 610 | 611 | cases := map[string]struct { 612 | reason string 613 | args args 614 | want want 615 | }{ 616 | "SortByString": { 617 | reason: "The Function should sort the Extras by the string value at the specified field path", 618 | args: args{ 619 | extras: []resource.Extra{ 620 | resourceWithFieldPathValue("metadata.name", "c"), 621 | resourceWithFieldPathValue("metadata.name", "a"), 622 | resourceWithFieldPathValue("metadata.name", "b"), 623 | }, 624 | path: "metadata.name", 625 | }, 626 | want: want{ 627 | extras: []resource.Extra{ 628 | resourceWithFieldPathValue("metadata.name", "a"), 629 | resourceWithFieldPathValue("metadata.name", "b"), 630 | resourceWithFieldPathValue("metadata.name", "c"), 631 | }, 632 | }, 633 | }, 634 | "SortByInt": { 635 | reason: "The Function should sort the Extras by the int value at the specified field path", 636 | args: args{ 637 | extras: []resource.Extra{ 638 | resourceWithFieldPathValue("data.someInt", 3), 639 | resourceWithFieldPathValue("data.someInt", 1), 640 | resourceWithFieldPathValue("data.someInt", 2), 641 | }, 642 | path: "data.someInt", 643 | }, 644 | want: want{ 645 | extras: []resource.Extra{ 646 | resourceWithFieldPathValue("data.someInt", 1), 647 | resourceWithFieldPathValue("data.someInt", 2), 648 | resourceWithFieldPathValue("data.someInt", 3), 649 | }, 650 | }, 651 | }, 652 | "SortByFloat": { 653 | reason: "The Function should sort the Extras by the float value at the specified field path", 654 | args: args{ 655 | extras: []resource.Extra{ 656 | resourceWithFieldPathValue("data.someFloat", 1.3), 657 | resourceWithFieldPathValue("data.someFloat", 1.1), 658 | resourceWithFieldPathValue("data.someFloat", 1.2), 659 | resourceWithFieldPathValue("data.someFloat", 1.4), 660 | }, 661 | path: "data.someFloat", 662 | }, 663 | want: want{ 664 | extras: []resource.Extra{ 665 | resourceWithFieldPathValue("data.someFloat", 1.1), 666 | resourceWithFieldPathValue("data.someFloat", 1.2), 667 | resourceWithFieldPathValue("data.someFloat", 1.3), 668 | resourceWithFieldPathValue("data.someFloat", 1.4), 669 | }, 670 | }, 671 | }, 672 | "InconsistentTypeSortByInt": { 673 | reason: "The Function should sort the Extras by the int value at the specified field path", 674 | args: args{ 675 | extras: []resource.Extra{ 676 | resourceWithFieldPathValue("data.someInt", 3), 677 | resourceWithFieldPathValue("data.someInt", 1), 678 | resourceWithFieldPathValue("data.someInt", "2"), 679 | }, 680 | path: "data.someInt", 681 | }, 682 | want: want{ 683 | err: cmpopts.AnyError, 684 | }, 685 | }, 686 | "EmptyPath": { 687 | reason: "The Function should return an error if the path is empty", 688 | args: args{ 689 | extras: []resource.Extra{ 690 | resourceWithFieldPathValue("metadata.name", "c"), 691 | resourceWithFieldPathValue("metadata.name", "a"), 692 | resourceWithFieldPathValue("metadata.name", "b"), 693 | }, 694 | path: "", 695 | }, 696 | want: want{ 697 | err: cmpopts.AnyError, 698 | }, 699 | }, 700 | "InvalidPathAll": { 701 | reason: "The Function should return no error if the path is invalid for all resources", 702 | args: args{ 703 | extras: []resource.Extra{ 704 | resourceWithFieldPathValue("metadata.name", "c"), 705 | resourceWithFieldPathValue("metadata.name", "a"), 706 | resourceWithFieldPathValue("metadata.name", "b"), 707 | }, 708 | path: "metadata.invalid", 709 | }, 710 | want: want{ 711 | extras: []resource.Extra{ 712 | resourceWithFieldPathValue("metadata.name", "c"), 713 | resourceWithFieldPathValue("metadata.name", "a"), 714 | resourceWithFieldPathValue("metadata.name", "b"), 715 | }, 716 | }, 717 | }, 718 | "InvalidPathSome": { 719 | reason: "The Function should return no error if the path is invalid for some resources, just use the rest of the resources zero value", 720 | args: args{ 721 | extras: []resource.Extra{ 722 | resourceWithFieldPathValue("metadata.name", "c"), 723 | resourceWithFieldPathValue("metadata.invalid", "a"), 724 | resourceWithFieldPathValue("metadata.name", "b"), 725 | }, 726 | path: "metadata.name", 727 | }, 728 | want: want{ 729 | extras: []resource.Extra{ 730 | resourceWithFieldPathValue("metadata.invalid", "a"), 731 | resourceWithFieldPathValue("metadata.name", "b"), 732 | resourceWithFieldPathValue("metadata.name", "c"), 733 | }, 734 | }, 735 | }, 736 | } 737 | 738 | for name, tc := range cases { 739 | t.Run(name, func(t *testing.T) { 740 | got := sortExtrasByFieldPath(tc.args.extras, tc.args.path) 741 | if diff := cmp.Diff(tc.want.err, got, cmpopts.EquateErrors()); diff != "" { 742 | t.Errorf("%s\n(...): -want err, +got err:\n%s", tc.reason, diff) 743 | } 744 | if tc.want.err != nil { 745 | return 746 | } 747 | if diff := cmp.Diff(tc.want.extras, tc.args.extras); diff != "" { 748 | t.Errorf("%s\n(...): -want, +got:\n%s", tc.reason, diff) 749 | } 750 | }) 751 | } 752 | } 753 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crossplane-contrib/function-extra-resources 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/alecthomas/kong v0.8.1 9 | github.com/crossplane/crossplane-runtime v1.19.0 10 | github.com/crossplane/function-sdk-go v0.4.0 11 | github.com/google/go-cmp v0.6.0 12 | google.golang.org/protobuf v1.34.3-0.20240816073751-94ecbc261689 13 | k8s.io/apimachinery v0.31.0 14 | sigs.k8s.io/controller-tools v0.16.0 15 | ) 16 | 17 | require ( 18 | dario.cat/mergo v1.0.1 // indirect 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 21 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 22 | github.com/fatih/color v1.17.0 // indirect 23 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 24 | github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 // indirect 25 | github.com/go-logr/logr v1.4.2 // indirect 26 | github.com/go-logr/zapr v1.3.0 // indirect 27 | github.com/go-openapi/jsonpointer v0.20.2 // indirect 28 | github.com/go-openapi/jsonreference v0.20.2 // indirect 29 | github.com/go-openapi/swag v0.22.8 // indirect 30 | github.com/gobuffalo/flect v1.0.2 // indirect 31 | github.com/gogo/protobuf v1.3.2 // indirect 32 | github.com/golang/protobuf v1.5.4 // indirect 33 | github.com/google/gnostic-models v0.6.8 // indirect 34 | github.com/google/gofuzz v1.2.0 // 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.21.0 // indirect 55 | golang.org/x/net v0.33.0 // indirect 56 | golang.org/x/oauth2 v0.22.0 // indirect 57 | golang.org/x/sync v0.10.0 // indirect 58 | golang.org/x/sys v0.28.0 // indirect 59 | golang.org/x/term v0.27.0 // indirect 60 | golang.org/x/text v0.21.0 // indirect 61 | golang.org/x/time v0.5.0 // indirect 62 | golang.org/x/tools v0.25.0 // indirect 63 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect 64 | google.golang.org/grpc v1.67.0 // indirect 65 | gopkg.in/inf.v0 v0.9.1 // indirect 66 | gopkg.in/yaml.v2 v2.4.0 // indirect 67 | gopkg.in/yaml.v3 v3.0.1 // indirect 68 | k8s.io/api v0.31.0 // indirect 69 | k8s.io/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-20240228011516-70dd3763d340 // indirect 73 | k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 // indirect 74 | sigs.k8s.io/controller-runtime v0.19.0 // indirect 75 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 76 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // 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.19.0 h1:yZmF8cYy8/PYiA+aF3ONjVinfuV06Vi3hHQwjYR2Qak= 26 | github.com/crossplane/crossplane-runtime v1.19.0/go.mod h1:z8CV6dXxO5yxzfJGa4bgnWuyAnjZ3hY19cqGDcr5Bg0= 27 | github.com/crossplane/function-sdk-go v0.4.0 h1:1jd+UIaZlVNQCUO4hLAgUqWBRnUKw2ObF9ZuMw5CpKk= 28 | github.com/crossplane/function-sdk-go v0.4.0/go.mod h1:jLnzUG8pt8tn/U6/uvtNStAhDjhIq4wCR31yECT54NM= 29 | github.com/crossplane/upjet v1.4.1-0.20240911184956-3afbb7796d46 h1:2IH1YPTBrNmBj0Z1OCjEBTrQCuRaLutZbWLaswFeCFQ= 30 | github.com/crossplane/upjet v1.4.1-0.20240911184956-3afbb7796d46/go.mod h1:wkdZf/Cvhr6PI30VdHIOjg4dX39Z5uijqnLWFk5PbGM= 31 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 36 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 37 | github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= 38 | github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 39 | github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= 40 | github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 41 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 42 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 43 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 44 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 45 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 46 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 47 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 48 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 49 | github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 h1:xcuWappghOVI8iNWoF2OKahVejd1LSVi/v4JED44Amo= 50 | github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= 51 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 52 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 53 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 54 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 55 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 56 | github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= 57 | github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= 58 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 59 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 60 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 61 | github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= 62 | github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= 63 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 64 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 65 | github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= 66 | github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= 67 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 68 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 69 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 70 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 71 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 72 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 73 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 74 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 75 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 76 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 77 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 78 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 79 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 80 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 81 | github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= 82 | github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 83 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 84 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 85 | github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 h1:Ud/6/AdmJ1R7ibdS0Wo5MWPj0T1R0fkpaD087bBaW8I= 86 | github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= 87 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 88 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 89 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 90 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 91 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 92 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 93 | github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= 94 | github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= 95 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= 96 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 97 | github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= 98 | github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= 99 | github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc= 100 | github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= 101 | github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= 102 | github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= 103 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= 104 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 105 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= 106 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= 107 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 108 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 109 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 110 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 111 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 112 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 113 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 114 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 115 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 116 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 117 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 118 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 119 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 120 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 121 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 122 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 123 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 124 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 125 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 126 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 127 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 128 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 129 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 130 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 131 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 132 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 133 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 134 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 135 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 136 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 137 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 138 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 139 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 140 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 141 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= 142 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 143 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 144 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 145 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 146 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 147 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 148 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 149 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 150 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 151 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 152 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 153 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 154 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 155 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 156 | github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= 157 | github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= 158 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 159 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 160 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 161 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 162 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 163 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 164 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 165 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 166 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 167 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 168 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 169 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 170 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 171 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 172 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 173 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 174 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 175 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 176 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 177 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 178 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 179 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 180 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 181 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 182 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 183 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 184 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 185 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 186 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 187 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 188 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 189 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 190 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 191 | github.com/tmccombs/hcl2json v0.3.3 h1:+DLNYqpWE0CsOQiEZu+OZm5ZBImake3wtITYxQ8uLFQ= 192 | github.com/tmccombs/hcl2json v0.3.3/go.mod h1:Y2chtz2x9bAeRTvSibVRVgbLJhLJXKlUeIvjeVdnm4w= 193 | github.com/upbound/provider-aws v1.14.0 h1:DDUdlMp+dNlFXXlhsGdCvQD7qFdT1AsEcaqlRU3BO14= 194 | github.com/upbound/provider-aws v1.14.0/go.mod h1:IvyvgGlhRVr737E4P75tyD/i53hxnyO7KPM8bbXH+SU= 195 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 196 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 197 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 198 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 199 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 200 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 201 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 202 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 203 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 204 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 205 | github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= 206 | github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 207 | github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= 208 | github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 209 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 210 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 211 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 212 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 213 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 214 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 215 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 216 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 217 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 218 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= 219 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 220 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 221 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 222 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 223 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 224 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 225 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 226 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 227 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 228 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 229 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 230 | golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= 231 | golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 232 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 233 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 234 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 235 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 236 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 237 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 238 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 241 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 243 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 244 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 245 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 246 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 247 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 248 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 249 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 250 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 251 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 252 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 253 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 254 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 255 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 256 | golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= 257 | golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= 258 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 259 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 260 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 261 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 262 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 263 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 264 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 265 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 266 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= 267 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 268 | google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= 269 | google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 270 | google.golang.org/protobuf v1.34.3-0.20240816073751-94ecbc261689 h1:hNwajDgT0MlsxZzlUajZVmUYFpts8/CYe4BSNx503ZE= 271 | google.golang.org/protobuf v1.34.3-0.20240816073751-94ecbc261689/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 272 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 273 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 274 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 275 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 276 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 277 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 278 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 279 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 280 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 281 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 282 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 283 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 284 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 285 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 286 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 287 | k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= 288 | k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= 289 | k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= 290 | k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= 291 | k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= 292 | k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= 293 | k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= 294 | k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= 295 | k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs= 296 | k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo= 297 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 298 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 299 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= 300 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= 301 | k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= 302 | k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 303 | sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= 304 | sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= 305 | sigs.k8s.io/controller-tools v0.16.0 h1:EJPB+a5Bve861SPBPPWRbP6bbKyNxqK12oYT5zEns9s= 306 | sigs.k8s.io/controller-tools v0.16.0/go.mod h1:0I0xqjR65YTfoO12iR+mZR6s6UAVcUARgXRlsu0ljB0= 307 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 308 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 309 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 310 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 311 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 312 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 313 | -------------------------------------------------------------------------------- /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 | // Into is the key into which extra resources for this selector will be placed. 93 | Into string `json:"into"` 94 | } 95 | 96 | // GetType returns the type of the resource source, returning the default if not set. 97 | func (e *ResourceSource) GetType() ResourceSourceType { 98 | if e == nil || e.Type == "" { 99 | return ResourceSourceTypeReference 100 | } 101 | return e.Type 102 | } 103 | 104 | // An ResourceSourceReference references an ExtraResource by it's name. 105 | type ResourceSourceReference struct { 106 | // The name of the object. 107 | Name string `json:"name"` 108 | } 109 | 110 | // An ResourceSourceSelector selects an ExtraResource via labels. 111 | type ResourceSourceSelector struct { 112 | // MaxMatch specifies the number of extracted ExtraResources in Multiple mode, extracts all if nil. 113 | MaxMatch *uint64 `json:"maxMatch,omitempty"` 114 | 115 | // MinMatch specifies the required minimum of extracted ExtraResources in Multiple mode. 116 | MinMatch *uint64 `json:"minMatch,omitempty"` 117 | 118 | // SortByFieldPath is the path to the field based on which list of ExtraResources is alphabetically sorted. 119 | // +kubebuilder:default="metadata.name" 120 | SortByFieldPath string `json:"sortByFieldPath,omitempty"` 121 | 122 | // MatchLabels ensures an object with matching labels is selected. 123 | MatchLabels []ResourceSourceSelectorLabelMatcher `json:"matchLabels,omitempty"` 124 | } 125 | 126 | // GetSortByFieldPath returns the sort by path if set or a sane default. 127 | func (e *ResourceSourceSelector) GetSortByFieldPath() string { 128 | if e == nil || e.SortByFieldPath == "" { 129 | return "metadata.name" 130 | } 131 | return e.SortByFieldPath 132 | } 133 | 134 | // ResourceSourceSelectorLabelMatcherType specifies where the value for a label comes from. 135 | type ResourceSourceSelectorLabelMatcherType string 136 | 137 | const ( 138 | // ResourceSourceSelectorLabelMatcherTypeFromCompositeFieldPath extracts 139 | // the label value from a composite fieldpath. 140 | ResourceSourceSelectorLabelMatcherTypeFromCompositeFieldPath ResourceSourceSelectorLabelMatcherType = "FromCompositeFieldPath" 141 | // ResourceSourceSelectorLabelMatcherTypeValue uses a literal as label 142 | // value. 143 | ResourceSourceSelectorLabelMatcherTypeValue ResourceSourceSelectorLabelMatcherType = "Value" 144 | ) 145 | 146 | // An ResourceSourceSelectorLabelMatcher acts like a k8s label selector but 147 | // can draw the label value from a different path. 148 | type ResourceSourceSelectorLabelMatcher struct { 149 | // Type specifies where the value for a label comes from. 150 | // +optional 151 | // +kubebuilder:validation:Enum=FromCompositeFieldPath;Value 152 | // +kubebuilder:default=FromCompositeFieldPath 153 | Type ResourceSourceSelectorLabelMatcherType `json:"type,omitempty"` 154 | 155 | // Key of the label to match. 156 | Key string `json:"key"` 157 | 158 | // ValueFromFieldPath specifies the field path to look for the label value. 159 | ValueFromFieldPath *string `json:"valueFromFieldPath,omitempty"` 160 | 161 | // FromFieldPathPolicy specifies the policy for the valueFromFieldPath. 162 | // The default is Required, meaning that an error will be returned if the 163 | // field is not found in the composite resource. 164 | // Optional means that if the field is not found in the composite resource, 165 | // that label pair will just be skipped. N.B. other specified label 166 | // matchers will still be used to retrieve the desired 167 | // resource config, if any. 168 | // +kubebuilder:validation:Enum=Optional;Required 169 | // +kubebuilder:default=Required 170 | FromFieldPathPolicy *FromFieldPathPolicy `json:"fromFieldPathPolicy,omitempty"` 171 | 172 | // Value specifies a literal label value. 173 | Value *string `json:"value,omitempty"` 174 | } 175 | 176 | // FromFieldPathIsOptional returns true if the FromFieldPathPolicy is set to 177 | // +optional 178 | func (e *ResourceSourceSelectorLabelMatcher) FromFieldPathIsOptional() bool { 179 | return e.FromFieldPathPolicy != nil && *e.FromFieldPathPolicy == FromFieldPathPolicyOptional 180 | } 181 | 182 | // GetType returns the type of the label matcher, returning the default if not set. 183 | func (e *ResourceSourceSelectorLabelMatcher) GetType() ResourceSourceSelectorLabelMatcherType { 184 | if e == nil || e.Type == "" { 185 | return ResourceSourceSelectorLabelMatcherTypeFromCompositeFieldPath 186 | } 187 | return e.Type 188 | } 189 | 190 | // A FromFieldPathPolicy determines how to patch from a field path. 191 | type FromFieldPathPolicy string 192 | 193 | // FromFieldPath patch policies. 194 | const ( 195 | FromFieldPathPolicyOptional FromFieldPathPolicy = "Optional" 196 | FromFieldPathPolicyRequired FromFieldPathPolicy = "Required" 197 | ) 198 | 199 | // A PatchPolicy configures the specifics of patching behaviour. 200 | type PatchPolicy struct { 201 | // FromFieldPath specifies how to patch from a field path. The default is 202 | // 'Optional', which means the patch will be a no-op if the specified 203 | // fromFieldPath does not exist. Use 'Required' if the patch should fail if 204 | // the specified path does not exist. 205 | // +kubebuilder:validation:Enum=Optional;Required 206 | // +optional 207 | FromFieldPath *FromFieldPathPolicy `json:"fromFieldPath,omitempty"` 208 | MergeOptions *xpv1.MergeOptions `json:"mergeOptions,omitempty"` 209 | } 210 | 211 | // GetFromFieldPathPolicy returns the FromFieldPathPolicy for this PatchPolicy, defaulting to FromFieldPathPolicyOptional if not specified. 212 | func (pp *PatchPolicy) GetFromFieldPathPolicy() FromFieldPathPolicy { 213 | if pp == nil || pp.FromFieldPath == nil { 214 | return FromFieldPathPolicyOptional 215 | } 216 | return *pp.FromFieldPath 217 | } 218 | -------------------------------------------------------------------------------- /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 | } 124 | 125 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSource. 126 | func (in *ResourceSource) DeepCopy() *ResourceSource { 127 | if in == nil { 128 | return nil 129 | } 130 | out := new(ResourceSource) 131 | in.DeepCopyInto(out) 132 | return out 133 | } 134 | 135 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 136 | func (in *ResourceSourceReference) DeepCopyInto(out *ResourceSourceReference) { 137 | *out = *in 138 | } 139 | 140 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSourceReference. 141 | func (in *ResourceSourceReference) DeepCopy() *ResourceSourceReference { 142 | if in == nil { 143 | return nil 144 | } 145 | out := new(ResourceSourceReference) 146 | in.DeepCopyInto(out) 147 | return out 148 | } 149 | 150 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 151 | func (in *ResourceSourceSelector) DeepCopyInto(out *ResourceSourceSelector) { 152 | *out = *in 153 | if in.MaxMatch != nil { 154 | in, out := &in.MaxMatch, &out.MaxMatch 155 | *out = new(uint64) 156 | **out = **in 157 | } 158 | if in.MinMatch != nil { 159 | in, out := &in.MinMatch, &out.MinMatch 160 | *out = new(uint64) 161 | **out = **in 162 | } 163 | if in.MatchLabels != nil { 164 | in, out := &in.MatchLabels, &out.MatchLabels 165 | *out = make([]ResourceSourceSelectorLabelMatcher, len(*in)) 166 | for i := range *in { 167 | (*in)[i].DeepCopyInto(&(*out)[i]) 168 | } 169 | } 170 | } 171 | 172 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSourceSelector. 173 | func (in *ResourceSourceSelector) DeepCopy() *ResourceSourceSelector { 174 | if in == nil { 175 | return nil 176 | } 177 | out := new(ResourceSourceSelector) 178 | in.DeepCopyInto(out) 179 | return out 180 | } 181 | 182 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 183 | func (in *ResourceSourceSelectorLabelMatcher) DeepCopyInto(out *ResourceSourceSelectorLabelMatcher) { 184 | *out = *in 185 | if in.ValueFromFieldPath != nil { 186 | in, out := &in.ValueFromFieldPath, &out.ValueFromFieldPath 187 | *out = new(string) 188 | **out = **in 189 | } 190 | if in.FromFieldPathPolicy != nil { 191 | in, out := &in.FromFieldPathPolicy, &out.FromFieldPathPolicy 192 | *out = new(FromFieldPathPolicy) 193 | **out = **in 194 | } 195 | if in.Value != nil { 196 | in, out := &in.Value, &out.Value 197 | *out = new(string) 198 | **out = **in 199 | } 200 | } 201 | 202 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSourceSelectorLabelMatcher. 203 | func (in *ResourceSourceSelectorLabelMatcher) DeepCopy() *ResourceSourceSelectorLabelMatcher { 204 | if in == nil { 205 | return nil 206 | } 207 | out := new(ResourceSourceSelectorLabelMatcher) 208 | in.DeepCopyInto(out) 209 | return out 210 | } 211 | -------------------------------------------------------------------------------- /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: ">=v1.15.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.14.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 | ref: 65 | description: |- 66 | Ref is a named reference to a single ExtraResource. 67 | Either Ref or Selector is required. 68 | properties: 69 | name: 70 | description: The name of the object. 71 | type: string 72 | required: 73 | - name 74 | type: object 75 | selector: 76 | description: Selector selects ExtraResource(s) via labels. 77 | properties: 78 | matchLabels: 79 | description: MatchLabels ensures an object with matching 80 | labels is selected. 81 | items: 82 | description: |- 83 | An ResourceSourceSelectorLabelMatcher acts like a k8s label selector but 84 | can draw the label value from a different path. 85 | properties: 86 | fromFieldPathPolicy: 87 | default: Required 88 | description: |- 89 | FromFieldPathPolicy specifies the policy for the valueFromFieldPath. 90 | The default is Required, meaning that an error will be returned if the 91 | field is not found in the composite resource. 92 | Optional means that if the field is not found in the composite resource, 93 | that label pair will just be skipped. N.B. other specified label 94 | matchers will still be used to retrieve the desired 95 | resource config, if any. 96 | enum: 97 | - Optional 98 | - Required 99 | type: string 100 | key: 101 | description: Key of the label to match. 102 | type: string 103 | type: 104 | default: FromCompositeFieldPath 105 | description: Type specifies where the value for a 106 | label comes from. 107 | enum: 108 | - FromCompositeFieldPath 109 | - Value 110 | type: string 111 | value: 112 | description: Value specifies a literal label value. 113 | type: string 114 | valueFromFieldPath: 115 | description: ValueFromFieldPath specifies the field 116 | path to look for the label value. 117 | type: string 118 | required: 119 | - key 120 | type: object 121 | type: array 122 | maxMatch: 123 | description: MaxMatch specifies the number of extracted 124 | ExtraResources in Multiple mode, extracts all if nil. 125 | format: int64 126 | type: integer 127 | minMatch: 128 | description: MinMatch specifies the required minimum of 129 | extracted ExtraResources in Multiple mode. 130 | format: int64 131 | type: integer 132 | sortByFieldPath: 133 | default: metadata.name 134 | description: SortByFieldPath is the path to the field based 135 | on which list of ExtraResources is alphabetically sorted. 136 | type: string 137 | type: object 138 | type: 139 | default: Reference 140 | description: |- 141 | Type specifies the way the ExtraResource is selected. 142 | Default is `Reference` 143 | enum: 144 | - Reference 145 | - Selector 146 | type: string 147 | required: 148 | - into 149 | type: object 150 | type: array 151 | policy: 152 | description: |- 153 | Policy represents the Resolution policies which apply to all 154 | ResourceSourceReferences in ExtraResources list. 155 | properties: 156 | resolution: 157 | default: Required 158 | description: |- 159 | Resolution specifies whether resolution of this reference is required. 160 | The default is 'Required', which means the reconcile will fail if the 161 | reference cannot be resolved. 'Optional' means this reference will be 162 | a no-op if it cannot be resolved. 163 | enum: 164 | - Required 165 | - Optional 166 | type: string 167 | type: object 168 | required: 169 | - extraResources 170 | type: object 171 | type: object 172 | served: true 173 | storage: true 174 | -------------------------------------------------------------------------------- /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 beta render \ 6 | --extra-resources example/extraResources.yaml \ 7 | --include-context \ 8 | example/xr.yaml example/composition.yaml example/functions.yaml 9 | --------------------------------------------------------------------------------