Repository: crossplane/crossplane-runtime Branch: main Commit: 301f28c13d44 Files: 201 Total size: 1.6 MB Directory structure: gitextract_55jcywba/ ├── .coderabbit.yaml ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── renovate-base.json5 │ ├── renovate-earthly.json5 │ ├── renovate-entrypoint.sh │ ├── renovate-nix.json5 │ ├── renovate.json5 │ └── workflows/ │ ├── backport.yml │ ├── ci.yml │ ├── commands.yml │ ├── renovate.yml │ ├── stale.yml │ └── tag.yml ├── .gitignore ├── .golangci.yml ├── CODEOWNERS ├── DCO ├── LICENSE ├── OWNERS.md ├── PROJECT ├── README.md ├── RELEASE.md ├── SECURITY.md ├── apis/ │ ├── apis.go │ ├── changelogs/ │ │ └── proto/ │ │ └── v1alpha1/ │ │ ├── changelog.pb.go │ │ ├── changelog.proto │ │ └── changelog_grpc.pb.go │ ├── pipelineinspector/ │ │ └── proto/ │ │ └── v1alpha1/ │ │ ├── pipeline_inspector.pb.go │ │ ├── pipeline_inspector.proto │ │ └── pipeline_inspector_grpc.pb.go │ └── proto/ │ └── v1alpha1/ │ ├── ess.pb.go │ ├── ess.proto │ └── ess_grpc.pb.go ├── buf.gen.yaml ├── buf.yaml ├── flake.nix ├── generate.go ├── go.mod ├── go.sum ├── gomod2nix.toml ├── hack/ │ ├── boilerplate.go.txt │ └── linter-violation.tmpl ├── nix/ │ ├── apps.nix │ └── checks.nix ├── nix.sh ├── pkg/ │ ├── certificates/ │ │ ├── certificates.go │ │ ├── certificates_test.go │ │ └── test-data/ │ │ ├── certs/ │ │ │ ├── ca.crt │ │ │ ├── tls.crt │ │ │ └── tls.key │ │ ├── invalid-certs/ │ │ │ ├── ca.crt │ │ │ ├── tls.crt │ │ │ └── tls.key │ │ └── no-ca/ │ │ ├── tls.crt │ │ └── tls.key │ ├── conditions/ │ │ ├── manager.go │ │ └── manager_test.go │ ├── controller/ │ │ ├── gate.go │ │ └── options.go │ ├── errors/ │ │ ├── errors.go │ │ ├── errors_test.go │ │ ├── reconcile.go │ │ └── reconcile_test.go │ ├── event/ │ │ ├── event.go │ │ └── event_test.go │ ├── feature/ │ │ ├── feature.go │ │ ├── feature_test.go │ │ └── features.go │ ├── fieldpath/ │ │ ├── fieldpath.go │ │ ├── fieldpath_test.go │ │ ├── merge.go │ │ ├── merge_test.go │ │ ├── paved.go │ │ └── paved_test.go │ ├── gate/ │ │ ├── gate.go │ │ └── gate_test.go │ ├── logging/ │ │ ├── klog.go │ │ └── logging.go │ ├── meta/ │ │ ├── meta.go │ │ └── meta_test.go │ ├── password/ │ │ ├── password.go │ │ └── password_test.go │ ├── ratelimiter/ │ │ ├── default.go │ │ ├── reconciler.go │ │ └── reconciler_test.go │ ├── reconciler/ │ │ ├── customresourcesgate/ │ │ │ ├── reconciler.go │ │ │ ├── reconciler_test.go │ │ │ └── setup.go │ │ ├── doc.go │ │ ├── managed/ │ │ │ ├── api.go │ │ │ ├── api_test.go │ │ │ ├── changelogger.go │ │ │ ├── changelogger_test.go │ │ │ ├── doc.go │ │ │ ├── metrics.go │ │ │ ├── policies.go │ │ │ ├── reconciler.go │ │ │ ├── reconciler_deprecated.go │ │ │ ├── reconciler_legacy_test.go │ │ │ ├── reconciler_modern_test.go │ │ │ └── reconciler_typed.go │ │ └── providerconfig/ │ │ ├── reconciler.go │ │ └── reconciler_test.go │ ├── reference/ │ │ ├── namespaced_reference.go │ │ ├── namespaced_reference_test.go │ │ ├── reference.go │ │ └── reference_test.go │ ├── resource/ │ │ ├── api.go │ │ ├── api_test.go │ │ ├── doc.go │ │ ├── enqueue_handlers.go │ │ ├── enqueue_handlers_test.go │ │ ├── fake/ │ │ │ └── mocks.go │ │ ├── interfaces.go │ │ ├── interfaces_test.go │ │ ├── late_initializer.go │ │ ├── late_initializer_test.go │ │ ├── predicates.go │ │ ├── predicates_test.go │ │ ├── providerconfig.go │ │ ├── providerconfig_test.go │ │ ├── reference.go │ │ ├── reference_test.go │ │ ├── resource.go │ │ ├── resource_test.go │ │ └── unstructured/ │ │ ├── claim/ │ │ │ ├── claim.go │ │ │ ├── claim_test.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── client.go │ │ ├── client_test.go │ │ ├── composed/ │ │ │ ├── composed.go │ │ │ ├── composed_test.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── composite/ │ │ │ ├── composite.go │ │ │ ├── composite_test.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── generate.go │ │ └── reference/ │ │ └── reference.go │ ├── statemetrics/ │ │ ├── mr_state_metrics.go │ │ └── state_recorder.go │ ├── test/ │ │ ├── cmp.go │ │ ├── doc.go │ │ ├── fake.go │ │ └── retry.go │ ├── version/ │ │ ├── fake/ │ │ │ └── mocks.go │ │ ├── version.go │ │ └── version_test.go │ ├── webhook/ │ │ ├── mutator.go │ │ ├── mutator_test.go │ │ ├── validator.go │ │ └── validator_test.go │ ├── xcrd/ │ │ ├── composite.go │ │ ├── crd.go │ │ ├── crd_test.go │ │ ├── fuzz_test.go │ │ └── schemas.go │ └── xpkg/ │ ├── build.go │ ├── build_test.go │ ├── cache.go │ ├── cache_test.go │ ├── client.go │ ├── client_test.go │ ├── config.go │ ├── config_test.go │ ├── doc.go │ ├── fake/ │ │ ├── config.go │ │ └── mocks.go │ ├── fetch.go │ ├── find.go │ ├── find_test.go │ ├── fuzz_test.go │ ├── layers.go │ ├── lint.go │ ├── lint_test.go │ ├── name.go │ ├── name_test.go │ ├── parser/ │ │ ├── examples/ │ │ │ ├── parser.go │ │ │ └── parser_test.go │ │ ├── fsreader.go │ │ ├── fuzz_test.go │ │ ├── linter.go │ │ ├── linter_test.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ └── yaml/ │ │ └── parser.go │ ├── reader.go │ ├── scheme.go │ ├── scheme_test.go │ ├── signature/ │ │ ├── attestation.go │ │ ├── doc.go │ │ └── validate.go │ ├── testdata/ │ │ ├── examples/ │ │ │ ├── ec2/ │ │ │ │ ├── instance.yaml │ │ │ │ └── internetgateway.yaml │ │ │ ├── ecr/ │ │ │ │ └── repository.yaml │ │ │ └── provider.yaml │ │ ├── provider_meta.yaml │ │ └── providerconfigs.helm.crossplane.io.yaml │ └── validate.go └── test/ └── fuzz/ └── oss_fuzz_build.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coderabbit.yaml ================================================ # CodeRabbit Configuration for Crossplane Runtime # This configuration is optimized for the Crossplane Runtime Go library # ============================================================================= # GLOBAL SETTINGS # ============================================================================= # Language for CodeRabbit reviews and comments (default: en-US, keeping explicit) language: "en-US" # Instructions for CodeRabbit's tone and style in reviews (max 250 chars) tone_instructions: | Be collaborative and supportive. Ask clarifying questions rather than making assumptions. Focus on the 'why' behind decisions. Frame concerns constructively and thank contributors. # Disable early-access features for stability early_access: false # ============================================================================= # REVIEWS # ============================================================================= reviews: # We tested assertive and found it too verbose, e.g. approxing 200 comments on # https://github.com/crossplane/crossplane/pull/6777. Some of the nitpicks do # look valuable to me, but the signal to noise ratio isn't good enough. profile: "chill" # Don't generate summary in PR description - let authors write their own high_level_summary: false # Include the high-level summary in the walkthrough comment instead high_level_summary_in_walkthrough: true # Collapse walkthrough comment to reduce visual clutter in PRs collapse_walkthrough: true # Enable automatic label application auto_apply_labels: true # Automatically assign suggested reviewers (disabled - let maintainers control) auto_assign_reviewers: false # Disable poem generation in walkthrough comments poem: false # Disable review status messages to reduce comment noise review_status: false # Focus reviews on source code, exclude generated and vendor files path_filters: # Include source code - "**/*.go" - "**/*.yaml" - "**/*.yml" - "**/*.md" - "**/*.proto" - "**/Dockerfile*" - "**/flake.nix" - "**/*.sh" # Exclude generated and vendor files - "!**/zz_generated*.go" - "!**/vendor/**" - "!**/node_modules/**" - "!**/*.pb.go" - "!**/*.pb.gw.go" - "!**/mock_*.go" - "!**/fake/**" - "!**/testdata/**" - "!**/dist/**" - "!**/build/**" # Path-specific instructions for different areas of the codebase path_instructions: - path: "**/*.go" instructions: | Enforce Crossplane-specific patterns: Use crossplane-runtime/pkg/errors for wrapping. Check variable naming (short for local scope, descriptive for wider scope). Ensure 'return early' pattern. Verify error scoping (declare in conditionals when possible). For nolint directives, require specific linter names and explanations. CRITICAL: Ensure all error messages are meaningful to end users, not just developers - avoid technical jargon, include context about what the user was trying to do, and suggest next steps when possible. - path: "**/*_test.go" instructions: | Enforce table-driven test structure: PascalCase test names (no underscores), args/want pattern, use cmp.Diff with cmpopts.EquateErrors() for error testing. Check for proper test case naming and reason fields. Ensure no third-party test frameworks (no Ginkgo, Gomega, Testify). - path: "**/*.md" instructions: | Ensure Markdown files are wrapped at 100 columns for consistency and readability. Lines can be longer if it makes links more readable, but otherwise should wrap at 100 characters. Check for proper heading structure, clear language, and that documentation is helpful for users. - path: "**/apis/**" instructions: | Focus on API design following Kubernetes API conventions from https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md. Check for proper field naming (camelCase), appropriate types, validation tags, and documentation. Ask about backward compatibility and the impact on existing users and upgrade paths. Consider if changes need feature gates or alpha/beta graduation. Ensure error messages in validation are user-friendly. Pay attention to API consistency, proper use of optional vs required fields, and following established Kubernetes patterns. - path: "**/pkg/reconciler/**" instructions: | Review reconciler logic for proper reconciliation patterns, error handling, and resource management. Pay special attention to conditions and events: Conditions must be actionable for users (not developers), stable/deterministic, with proper Type/Reason/Message format. Events only when something actually happens, with specific details about what changed. No transient errors in conditions/events. All error messages must be meaningful to end users - include context about what resource/operation failed and why. - path: "**/test/**" instructions: | Focus on test coverage, test clarity, and proper use of testing utilities. Ask about testing scenarios and edge cases. Ensure tests are maintainable and cover the happy path and error conditions. Verify error testing uses proper patterns (cmpopts.EquateErrors, sentinel errors for complex cases). # Automatic review settings auto_review: # Skip reviewing draft PRs until they're ready for review (default: false, keeping explicit) drafts: false # Skip reviews if PR title contains these keywords (case-insensitive) ignore_title_keywords: - "wip" - "draft" - "do not merge" - "dnm" # Skip reviews from these automated bot accounts ignore_usernames: - "dependabot[bot]" - "renovate[bot]" - "github-actions[bot]" # Quality gates that run during CodeRabbit's review to check PR readiness pre_merge_checks: # Check PR title for length and descriptiveness title: requirements: "Keep under 72 characters and be descriptive about what the change does." # Disable docstring coverage check (too noisy for Go projects) docstrings: mode: "off" # Custom checks specific to Crossplane Runtime development practices custom_checks: - name: "Breaking Changes" mode: "error" instructions: | Fails if any public Go code (exported functions, types, methods, or fields) in '**/*.go' (excluding *_test.go and generated files) is removed, renamed, has signature changes, or has behavior changes that could break existing users, without the 'breaking-change' label. This is a library repo - all exported APIs are public. # Disable automatic code generation features finishing_touches: # Disable automatic docstring generation docstrings: enabled: false # Disable automatic unit test generation unit_tests: enabled: false # Tools - DISABLED: We prefer to run linting tools directly in CI # Our comprehensive golangci-lint setup with "default: all" already covers # most static analysis. Additional tools can be added to CI as needed. tools: # Go linting - disabled (we run golangci-lint with comprehensive config) golangci-lint: enabled: false # Security and vulnerability scanning - disabled (prefer direct CI integration) gitleaks: enabled: false semgrep: enabled: false osvScanner: enabled: false # File format linting - disabled (prefer direct CI integration) yamllint: enabled: false markdownlint: enabled: false shellcheck: enabled: false hadolint: enabled: false actionlint: enabled: false buf: enabled: false # GitHub integration - disabled for now github-checks: enabled: false # ============================================================================= # CHAT # Interactive chat with CodeRabbit in PR comments. You can ask questions like: # - @coderabbitai explain this error handling approach # - @coderabbitai what are the edge cases for this function? # - @coderabbitai how does this affect backward compatibility? # - @coderabbitai generate unit tests for this function # ============================================================================= chat: # Disable ASCII/emoji art in responses art: false # ============================================================================= # KNOWLEDGE BASE # ============================================================================= knowledge_base: # Enable MCP integration to provide context about external libraries and APIs mcp: usage: "enabled" ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug Report about: Help us diagnose and fix bugs in Crossplane labels: bug --- ### What happened? ### How can we reproduce it? ### What environment did it happen in? Crossplane version: ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature Request about: Help us make Crossplane more useful labels: enhancement --- ### What problem are you facing? ### How could Crossplane help solve your problem? ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Description of your changes Fixes # I have: - [ ] Read and followed Crossplane's [contribution process]. - [ ] Run `./nix.sh flake check` to ensure this PR is ready for review. - [ ] Added or updated unit tests. - [ ] Linked a PR or a [docs tracking issue] to [document this change]. - [ ] Added `backport release-x.y` labels to auto-backport this PR. Need help with this checklist? See the [cheat sheet]. [contribution process]: https://github.com/crossplane/crossplane/tree/main/contributing [docs tracking issue]: https://github.com/crossplane/docs/issues/new [document this change]: https://docs.crossplane.io/contribute/contribute [cheat sheet]: https://github.com/crossplane/crossplane/tree/main/contributing#checklist-cheat-sheet ================================================ FILE: .github/renovate-base.json5 ================================================ { $schema: 'https://docs.renovatebot.com/renovate-schema.json', extends: [ 'config:recommended', 'helpers:pinGitHubActionDigests', ':semanticCommits', ], // We only want renovate to rebase PRs when they have conflicts, default // "auto" mode is not required. rebaseWhen: 'conflicted', // The maximum number of PRs to be created in parallel prConcurrentLimit: 5, // The branches renovate should target // PLEASE UPDATE THIS WHEN RELEASING. baseBranches: [ 'main', 'release-1.20', 'release-2.0', 'release-2.1', 'release-2.2', 'release-2.3', ], ignorePaths: [ 'design/**', ], postUpdateOptions: [ 'gomodTidy', ], // All PRs should have a label labels: [ 'automated', ], // PackageRules disabled below should be enabled in case of vulnerabilities vulnerabilityAlerts: { enabled: true, }, osvVulnerabilityAlerts: true, // Renovate evaluates all packageRules in order, so low priority rules should // be at the beginning, high priority at the end packageRules: [ { description: 'Ignore non-security related updates to release branches', matchBaseBranches: [ '/^release-.*/', ], enabled: false, }, { description: 'Still update Docker images on release branches though', matchDatasources: [ 'docker', ], matchBaseBranches: [ '/^release-.*/', ], enabled: true, }, { description: 'Only get Docker image updates every 2 weeks to reduce noise', matchDatasources: [ 'docker', ], schedule: [ 'every 2 week on monday', ], enabled: true, }, { description: "Ignore k8s.io/client-go older versions, they switched to semantic version and old tags are still available in the repo", matchDatasources: [ 'go', ], matchDepNames: [ 'k8s.io/client-go', ], allowedVersions: '<1.0', }, { description: 'Only get dependency digest updates every month to reduce noise', matchDatasources: [ 'go', ], matchUpdateTypes: [ 'digest', ], extends: [ 'schedule:monthly', ], }, { description: "Ignore oss-fuzz, it's not using tags, we'll stick to master", matchDepTypes: [ 'action', ], matchDepNames: [ 'google/oss-fuzz', ], enabled: false, }, { description: 'Group all go version updates', matchDatasources: [ 'golang-version', ], groupName: 'golang version', }, ], } ================================================ FILE: .github/renovate-earthly.json5 ================================================ { // Earthly-specific configuration for release branches. // Main branch uses Nix - see renovate-nix.json5. customManagers: [ { customType: 'regex', description: 'Bump Earthly version in GitHub workflows', fileMatch: [ '^\\.github\\/workflows\\/[^/]+\\.ya?ml$', ], matchStrings: [ "EARTHLY_VERSION: '(?.*?)'\\n", ], datasourceTemplate: 'github-releases', depNameTemplate: 'earthly/earthly', extractVersionTemplate: '^v(?.*)$', }, { customType: 'regex', description: 'Bump Go version in Earthfile', fileMatch: [ '^Earthfile$', ], matchStrings: [ 'ARG --global GO_VERSION=(?.*?)\\n', ], datasourceTemplate: 'golang-version', depNameTemplate: 'golang', }, { customType: 'regex', description: 'Bump golangci-lint version in the Earthfile', fileMatch: [ '^Earthfile$', ], matchStrings: [ 'ARG GOLANGCI_LINT_VERSION=(?.*?)\\n', ], datasourceTemplate: 'github-releases', depNameTemplate: 'golangci/golangci-lint', }, { customType: 'regex', description: 'Bump codeql version in the Earthfile', fileMatch: [ '^Earthfile$', ], matchStrings: [ 'ARG CODEQL_VERSION=(?.*?)\\n', ], datasourceTemplate: 'github-releases', depNameTemplate: 'github/codeql-action', extractVersionTemplate: '^codeql-bundle-(?.*)$', }, ], // Renovate doesn't have native Earthfile support, but because Earthfile // syntax is a superset of Dockerfile syntax this works to update FROM images. // https://github.com/renovatebot/renovate/issues/15975 dockerfile: { fileMatch: [ '(^|/)Earthfile$', ], }, packageRules: [ { description: 'Generate code after upgrading go dependencies (Earthly)', matchDatasources: [ 'go', ], matchBaseBranches: [ // Release 2.1 and older use earthly. '/^release-1\..*/', '/^release-2\.[0-1]$/', ], postUpgradeTasks: { commands: [ 'earthly --strict +go-generate', ], fileFilters: [ '**/*', ], executionMode: 'update', }, }, { description: 'Lint code after upgrading golangci-lint (Earthly)', matchDepNames: [ 'golangci/golangci-lint', ], matchBaseBranches: [ // Release 2.1 and older use earthly. '/^release-1\..*/', '/^release-2\.[0-1]$/', ], postUpgradeTasks: { commands: [ 'earthly --strict +go-lint', ], fileFilters: [ '**/*', ], executionMode: 'update', }, }, ], } ================================================ FILE: .github/renovate-entrypoint.sh ================================================ #!/bin/bash set -e # Install Earthly (for release branches) echo "Installing Earthly..." curl -fsSLo /usr/local/bin/earthly https://github.com/earthly/earthly/releases/latest/download/earthly-linux-amd64 chmod +x /usr/local/bin/earthly /usr/local/bin/earthly bootstrap # Install Nix (for main branch) echo "Installing Nix..." apt-get update && apt-get install -y nix-bin # Configure Nix mkdir -p /etc/nix cat > /etc/nix/nix.conf << 'EOF' # Enable flakes and the nix command (e.g. nix run, nix build). experimental-features = nix-command flakes # Run builds as the calling user, not dedicated nixbld users. This avoids # needing to create the nixbld group and users in this ephemeral container. build-users-group = # Build derivations in parallel, one per CPU core. max-jobs = auto # Use the Crossplane Cachix cache to download pre-built binaries from CI. extra-substituters = https://crossplane.cachix.org extra-trusted-public-keys = crossplane.cachix.org-1:NJluVUN9TX0rY/zAxHYaT19Y5ik4ELH4uFuxje+62d4= EOF echo "Nix $(nix --version) installed successfully" renovate ================================================ FILE: .github/renovate-nix.json5 ================================================ { // Nix-specific configuration for main branch. // Release branches use Earthly - see renovate-earthly.json5. // Enable the nix manager to update flake.lock when flake inputs change. nix: { enabled: true, }, packageRules: [ { description: 'Update flake.lock monthly', matchManagers: [ 'nix', ], extends: [ 'schedule:monthly', ], }, { description: 'Regenerate gomod2nix.toml and generated code after upgrading go dependencies (Nix)', matchDatasources: [ 'go', ], matchBaseBranches: [ 'main', // Release 2.2 and newer use nix. '/^release-2\.([2-9]|..+)$/', ], postUpgradeTasks: { commands: [ 'nix run .#tidy', 'nix run .#generate', ], fileFilters: [ '**/*', ], executionMode: 'update', }, }, { description: 'Lint code after upgrading golangci-lint (Nix)', matchDepNames: [ 'golangci/golangci-lint', ], matchBaseBranches: [ 'main', // Release 2.2 and newer use nix. '/^release-2\.([2-9]|..+)$/', ], postUpgradeTasks: { commands: [ 'nix run .#lint', ], fileFilters: [ '**/*', ], executionMode: 'update', }, }, ], } ================================================ FILE: .github/renovate.json5 ================================================ { // This is the main Renovate configuration file. // It extends base config and build-tool-specific configs. // // - renovate-base.json5: Common configuration for all branches // - renovate-earthly.json5: Earthly-specific config for release branches // - renovate-nix.json5: Nix-specific config for main branch // // The local> syntax requires owner/repo format with // for paths. // See: https://docs.renovatebot.com/config-presets/#local-presets extends: [ 'local>crossplane/crossplane-runtime//.github/renovate-base.json5', 'local>crossplane/crossplane-runtime//.github/renovate-earthly.json5', 'local>crossplane/crossplane-runtime//.github/renovate-nix.json5', ], } ================================================ FILE: .github/workflows/backport.yml ================================================ name: Backport on: # NOTE(negz): This is a risky target, but we run this action only when and if # a PR is closed, then filter down to specifically merged PRs. We also don't # invoke any scripts, etc from within the repo. I believe the fact that we'll # be able to review PRs before this runs makes this fairly safe. # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ pull_request_target: types: [closed] # See also commands.yml for the /backport triggered variant of this workflow. jobs: # NOTE(negz): I tested many backport GitHub actions before landing on this # one. Many do not support merge commits, or do not support pull requests with # more than one commit. This one does. It also handily links backport PRs with # new PRs, and provides commentary and instructions when it can't backport. # The main gotchas with this action are that it _only_ supports merge commits, # and that PRs _must_ be labelled before they're merged to trigger a backport. open-pr: runs-on: ubuntu-22.04 if: github.event.pull_request.merged steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Open Backport PR uses: zeebe-io/backport-action@3c06f323a58619da1e8522229ebc8d5de2633e46 # v4.3.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} github_workspace: ${{ github.workspace }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main - release-* pull_request: {} workflow_dispatch: {} env: # We can't run a step 'if secrets.FOO != ""' but we can run a step # 'if env.FOO' != ""', so we copy secrets to env vars for conditional checks. DOCKER_USR: ${{ secrets.DOCKER_USR }} jobs: check-diff: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Nix uses: cachix/install-nix-action@616559265b40713947b9c190a8ff4b507b5df49b # v31 - name: Setup Cachix uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: crossplane authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: Verify Generated Code run: nix build .#checks.x86_64-linux.generate --print-build-logs validate-renovate-config: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # renovate-config-validator only looks at the top-level file and does # not recursively resolve local> presets, so we also syntax-check every # renovate*.json5 with the json5 CLI to catch preset parse errors at # PR time rather than 24h later in the scheduled Renovate job. - name: Validate Renovate preset syntax run: | for f in .github/renovate*.json5; do npx --yes json5 "$f" > /dev/null done - name: Validate Renovate JSON run: npx --yes --package renovate -- renovate-config-validator lint: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Nix uses: cachix/install-nix-action@616559265b40713947b9c190a8ff4b507b5df49b # v31 - name: Setup Cachix uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: crossplane authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: Lint run: nix build .#checks.x86_64-linux.go-lint --print-build-logs codeql: runs-on: ubuntu-22.04 permissions: contents: read security-events: write steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Nix uses: cachix/install-nix-action@616559265b40713947b9c190a8ff4b507b5df49b # v31 - name: Setup Cachix uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: crossplane authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: Setup Nix Environment uses: nicknovitski/nix-develop@9be7cfb4b10451d3390a75dc18ad0465bed4932a # v1 - name: Initialize CodeQL uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4 with: languages: go - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4 unit-tests: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Nix uses: cachix/install-nix-action@616559265b40713947b9c190a8ff4b507b5df49b # v31 - name: Setup Cachix uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: crossplane authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: Run Unit Tests run: nix build .#checks.x86_64-linux.test --print-build-logs - name: Publish Unit Test Coverage uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 with: flags: unittests file: result/coverage.txt token: ${{ secrets.CODECOV_TOKEN }} protobuf-schemas: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Buf uses: bufbuild/buf-setup-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Lint Protocol Buffers uses: bufbuild/buf-lint-action@v1 # buf-breaking-action doesn't support branches # https://github.com/bufbuild/buf-push-action/issues/34 - name: Detect Breaking Changes in Protocol Buffers uses: bufbuild/buf-breaking-action@a074e988ee34efcd4927079e79c611f428354c01 # v1 # We want to run this for the main branch, and PRs against main. if: ${{ github.ref == 'refs/heads/main' || github.base_ref == 'main' }} with: against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=main" - name: Push Protocol Buffers to Buf Schema Registry if: ${{ github.repository == 'crossplane/crossplane-runtime' && github.ref == 'refs/heads/main' }} uses: bufbuild/buf-push-action@v1 with: buf_token: ${{ secrets.BUF_TOKEN }} ================================================ FILE: .github/workflows/commands.yml ================================================ name: Comment Commands on: issue_comment jobs: points: runs-on: ubuntu-22.04 if: startsWith(github.event.comment.body, '/points') steps: - name: Extract Command id: command uses: xt0rted/slash-command-action@bf51f8f5f4ea3d58abc7eca58f77104182b23e88 # v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} command: points reaction: "true" reaction-type: "eyes" allow-edits: "false" permission-level: write - name: Handle Command uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: POINTS: ${{ steps.command.outputs.command-arguments }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const points = process.env.POINTS if (isNaN(parseInt(points))) { console.log("Malformed command - expected '/points '") github.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: context.payload.comment.id, content: "confused" }) return } const label = "points/" + points // Delete our needs-points-label label. try { await github.issues.deleteLabel({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, name: ['needs-points-label'] }) console.log("Deleted 'needs-points-label' label.") } catch(e) { console.log("Label 'needs-points-label' probably didn't exist.") } // Add our points label. github.issues.addLabels({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, labels: [label] }) console.log("Added '" + label + "' label.") # NOTE(negz): See also backport.yml, which is the variant that triggers on PR # merge rather than on comment. backport: runs-on: ubuntu-22.04 if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/backport') steps: - name: Extract Command id: command uses: xt0rted/slash-command-action@bf51f8f5f4ea3d58abc7eca58f77104182b23e88 # v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} command: backport reaction: "true" reaction-type: "eyes" allow-edits: "false" permission-level: write - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Open Backport PR uses: zeebe-io/backport-action@3c06f323a58619da1e8522229ebc8d5de2633e46 # v4.3.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} github_workspace: ${{ github.workspace }} fresh: runs-on: ubuntu-22.04 if: startsWith(github.event.comment.body, '/fresh') steps: - name: Extract Command id: command uses: xt0rted/slash-command-action@bf51f8f5f4ea3d58abc7eca58f77104182b23e88 # v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} command: fresh reaction: "true" reaction-type: "eyes" allow-edits: "false" permission-level: read - name: Handle Command uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} labels: stale ================================================ FILE: .github/workflows/renovate.yml ================================================ name: Renovate on: # Allows manual/automated trigger for debugging purposes workflow_dispatch: inputs: logLevel: description: "Renovate's log level" required: true default: "info" type: string schedule: - cron: '0 8 * * *' env: LOG_LEVEL: "info" jobs: renovate: runs-on: ubuntu-latest if: | !github.event.repository.fork && !github.event.pull_request.head.repo.fork steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Don't waste time starting Renovate if any preset is unparseable. # renovate-config-validator only looks at the top-level file and does # not recursively resolve local> presets, so we also syntax-check every # renovate*.json5 with the json5 CLI. - name: Validate Renovate preset syntax run: | for f in .github/renovate*.json5; do npx --yes json5 "$f" > /dev/null done - name: Validate Renovate JSON run: npx --yes --package renovate -- renovate-config-validator - name: Get token id: get-github-app-token uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2 with: app-id: ${{ secrets.RENOVATE_GITHUB_APP_ID }} private-key: ${{ secrets.RENOVATE_GITHUB_APP_PRIVATE_KEY }} - name: Self-hosted Renovate uses: renovatebot/github-action@eb932558ad942cccfd8211cf535f17ff183a9f74 # v46.1.9 env: RENOVATE_REPOSITORIES: ${{ github.repository }} # Use GitHub API to create commits RENOVATE_PLATFORM_COMMIT: "true" LOG_LEVEL: ${{ github.event.inputs.logLevel || env.LOG_LEVEL }} RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^nix .+", "^earthly .+"]' with: configurationFile: .github/renovate.json5 token: '${{ steps.get-github-app-token.outputs.token }}' mount-docker-socket: true docker-user: root docker-cmd-file: .github/renovate-entrypoint.sh ================================================ FILE: .github/workflows/stale.yml ================================================ name: Stale Issues and PRs on: schedule: # Process new stale issues once a day. Folks can /fresh for a fast un-stale # per the commands workflow. Run at 1:15 mostly as a somewhat unique time to # help correlate any issues with this workflow. - cron: '15 1 * * *' workflow_dispatch: {} permissions: issues: write pull-requests: write jobs: stale: runs-on: ubuntu-22.04 steps: - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 with: # This action uses ~2 operations per stale issue per run to determine # whether it's still stale. It also uses 2-3 operations to mark an issue # stale or not. During steady state (no issues to mark stale, check, or # close) we seem to use less than 10 operations with ~150 issues and PRs # open. # # Our hourly rate-limit budget for all workflows that use GITHUB_TOKEN # is 1,000 requests per the below docs. # https://docs.github.com/en/rest/overview/resources-in-the-rest-api#requests-from-github-actions operations-per-run: 100 days-before-stale: 90 days-before-close: 14 stale-issue-label: stale exempt-issue-labels: exempt-from-stale stale-issue-message: > Crossplane does not currently have enough maintainers to address every issue and pull request. This issue has been automatically marked as `stale` because it has had no activity in the last 90 days. It will be closed in 14 days if no further activity occurs. Leaving a comment **starting with** `/fresh` will mark this issue as not stale. stale-pr-label: stale exempt-pr-labels: exempt-from-stale stale-pr-message: Crossplane does not currently have enough maintainers to address every issue and pull request. This pull request has been automatically marked as `stale` because it has had no activity in the last 90 days. It will be closed in 14 days if no further activity occurs. Adding a comment **starting with** `/fresh` will mark this PR as not stale. ================================================ FILE: .github/workflows/tag.yml ================================================ name: Tag on: workflow_dispatch: inputs: version: description: 'Release version (e.g. v0.1.0)' required: true message: description: 'Tag message' required: true jobs: create-tag: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Create Tag uses: negz/create-tag@39bae1e0932567a58c20dea5a1a0d18358503320 # v1 with: version: ${{ github.event.inputs.version }} message: ${{ github.event.inputs.message }} token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ /.cache /.work /_output /config/ /config cover.out /vendor /.vendor-new # Nix build output result result-* # gitlab example # exclude files generate by running the example external-dns-*.tgz gitlab-*.tgz gitlab-gcp.yaml gitlab/ # ignore IDE folders .vscode/ .idea/ ================================================ FILE: .golangci.yml ================================================ version: "2" output: formats: text: path: stderr linters: default: all disable: # These are linters we'd like to enable, but that will be labor intensive to # make existing code compliant. - wrapcheck - varnamelen - testpackage - paralleltest - nilnil - funcorder # Below are linters that lint for things we don't value. Each entry below # this line must have a comment explaining the rationale. # These linters add whitespace in an attempt to make code more readable. # This isn't a widely accepted Go best practice, and would be laborious to # apply to existing code. - wsl - wsl_v5 - nlreturn # Warns about uses of fmt.Sprintf that are less performant than alternatives # such as string concatenation. We value readability more than performance # unless performance is measured to be an issue. - perfsprint # This linter: # # 1. Requires errors.Is/errors.As to test equality. # 2. Requires all errors be wrapped with fmt.Errorf specifically. # 3. Disallows errors.New inline - requires package level errors. # # 1 is covered by other linters. 2 is covered by wrapcheck, which can also # handle our use of crossplane-runtime's errors package. 3 is more strict # than we need. Not every error needs to be tested for equality. - err113 # These linters duplicate gocognit, but calculate complexity differently. - gocyclo - cyclop - nestif - funlen - maintidx # Enforces max line length. It's not idiomatic to enforce a strict limit on # line length in Go. We'd prefer to lint for things that often cause long # lines, like functions with too many parameters or long parameter names # that duplicate their types. - lll # Warns about struct instantiations that don't specify every field. Could be # useful in theory to catch fields that are accidentally omitted. Seems like # it would have many more false positives than useful catches, though. - exhaustruct # Warns about TODO comments. The rationale being they should be issues # instead. We're okay with using TODO to track minor cleanups for next time # we touch a particular file. - godox # Warns about duplicated code blocks within the same file. Could be useful # to prompt folks to think about whether code should be broken out into a # function, but generally we're less worried about DRY and fine with a # little copying. We don't want to give folks the impression that we require # every duplicated code block to be factored out into a function. - dupl # Warns about returning interfaces rather than concrete types. We do think # it's best to avoid returning interfaces where possible. However, at the # time of writing enabling this linter would only catch the (many) cases # where we must return an interface. - ireturn # Warns about returning named variables. We do think it's best to avoid # returning named variables where possible. However, at the time of writing # enabling this linter would only catch the (many) cases where returning # named variables is useful to document what the variables are. For example # we believe it makes sense to return (ready bool) rather than just (bool) # to communicate what the bool means. - nonamedreturns # Warns about using magic numbers. We do think it's best to avoid magic # numbers, but we should not be strict about it. - mnd # Warns about if err := Foo(); err != nil style error checks. Seems to go # against idiomatic Go programming, which encourages this approach - e.g. # to scope errors. - noinlineerr settings: depguard: rules: no_third_party_test_libraries: list-mode: lax files: - $test deny: - pkg: github.com/stretchr/testify desc: See https://go.dev/wiki/TestComments#assert-libraries - pkg: github.com/onsi/ginkgo desc: See https://go.dev/wiki/TestComments#assert-libraries - pkg: github.com/onsi/gomega desc: See https://go.dev/wiki/TestComments#assert-libraries dupl: threshold: 100 errcheck: check-type-assertions: false check-blank: false goconst: min-len: 3 min-occurrences: 5 gocritic: enabled-tags: - performance settings: captLocal: paramsOnly: true rangeValCopy: sizeThreshold: 32 govet: disable: - shadow interfacebloat: max: 5 lll: tab-width: 1 nakedret: max-func-lines: 30 nolintlint: require-explanation: true require-specific: true prealloc: simple: true range-loops: true for-loops: false tagliatelle: case: rules: json: goCamel unparam: check-exported: false unused: exported-fields-are-used: true exclusions: generated: lax rules: - linters: - containedctx - errcheck - forcetypeassert - gochecknoglobals - gochecknoinits - gocognit - goconst - gosec - scopelint - unparam - embeddedstructfieldcheck path: _test(ing)?\.go - linters: - gocritic path: _test\.go text: (unnamedResult|exitAfterDefer) # It's idiomatic to register Kubernetes types with a package scoped # SchemeBuilder using an init function. - linters: - gochecknoglobals - gochecknoinits path: apis/ # These are performance optimisations rather than style issues per se. # They warn when function arguments or range values copy a lot of memory # rather than using a pointer. - linters: - gocritic text: '(hugeParam|rangeValCopy):' # This "TestMain should call os.Exit to set exit code" warning is not clever # enough to notice that we call a helper method that calls os.Exit. - linters: - staticcheck text: 'SA3000:' # This is a "potential hardcoded credentials" warning. It's triggered by # any variable with 'secret' in the same, and thus hits a lot of false # positives in Kubernetes land where a Secret is an object type. - linters: - gosec text: 'G101:' # This is an 'errors unhandled' warning that duplicates errcheck. - linters: - gosec text: 'G104:' # This is about implicit memory aliasing in a range loop. # This is a false positive with Go v1.22 and above. - linters: - gosec text: 'G601:' # Some k8s dependencies do not have JSON tags on all fields in structs. - linters: - musttag path: k8s.io/ # Various fields related to native patch and transform Composition are # deprecated, but we can't drop support from Crossplane 1.x. We ignore the # warnings globally instead of suppressing them with comments everywhere. - linters: - staticcheck text: 'SA1019: .+ is deprecated: Use Composition Functions instead.' # The runtime library defines deprecated types like LegacyManaged and # LegacyProviderConfigUsage. Code in the runtime must use these types to # support legacy resources, even though they're deprecated. - linters: - staticcheck text: 'SA1019: resource\.Legacy.+ is deprecated:' # Some shared structs in apis/common/v1 are moved to # apis/common. To preserve a backward-compatible directory structure # package had to be named common, which we suppress. - linters: - revive text: "var-naming: avoid meaningless package names" path: apis/common # The errors package intentionally shadows the stdlib errors package # to provide a drop-in compatible API with additional functionality. - linters: - revive text: "var-naming: avoid package names that conflict with Go standard library package names" path: pkg/errors/ paths: - zz_generated\..+\.go$ - .+\.pb.go$ - third_party$ - builtin$ - examples$ issues: max-issues-per-linter: 0 max-same-issues: 0 new: false formatters: enable: - gci - gofmt - gofumpt - goimports settings: gci: sections: - standard - default - prefix(github.com/crossplane/crossplane-runtime) - blank - dot custom-order: true gofmt: simplify: true exclusions: generated: lax paths: - zz_generated\..+\.go$ - .+\.pb.go$ - third_party$ - builtin$ - examples$ ================================================ FILE: CODEOWNERS ================================================ # This file controls automatic PR reviewer assignment. See the following docs: # # * https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # * https://docs.github.com/en/organizations/organizing-members-into-teams/managing-code-review-settings-for-your-team # # The goal of this file is for most PRs to automatically and fairly have one # maintainer and two reviewers set as PR reviewers. All maintainers have # permission to approve and merge PRs, but reviewers do not. Most PRs should be # reviewed by members of the reviewers group before being passed to a maintainer # for final review. # # This in part depends on how the groups in this file are configured. # # @crossplane/steering-committee - Assigns 3 members. Admin perms to this repo. # @crossplane/crossplane-maintainers - Assigns 1 member. Maintain perms to this repo. # @crossplane/crossplane-reviewers - Assigns 2 members. Write perms to this repo. # # Where possible, prefer explicitly specifying a maintainer who is a subject # matter expert for a particular part of the codebase rather than using the # @crossplane/crossplane-maintainers group. # # See also OWNERS.md for governance details # Fallback owners * @crossplane/crossplane-maintainers # Governance owners - steering committee /README.md @crossplane/steering-committee /OWNERS.md @crossplane/steering-committee /LICENSE @crossplane/steering-committee ================================================ FILE: DCO ================================================ Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 York Street, Suite 102, San Francisco, CA 94110 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2016 The Crossplane Authors. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: OWNERS.md ================================================ # Crossplane Maintainers This page lists all active maintainers and reviewers for **this** repository. Each repository in the [Crossplane organization](https://github.com/crossplane/) will list their repository maintainers and reviewers in their own `OWNERS.md` file. Please see [GOVERNANCE.md](https://github.com/crossplane/crossplane/blob/main/GOVERNANCE.md) for governance guidelines and responsibilities for maintainers, and reviewers. See [CODEOWNERS](CODEOWNERS) for automatic PR assignment. ## Maintainers * Nic Cope ([negz](https://github.com/negz)) * Bob Haddleton ([bobh66](https://github.com/bobh66)) * Philippe Scorsolini ([phisco](https://github.com/phisco)) * Jared Watts ([jbw976](https://github.com/jbw976)) * Adam Wolfe Gordon ([adamwg](https://github.com/adamwg)) * Christopher Haar ([haarchri](https://github.com/haarchri)) ## Reviewers * Yury Tsarev ([ytsarev](https://github.com/ytsarev)) * Ezgi Demirel ([ezgidemirel](https://github.com/ezgidemirel)) * Max Blatt ([MisterMX](https://github.com/MisterMX)) ## Emeritus maintainers * Illya Chekrygin ([ichekrygin](https://github.com/ichekrygin)) * Hasan Turken ([turkenh](https://github.com/turkenh)) ================================================ FILE: PROJECT ================================================ version: "1" domain: crossplane.io repo: github.com/crossplane/crossplane-runtime ================================================ FILE: README.md ================================================ # crossplane-runtime [![CI](https://github.com/crossplane/crossplane-runtime/actions/workflows/ci.yml/badge.svg)](https://github.com/crossplane/crossplane-runtime/actions/workflows/ci.yml) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/crossplane/crossplane-runtime) [![Godoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/crossplane/crossplane-runtime) ## Overview crossplane-runtime is a set of go libraries used to build Kubernetes controllers in Crossplane and its related stacks. Take a look at our [developer guide] and [API documentation] for help getting started with crossplane-runtime. ## Contributing crossplane-runtime is a community driven project and we welcome contributions. See the Crossplane [contributing] guidelines to get started. ## Report a Bug For filing bugs, suggesting improvements, or requesting new features, please open an [issue]. ## Contact Please use the following to reach members of the community: - Slack: Join our [slack channel] - Forums: [crossplane-dev] - Twitter: [@crossplane_io] - Email: [info@crossplane.io] ## Roadmap crossplane-runtime goals and milestones are currently tracked in Crossplane's [roadmap]. ## Governance and Owners crossplane-runtime is run according to the same [governance] and [ownership] structure as the core Crossplane project. ## Code of Conduct crossplane-runtime adheres to the same [code of conduct] as the core Crossplane project. ## Licensing crossplane-runtime is under the Apache 2.0 license. [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcrossplane%2Fcrossplane-runtime.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fcrossplane%2Fcrossplane-runtime?ref=badge_large) [developer guide]: https://github.com/crossplane/crossplane/tree/main/contributing [API documentation]: https://godoc.org/github.com/crossplane/crossplane-runtime [contributing]: https://github.com/crossplane/crossplane/blob/main/CONTRIBUTING.md [issue]: https://github.com/crossplane/crossplane-runtime/issues [slack channel]: https://slack.crossplane.io [crossplane-dev]: https://groups.google.com/forum/#!forum/crossplane-dev [@crossplane_io]: https://twitter.com/crossplane_io [info@crossplane.io]: mailto:info@crossplane.io [roadmap]: https://github.com/crossplane/crossplane/blob/main/ROADMAP.md [governance]: https://github.com/crossplane/crossplane/blob/main/GOVERNANCE.md [ownership]: https://github.com/crossplane/crossplane/blob/main/OWNERS.md [code of conduct]: https://github.com/crossplane/crossplane/blob/main/CODE_OF_CONDUCT.md ================================================ FILE: RELEASE.md ================================================ # Release Process ## New Patch Release (vX.Y.Z) In order to cut a new patch release from an existing release branch `release-X.Y`, follow these steps: - Run the [Tag workflow][tag-workflow] on the `release-X.Y` branch with the proper release version, `vX.Y.Z`. Message suggested, but not required: `Release vX.Y.Z`. - Draft the [new release notes], and share them with the rest of the team to ensure that all the required information is included. - Publish the above release notes. ## New Minor Release (vX.Y.0) In order to cut a new minor release, follow these steps: - Create a new release branch `release-X.Y` from `main`, using the [GitHub UI][create-branch]. - Create and merge an empty commit to the `main` branch, if required to have it at least one commit ahead of the release branch. - Run the [Tag workflow][tag-workflow] on the `main` branch with the release candidate tag for the next release, so `vX..0-rc.0`. - Run the [Tag workflow][tag-workflow] on the `release-X.Y` branch with the proper release version, `vX.Y.0`. Message suggested, but not required: `Release vX.Y.0`. - Draft the [new release notes], and share them with the rest of the team to ensure that all the required information is included. - Publish the above release notes. [create-branch]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository [new release notes]: https://github.com/crossplane/crossplane-runtime/releases/new [tag-workflow]: https://github.com/crossplane/crossplane-runtime/actions/workflows/tag.yml ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability Instructions for reporting a vulnerability can be found on the [crossplane repository](https://github.com/crossplane/crossplane/blob/main/SECURITY.md). ================================================ FILE: apis/apis.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package apis contains Kubernetes API groups. package apis ================================================ FILE: apis/changelogs/proto/v1alpha1/changelog.pb.go ================================================ // //Copyright 2024 The Crossplane Authors. //Licensed under the Apache License, Version 2.0 (the "License"); //you may not use this file except in compliance with the License. //You may obtain a copy of the License at //http://www.apache.org/licenses/LICENSE-2.0 //Unless required by applicable law or agreed to in writing, software //distributed under the License is distributed on an "AS IS" BASIS, //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //See the License for the specific language governing permissions and //limitations under the License. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc (unknown) // source: apis/changelogs/proto/v1alpha1/changelog.proto // buf:lint:ignore PACKAGE_DIRECTORY_MATCH package v1alpha1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // OperationType represents the type of operation that was performed on a // resource. type OperationType int32 const ( OperationType_OPERATION_TYPE_UNSPECIFIED OperationType = 0 OperationType_OPERATION_TYPE_CREATE OperationType = 1 OperationType_OPERATION_TYPE_UPDATE OperationType = 2 OperationType_OPERATION_TYPE_DELETE OperationType = 3 ) // Enum value maps for OperationType. var ( OperationType_name = map[int32]string{ 0: "OPERATION_TYPE_UNSPECIFIED", 1: "OPERATION_TYPE_CREATE", 2: "OPERATION_TYPE_UPDATE", 3: "OPERATION_TYPE_DELETE", } OperationType_value = map[string]int32{ "OPERATION_TYPE_UNSPECIFIED": 0, "OPERATION_TYPE_CREATE": 1, "OPERATION_TYPE_UPDATE": 2, "OPERATION_TYPE_DELETE": 3, } ) func (x OperationType) Enum() *OperationType { p := new(OperationType) *p = x return p } func (x OperationType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (OperationType) Descriptor() protoreflect.EnumDescriptor { return file_apis_changelogs_proto_v1alpha1_changelog_proto_enumTypes[0].Descriptor() } func (OperationType) Type() protoreflect.EnumType { return &file_apis_changelogs_proto_v1alpha1_changelog_proto_enumTypes[0] } func (x OperationType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use OperationType.Descriptor instead. func (OperationType) EnumDescriptor() ([]byte, []int) { return file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP(), []int{0} } // SendChangeLogRequest represents a request to send a single change log entry. type SendChangeLogRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The change log entry to send as part of this request. Entry *ChangeLogEntry `protobuf:"bytes,1,opt,name=entry,proto3" json:"entry,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SendChangeLogRequest) Reset() { *x = SendChangeLogRequest{} mi := &file_apis_changelogs_proto_v1alpha1_changelog_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SendChangeLogRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SendChangeLogRequest) ProtoMessage() {} func (x *SendChangeLogRequest) ProtoReflect() protoreflect.Message { mi := &file_apis_changelogs_proto_v1alpha1_changelog_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SendChangeLogRequest.ProtoReflect.Descriptor instead. func (*SendChangeLogRequest) Descriptor() ([]byte, []int) { return file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP(), []int{0} } func (x *SendChangeLogRequest) GetEntry() *ChangeLogEntry { if x != nil { return x.Entry } return nil } // ChangeLogEntry represents a single change log entry, with detailed information // about the resource that was changed. type ChangeLogEntry struct { state protoimpl.MessageState `protogen:"open.v1"` // The timestamp at which the change occurred. Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // The name and version of the provider that is making the change to the // resource. Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` // The API version of the resource that was changed, e.g. Group/Version. ApiVersion string `protobuf:"bytes,3,opt,name=api_version,json=apiVersion,proto3" json:"api_version,omitempty"` // The kind of the resource that was changed. Kind string `protobuf:"bytes,4,opt,name=kind,proto3" json:"kind,omitempty"` // The name of the resource that was changed. Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` // The external name of the resource that was changed. ExternalName string `protobuf:"bytes,6,opt,name=external_name,json=externalName,proto3" json:"external_name,omitempty"` // The type of operation that was performed on the resource, e.g. Create, // Update, or Delete. Operation OperationType `protobuf:"varint,7,opt,name=operation,proto3,enum=changelogs.proto.v1alpha1.OperationType" json:"operation,omitempty"` // A full snapshot of the resource's state, as observed directly before the // resource was changed. Snapshot *structpb.Struct `protobuf:"bytes,8,opt,name=snapshot,proto3" json:"snapshot,omitempty"` // An optional error message that describes any error encountered while // performing the operation on the resource. ErrorMessage *string `protobuf:"bytes,9,opt,name=error_message,json=errorMessage,proto3,oneof" json:"error_message,omitempty"` // An optional additional details that can be provided for further context // about the change. AdditionalDetails map[string]string `protobuf:"bytes,10,rep,name=additional_details,json=additionalDetails,proto3" json:"additional_details,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ChangeLogEntry) Reset() { *x = ChangeLogEntry{} mi := &file_apis_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ChangeLogEntry) String() string { return protoimpl.X.MessageStringOf(x) } func (*ChangeLogEntry) ProtoMessage() {} func (x *ChangeLogEntry) ProtoReflect() protoreflect.Message { mi := &file_apis_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ChangeLogEntry.ProtoReflect.Descriptor instead. func (*ChangeLogEntry) Descriptor() ([]byte, []int) { return file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP(), []int{1} } func (x *ChangeLogEntry) GetTimestamp() *timestamppb.Timestamp { if x != nil { return x.Timestamp } return nil } func (x *ChangeLogEntry) GetProvider() string { if x != nil { return x.Provider } return "" } func (x *ChangeLogEntry) GetApiVersion() string { if x != nil { return x.ApiVersion } return "" } func (x *ChangeLogEntry) GetKind() string { if x != nil { return x.Kind } return "" } func (x *ChangeLogEntry) GetName() string { if x != nil { return x.Name } return "" } func (x *ChangeLogEntry) GetExternalName() string { if x != nil { return x.ExternalName } return "" } func (x *ChangeLogEntry) GetOperation() OperationType { if x != nil { return x.Operation } return OperationType_OPERATION_TYPE_UNSPECIFIED } func (x *ChangeLogEntry) GetSnapshot() *structpb.Struct { if x != nil { return x.Snapshot } return nil } func (x *ChangeLogEntry) GetErrorMessage() string { if x != nil && x.ErrorMessage != nil { return *x.ErrorMessage } return "" } func (x *ChangeLogEntry) GetAdditionalDetails() map[string]string { if x != nil { return x.AdditionalDetails } return nil } // SendChangeLogResponse is the response returned by the ChangeLogService after // a change log entry is sent. Currently, this is an empty message as the only // useful information expected to sent back at this time will be through errors. type SendChangeLogResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SendChangeLogResponse) Reset() { *x = SendChangeLogResponse{} mi := &file_apis_changelogs_proto_v1alpha1_changelog_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SendChangeLogResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*SendChangeLogResponse) ProtoMessage() {} func (x *SendChangeLogResponse) ProtoReflect() protoreflect.Message { mi := &file_apis_changelogs_proto_v1alpha1_changelog_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SendChangeLogResponse.ProtoReflect.Descriptor instead. func (*SendChangeLogResponse) Descriptor() ([]byte, []int) { return file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP(), []int{2} } var File_apis_changelogs_proto_v1alpha1_changelog_proto protoreflect.FileDescriptor const file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDesc = "" + "\n" + ".apis/changelogs/proto/v1alpha1/changelog.proto\x12\x19changelogs.proto.v1alpha1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"W\n" + "\x14SendChangeLogRequest\x12?\n" + "\x05entry\x18\x01 \x01(\v2).changelogs.proto.v1alpha1.ChangeLogEntryR\x05entry\"\xc4\x04\n" + "\x0eChangeLogEntry\x128\n" + "\ttimestamp\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12\x1a\n" + "\bprovider\x18\x02 \x01(\tR\bprovider\x12\x1f\n" + "\vapi_version\x18\x03 \x01(\tR\n" + "apiVersion\x12\x12\n" + "\x04kind\x18\x04 \x01(\tR\x04kind\x12\x12\n" + "\x04name\x18\x05 \x01(\tR\x04name\x12#\n" + "\rexternal_name\x18\x06 \x01(\tR\fexternalName\x12F\n" + "\toperation\x18\a \x01(\x0e2(.changelogs.proto.v1alpha1.OperationTypeR\toperation\x123\n" + "\bsnapshot\x18\b \x01(\v2\x17.google.protobuf.StructR\bsnapshot\x12(\n" + "\rerror_message\x18\t \x01(\tH\x00R\ferrorMessage\x88\x01\x01\x12o\n" + "\x12additional_details\x18\n" + " \x03(\v2@.changelogs.proto.v1alpha1.ChangeLogEntry.AdditionalDetailsEntryR\x11additionalDetails\x1aD\n" + "\x16AdditionalDetailsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x10\n" + "\x0e_error_message\"\x17\n" + "\x15SendChangeLogResponse*\x80\x01\n" + "\rOperationType\x12\x1e\n" + "\x1aOPERATION_TYPE_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15OPERATION_TYPE_CREATE\x10\x01\x12\x19\n" + "\x15OPERATION_TYPE_UPDATE\x10\x02\x12\x19\n" + "\x15OPERATION_TYPE_DELETE\x10\x032\x88\x01\n" + "\x10ChangeLogService\x12t\n" + "\rSendChangeLog\x12/.changelogs.proto.v1alpha1.SendChangeLogRequest\x1a0.changelogs.proto.v1alpha1.SendChangeLogResponse\"\x00BLZJgithub.com/crossplane/crossplane-runtime/v2/apis/changelogs/proto/v1alpha1b\x06proto3" var ( file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescOnce sync.Once file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescData []byte ) func file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP() []byte { file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescOnce.Do(func() { file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDesc), len(file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDesc))) }) return file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDescData } var file_apis_changelogs_proto_v1alpha1_changelog_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_apis_changelogs_proto_v1alpha1_changelog_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_apis_changelogs_proto_v1alpha1_changelog_proto_goTypes = []any{ (OperationType)(0), // 0: changelogs.proto.v1alpha1.OperationType (*SendChangeLogRequest)(nil), // 1: changelogs.proto.v1alpha1.SendChangeLogRequest (*ChangeLogEntry)(nil), // 2: changelogs.proto.v1alpha1.ChangeLogEntry (*SendChangeLogResponse)(nil), // 3: changelogs.proto.v1alpha1.SendChangeLogResponse nil, // 4: changelogs.proto.v1alpha1.ChangeLogEntry.AdditionalDetailsEntry (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp (*structpb.Struct)(nil), // 6: google.protobuf.Struct } var file_apis_changelogs_proto_v1alpha1_changelog_proto_depIdxs = []int32{ 2, // 0: changelogs.proto.v1alpha1.SendChangeLogRequest.entry:type_name -> changelogs.proto.v1alpha1.ChangeLogEntry 5, // 1: changelogs.proto.v1alpha1.ChangeLogEntry.timestamp:type_name -> google.protobuf.Timestamp 0, // 2: changelogs.proto.v1alpha1.ChangeLogEntry.operation:type_name -> changelogs.proto.v1alpha1.OperationType 6, // 3: changelogs.proto.v1alpha1.ChangeLogEntry.snapshot:type_name -> google.protobuf.Struct 4, // 4: changelogs.proto.v1alpha1.ChangeLogEntry.additional_details:type_name -> changelogs.proto.v1alpha1.ChangeLogEntry.AdditionalDetailsEntry 1, // 5: changelogs.proto.v1alpha1.ChangeLogService.SendChangeLog:input_type -> changelogs.proto.v1alpha1.SendChangeLogRequest 3, // 6: changelogs.proto.v1alpha1.ChangeLogService.SendChangeLog:output_type -> changelogs.proto.v1alpha1.SendChangeLogResponse 6, // [6:7] is the sub-list for method output_type 5, // [5:6] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name } func init() { file_apis_changelogs_proto_v1alpha1_changelog_proto_init() } func file_apis_changelogs_proto_v1alpha1_changelog_proto_init() { if File_apis_changelogs_proto_v1alpha1_changelog_proto != nil { return } file_apis_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDesc), len(file_apis_changelogs_proto_v1alpha1_changelog_proto_rawDesc)), NumEnums: 1, NumMessages: 4, NumExtensions: 0, NumServices: 1, }, GoTypes: file_apis_changelogs_proto_v1alpha1_changelog_proto_goTypes, DependencyIndexes: file_apis_changelogs_proto_v1alpha1_changelog_proto_depIdxs, EnumInfos: file_apis_changelogs_proto_v1alpha1_changelog_proto_enumTypes, MessageInfos: file_apis_changelogs_proto_v1alpha1_changelog_proto_msgTypes, }.Build() File_apis_changelogs_proto_v1alpha1_changelog_proto = out.File file_apis_changelogs_proto_v1alpha1_changelog_proto_goTypes = nil file_apis_changelogs_proto_v1alpha1_changelog_proto_depIdxs = nil } ================================================ FILE: apis/changelogs/proto/v1alpha1/changelog.proto ================================================ /* Copyright 2024 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ syntax = "proto3"; // buf:lint:ignore PACKAGE_DIRECTORY_MATCH package changelogs.proto.v1alpha1; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; option go_package = "github.com/crossplane/crossplane-runtime/v2/apis/changelogs/proto/v1alpha1"; // ChangeLogService is a service that provides the ability to send change log // entries. service ChangeLogService { // SendChangeLog sends a change log entry to the change log service. rpc SendChangeLog(SendChangeLogRequest) returns (SendChangeLogResponse) {} } // SendChangeLogRequest represents a request to send a single change log entry. message SendChangeLogRequest { // The change log entry to send as part of this request. ChangeLogEntry entry = 1; } // ChangeLogEntry represents a single change log entry, with detailed information // about the resource that was changed. message ChangeLogEntry { // The timestamp at which the change occurred. google.protobuf.Timestamp timestamp = 1; // The name and version of the provider that is making the change to the // resource. string provider = 2; // The API version of the resource that was changed, e.g. Group/Version. string api_version = 3; // The kind of the resource that was changed. string kind = 4; // The name of the resource that was changed. string name = 5; // The external name of the resource that was changed. string external_name = 6; // The type of operation that was performed on the resource, e.g. Create, // Update, or Delete. OperationType operation = 7; // A full snapshot of the resource's state, as observed directly before the // resource was changed. google.protobuf.Struct snapshot = 8; // An optional error message that describes any error encountered while // performing the operation on the resource. optional string error_message = 9; // An optional additional details that can be provided for further context // about the change. map additional_details = 10; } // OperationType represents the type of operation that was performed on a // resource. enum OperationType { OPERATION_TYPE_UNSPECIFIED = 0; OPERATION_TYPE_CREATE = 1; OPERATION_TYPE_UPDATE = 2; OPERATION_TYPE_DELETE = 3; } // SendChangeLogResponse is the response returned by the ChangeLogService after // a change log entry is sent. Currently, this is an empty message as the only // useful information expected to sent back at this time will be through errors. message SendChangeLogResponse {} ================================================ FILE: apis/changelogs/proto/v1alpha1/changelog_grpc.pb.go ================================================ // //Copyright 2024 The Crossplane Authors. //Licensed under the Apache License, Version 2.0 (the "License"); //you may not use this file except in compliance with the License. //You may obtain a copy of the License at //http://www.apache.org/licenses/LICENSE-2.0 //Unless required by applicable law or agreed to in writing, software //distributed under the License is distributed on an "AS IS" BASIS, //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //See the License for the specific language governing permissions and //limitations under the License. // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 // - protoc (unknown) // source: apis/changelogs/proto/v1alpha1/changelog.proto // buf:lint:ignore PACKAGE_DIRECTORY_MATCH package v1alpha1 import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.64.0 or later. const _ = grpc.SupportPackageIsVersion9 const ( ChangeLogService_SendChangeLog_FullMethodName = "/changelogs.proto.v1alpha1.ChangeLogService/SendChangeLog" ) // ChangeLogServiceClient is the client API for ChangeLogService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // // ChangeLogService is a service that provides the ability to send change log // entries. type ChangeLogServiceClient interface { // SendChangeLog sends a change log entry to the change log service. SendChangeLog(ctx context.Context, in *SendChangeLogRequest, opts ...grpc.CallOption) (*SendChangeLogResponse, error) } type changeLogServiceClient struct { cc grpc.ClientConnInterface } func NewChangeLogServiceClient(cc grpc.ClientConnInterface) ChangeLogServiceClient { return &changeLogServiceClient{cc} } func (c *changeLogServiceClient) SendChangeLog(ctx context.Context, in *SendChangeLogRequest, opts ...grpc.CallOption) (*SendChangeLogResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SendChangeLogResponse) err := c.cc.Invoke(ctx, ChangeLogService_SendChangeLog_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } // ChangeLogServiceServer is the server API for ChangeLogService service. // All implementations must embed UnimplementedChangeLogServiceServer // for forward compatibility. // // ChangeLogService is a service that provides the ability to send change log // entries. type ChangeLogServiceServer interface { // SendChangeLog sends a change log entry to the change log service. SendChangeLog(context.Context, *SendChangeLogRequest) (*SendChangeLogResponse, error) mustEmbedUnimplementedChangeLogServiceServer() } // UnimplementedChangeLogServiceServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. type UnimplementedChangeLogServiceServer struct{} func (UnimplementedChangeLogServiceServer) SendChangeLog(context.Context, *SendChangeLogRequest) (*SendChangeLogResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SendChangeLog not implemented") } func (UnimplementedChangeLogServiceServer) mustEmbedUnimplementedChangeLogServiceServer() {} func (UnimplementedChangeLogServiceServer) testEmbeddedByValue() {} // UnsafeChangeLogServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ChangeLogServiceServer will // result in compilation errors. type UnsafeChangeLogServiceServer interface { mustEmbedUnimplementedChangeLogServiceServer() } func RegisterChangeLogServiceServer(s grpc.ServiceRegistrar, srv ChangeLogServiceServer) { // If the following call pancis, it indicates UnimplementedChangeLogServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } s.RegisterService(&ChangeLogService_ServiceDesc, srv) } func _ChangeLogService_SendChangeLog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SendChangeLogRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ChangeLogServiceServer).SendChangeLog(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: ChangeLogService_SendChangeLog_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ChangeLogServiceServer).SendChangeLog(ctx, req.(*SendChangeLogRequest)) } return interceptor(ctx, in, info, handler) } // ChangeLogService_ServiceDesc is the grpc.ServiceDesc for ChangeLogService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var ChangeLogService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "changelogs.proto.v1alpha1.ChangeLogService", HandlerType: (*ChangeLogServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "SendChangeLog", Handler: _ChangeLogService_SendChangeLog_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "apis/changelogs/proto/v1alpha1/changelog.proto", } ================================================ FILE: apis/pipelineinspector/proto/v1alpha1/pipeline_inspector.pb.go ================================================ // //Copyright 2026 The Crossplane Authors. // //Licensed under the Apache License, Version 2.0 (the "License"); //you may not use this file except in compliance with the License. //You may obtain a copy of the License at // //http://www.apache.org/licenses/LICENSE-2.0 // //Unless required by applicable law or agreed to in writing, software //distributed under the License is distributed on an "AS IS" BASIS, //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //See the License for the specific language governing permissions and //limitations under the License. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc (unknown) // source: apis/pipelineinspector/proto/v1alpha1/pipeline_inspector.proto //buf:lint:ignore PACKAGE_DIRECTORY_MATCH package v1alpha1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // EmitRequestRequest wraps the function request with correlation metadata. type EmitRequestRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The original function request as JSON bytes (with credentials stripped for security). // This allows consumers to parse the request without needing the proto schema. Request []byte `protobuf:"bytes,1,opt,name=request,proto3" json:"request,omitempty"` // Metadata for correlation and identification. Meta *StepMeta `protobuf:"bytes,2,opt,name=meta,proto3" json:"meta,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *EmitRequestRequest) Reset() { *x = EmitRequestRequest{} mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *EmitRequestRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*EmitRequestRequest) ProtoMessage() {} func (x *EmitRequestRequest) ProtoReflect() protoreflect.Message { mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EmitRequestRequest.ProtoReflect.Descriptor instead. func (*EmitRequestRequest) Descriptor() ([]byte, []int) { return file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescGZIP(), []int{0} } func (x *EmitRequestRequest) GetRequest() []byte { if x != nil { return x.Request } return nil } func (x *EmitRequestRequest) GetMeta() *StepMeta { if x != nil { return x.Meta } return nil } // EmitRequestResponse is empty - this is a fire-and-forget call. type EmitRequestResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *EmitRequestResponse) Reset() { *x = EmitRequestResponse{} mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *EmitRequestResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*EmitRequestResponse) ProtoMessage() {} func (x *EmitRequestResponse) ProtoReflect() protoreflect.Message { mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EmitRequestResponse.ProtoReflect.Descriptor instead. func (*EmitRequestResponse) Descriptor() ([]byte, []int) { return file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescGZIP(), []int{1} } // EmitResponseRequest wraps the function response with correlation metadata. type EmitResponseRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The function response as JSON bytes, empty if there was an error. // This allows consumers to parse the response without needing the proto schema. Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` // Error message if the function call failed. Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // Metadata for correlation and identification. // Must match the meta from the corresponding EmitRequest. Meta *StepMeta `protobuf:"bytes,3,opt,name=meta,proto3" json:"meta,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *EmitResponseRequest) Reset() { *x = EmitResponseRequest{} mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *EmitResponseRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*EmitResponseRequest) ProtoMessage() {} func (x *EmitResponseRequest) ProtoReflect() protoreflect.Message { mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EmitResponseRequest.ProtoReflect.Descriptor instead. func (*EmitResponseRequest) Descriptor() ([]byte, []int) { return file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescGZIP(), []int{2} } func (x *EmitResponseRequest) GetResponse() []byte { if x != nil { return x.Response } return nil } func (x *EmitResponseRequest) GetError() string { if x != nil { return x.Error } return "" } func (x *EmitResponseRequest) GetMeta() *StepMeta { if x != nil { return x.Meta } return nil } // EmitResponseResponse is empty - this is a fire-and-forget call. type EmitResponseResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *EmitResponseResponse) Reset() { *x = EmitResponseResponse{} mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *EmitResponseResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*EmitResponseResponse) ProtoMessage() {} func (x *EmitResponseResponse) ProtoReflect() protoreflect.Message { mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EmitResponseResponse.ProtoReflect.Descriptor instead. func (*EmitResponseResponse) Descriptor() ([]byte, []int) { return file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescGZIP(), []int{3} } // StepMeta contains metadata for correlating and identifying a function // invocation within a pipeline execution. type StepMeta struct { state protoimpl.MessageState `protogen:"open.v1"` // Timestamp when this step was executed. Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // ID identifying the entire pipeline execution (all steps in one reconciliation). // All function invocations within a single reconciliation share the same trace_id. TraceId string `protobuf:"bytes,2,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` // ID identifying this specific function invocation. SpanId string `protobuf:"bytes,3,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` // Zero-based index of this step in the function pipeline. StepIndex int32 `protobuf:"varint,4,opt,name=step_index,json=stepIndex,proto3" json:"step_index,omitempty"` // Name of this step in the function pipeline. StepName string `protobuf:"bytes,5,opt,name=step_name,json=stepName,proto3" json:"step_name,omitempty"` // Per-step counter incremented when a function requests additional resources and // needs to be re-run, starting from 0. Iteration int32 `protobuf:"varint,6,opt,name=iteration,proto3" json:"iteration,omitempty"` // Name of the function being invoked. FunctionName string `protobuf:"bytes,7,opt,name=function_name,json=functionName,proto3" json:"function_name,omitempty"` // Only one of these can be set - identifies the pipeline context. // // Types that are valid to be assigned to Context: // // *StepMeta_OperationMeta // *StepMeta_CompositionMeta Context isStepMeta_Context `protobuf_oneof:"context"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *StepMeta) Reset() { *x = StepMeta{} mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *StepMeta) String() string { return protoimpl.X.MessageStringOf(x) } func (*StepMeta) ProtoMessage() {} func (x *StepMeta) ProtoReflect() protoreflect.Message { mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use StepMeta.ProtoReflect.Descriptor instead. func (*StepMeta) Descriptor() ([]byte, []int) { return file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescGZIP(), []int{4} } func (x *StepMeta) GetTimestamp() *timestamppb.Timestamp { if x != nil { return x.Timestamp } return nil } func (x *StepMeta) GetTraceId() string { if x != nil { return x.TraceId } return "" } func (x *StepMeta) GetSpanId() string { if x != nil { return x.SpanId } return "" } func (x *StepMeta) GetStepIndex() int32 { if x != nil { return x.StepIndex } return 0 } func (x *StepMeta) GetStepName() string { if x != nil { return x.StepName } return "" } func (x *StepMeta) GetIteration() int32 { if x != nil { return x.Iteration } return 0 } func (x *StepMeta) GetFunctionName() string { if x != nil { return x.FunctionName } return "" } func (x *StepMeta) GetContext() isStepMeta_Context { if x != nil { return x.Context } return nil } func (x *StepMeta) GetOperationMeta() *OperationMeta { if x != nil { if x, ok := x.Context.(*StepMeta_OperationMeta); ok { return x.OperationMeta } } return nil } func (x *StepMeta) GetCompositionMeta() *CompositionMeta { if x != nil { if x, ok := x.Context.(*StepMeta_CompositionMeta); ok { return x.CompositionMeta } } return nil } type isStepMeta_Context interface { isStepMeta_Context() } type StepMeta_OperationMeta struct { OperationMeta *OperationMeta `protobuf:"bytes,8,opt,name=operation_meta,json=operationMeta,proto3,oneof"` } type StepMeta_CompositionMeta struct { CompositionMeta *CompositionMeta `protobuf:"bytes,9,opt,name=composition_meta,json=compositionMeta,proto3,oneof"` } func (*StepMeta_OperationMeta) isStepMeta_Context() {} func (*StepMeta_CompositionMeta) isStepMeta_Context() {} // CompositionMeta contains metadata about the Composition and Composite Resource type CompositionMeta struct { state protoimpl.MessageState `protogen:"open.v1"` // Name of the Composition defining this pipeline. CompositionName string `protobuf:"bytes,1,opt,name=composition_name,json=compositionName,proto3" json:"composition_name,omitempty"` // UID of the composite resource being reconciled. CompositeResourceUid string `protobuf:"bytes,2,opt,name=composite_resource_uid,json=compositeResourceUid,proto3" json:"composite_resource_uid,omitempty"` // Name of the composite resource being reconciled. CompositeResourceName string `protobuf:"bytes,3,opt,name=composite_resource_name,json=compositeResourceName,proto3" json:"composite_resource_name,omitempty"` // Namespace of the composite resource (empty for cluster-scoped resources). CompositeResourceNamespace string `protobuf:"bytes,4,opt,name=composite_resource_namespace,json=compositeResourceNamespace,proto3" json:"composite_resource_namespace,omitempty"` // API version of the composite resource (e.g., "example.org/v1"). CompositeResourceApiVersion string `protobuf:"bytes,5,opt,name=composite_resource_api_version,json=compositeResourceApiVersion,proto3" json:"composite_resource_api_version,omitempty"` // Kind of the composite resource (e.g., "XDatabase"). CompositeResourceKind string `protobuf:"bytes,6,opt,name=composite_resource_kind,json=compositeResourceKind,proto3" json:"composite_resource_kind,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CompositionMeta) Reset() { *x = CompositionMeta{} mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CompositionMeta) String() string { return protoimpl.X.MessageStringOf(x) } func (*CompositionMeta) ProtoMessage() {} func (x *CompositionMeta) ProtoReflect() protoreflect.Message { mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CompositionMeta.ProtoReflect.Descriptor instead. func (*CompositionMeta) Descriptor() ([]byte, []int) { return file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescGZIP(), []int{5} } func (x *CompositionMeta) GetCompositionName() string { if x != nil { return x.CompositionName } return "" } func (x *CompositionMeta) GetCompositeResourceUid() string { if x != nil { return x.CompositeResourceUid } return "" } func (x *CompositionMeta) GetCompositeResourceName() string { if x != nil { return x.CompositeResourceName } return "" } func (x *CompositionMeta) GetCompositeResourceNamespace() string { if x != nil { return x.CompositeResourceNamespace } return "" } func (x *CompositionMeta) GetCompositeResourceApiVersion() string { if x != nil { return x.CompositeResourceApiVersion } return "" } func (x *CompositionMeta) GetCompositeResourceKind() string { if x != nil { return x.CompositeResourceKind } return "" } // OperationMeta contains metadata about the Operation being performed. type OperationMeta struct { state protoimpl.MessageState `protogen:"open.v1"` // Name of the Operation. OperationName string `protobuf:"bytes,1,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` // UID of the Operation. OperationUid string `protobuf:"bytes,2,opt,name=operation_uid,json=operationUid,proto3" json:"operation_uid,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *OperationMeta) Reset() { *x = OperationMeta{} mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *OperationMeta) String() string { return protoimpl.X.MessageStringOf(x) } func (*OperationMeta) ProtoMessage() {} func (x *OperationMeta) ProtoReflect() protoreflect.Message { mi := &file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use OperationMeta.ProtoReflect.Descriptor instead. func (*OperationMeta) Descriptor() ([]byte, []int) { return file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescGZIP(), []int{6} } func (x *OperationMeta) GetOperationName() string { if x != nil { return x.OperationName } return "" } func (x *OperationMeta) GetOperationUid() string { if x != nil { return x.OperationUid } return "" } var File_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto protoreflect.FileDescriptor const file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDesc = "" + "\n" + ">apis/pipelineinspector/proto/v1alpha1/pipeline_inspector.proto\x12\x1ccrossplane.pipeline.v1alpha1\x1a\x1fgoogle/protobuf/timestamp.proto\"j\n" + "\x12EmitRequestRequest\x12\x18\n" + "\arequest\x18\x01 \x01(\fR\arequest\x12:\n" + "\x04meta\x18\x02 \x01(\v2&.crossplane.pipeline.v1alpha1.StepMetaR\x04meta\"\x15\n" + "\x13EmitRequestResponse\"\x83\x01\n" + "\x13EmitResponseRequest\x12\x1a\n" + "\bresponse\x18\x01 \x01(\fR\bresponse\x12\x14\n" + "\x05error\x18\x02 \x01(\tR\x05error\x12:\n" + "\x04meta\x18\x03 \x01(\v2&.crossplane.pipeline.v1alpha1.StepMetaR\x04meta\"\x16\n" + "\x14EmitResponseResponse\"\xb4\x03\n" + "\bStepMeta\x128\n" + "\ttimestamp\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12\x19\n" + "\btrace_id\x18\x02 \x01(\tR\atraceId\x12\x17\n" + "\aspan_id\x18\x03 \x01(\tR\x06spanId\x12\x1d\n" + "\n" + "step_index\x18\x04 \x01(\x05R\tstepIndex\x12\x1b\n" + "\tstep_name\x18\x05 \x01(\tR\bstepName\x12\x1c\n" + "\titeration\x18\x06 \x01(\x05R\titeration\x12#\n" + "\rfunction_name\x18\a \x01(\tR\ffunctionName\x12T\n" + "\x0eoperation_meta\x18\b \x01(\v2+.crossplane.pipeline.v1alpha1.OperationMetaH\x00R\roperationMeta\x12Z\n" + "\x10composition_meta\x18\t \x01(\v2-.crossplane.pipeline.v1alpha1.CompositionMetaH\x00R\x0fcompositionMetaB\t\n" + "\acontext\"\xe9\x02\n" + "\x0fCompositionMeta\x12)\n" + "\x10composition_name\x18\x01 \x01(\tR\x0fcompositionName\x124\n" + "\x16composite_resource_uid\x18\x02 \x01(\tR\x14compositeResourceUid\x126\n" + "\x17composite_resource_name\x18\x03 \x01(\tR\x15compositeResourceName\x12@\n" + "\x1ccomposite_resource_namespace\x18\x04 \x01(\tR\x1acompositeResourceNamespace\x12C\n" + "\x1ecomposite_resource_api_version\x18\x05 \x01(\tR\x1bcompositeResourceApiVersion\x126\n" + "\x17composite_resource_kind\x18\x06 \x01(\tR\x15compositeResourceKind\"[\n" + "\rOperationMeta\x12%\n" + "\x0eoperation_name\x18\x01 \x01(\tR\roperationName\x12#\n" + "\roperation_uid\x18\x02 \x01(\tR\foperationUid2\x89\x02\n" + "\x18PipelineInspectorService\x12t\n" + "\vEmitRequest\x120.crossplane.pipeline.v1alpha1.EmitRequestRequest\x1a1.crossplane.pipeline.v1alpha1.EmitRequestResponse\"\x00\x12w\n" + "\fEmitResponse\x121.crossplane.pipeline.v1alpha1.EmitResponseRequest\x1a2.crossplane.pipeline.v1alpha1.EmitResponseResponse\"\x00BSZQgithub.com/crossplane/crossplane-runtime/v2/apis/pipelineinspector/proto/v1alpha1b\x06proto3" var ( file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescOnce sync.Once file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescData []byte ) func file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescGZIP() []byte { file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescOnce.Do(func() { file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDesc), len(file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDesc))) }) return file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDescData } var file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_goTypes = []any{ (*EmitRequestRequest)(nil), // 0: crossplane.pipeline.v1alpha1.EmitRequestRequest (*EmitRequestResponse)(nil), // 1: crossplane.pipeline.v1alpha1.EmitRequestResponse (*EmitResponseRequest)(nil), // 2: crossplane.pipeline.v1alpha1.EmitResponseRequest (*EmitResponseResponse)(nil), // 3: crossplane.pipeline.v1alpha1.EmitResponseResponse (*StepMeta)(nil), // 4: crossplane.pipeline.v1alpha1.StepMeta (*CompositionMeta)(nil), // 5: crossplane.pipeline.v1alpha1.CompositionMeta (*OperationMeta)(nil), // 6: crossplane.pipeline.v1alpha1.OperationMeta (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp } var file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_depIdxs = []int32{ 4, // 0: crossplane.pipeline.v1alpha1.EmitRequestRequest.meta:type_name -> crossplane.pipeline.v1alpha1.StepMeta 4, // 1: crossplane.pipeline.v1alpha1.EmitResponseRequest.meta:type_name -> crossplane.pipeline.v1alpha1.StepMeta 7, // 2: crossplane.pipeline.v1alpha1.StepMeta.timestamp:type_name -> google.protobuf.Timestamp 6, // 3: crossplane.pipeline.v1alpha1.StepMeta.operation_meta:type_name -> crossplane.pipeline.v1alpha1.OperationMeta 5, // 4: crossplane.pipeline.v1alpha1.StepMeta.composition_meta:type_name -> crossplane.pipeline.v1alpha1.CompositionMeta 0, // 5: crossplane.pipeline.v1alpha1.PipelineInspectorService.EmitRequest:input_type -> crossplane.pipeline.v1alpha1.EmitRequestRequest 2, // 6: crossplane.pipeline.v1alpha1.PipelineInspectorService.EmitResponse:input_type -> crossplane.pipeline.v1alpha1.EmitResponseRequest 1, // 7: crossplane.pipeline.v1alpha1.PipelineInspectorService.EmitRequest:output_type -> crossplane.pipeline.v1alpha1.EmitRequestResponse 3, // 8: crossplane.pipeline.v1alpha1.PipelineInspectorService.EmitResponse:output_type -> crossplane.pipeline.v1alpha1.EmitResponseResponse 7, // [7:9] is the sub-list for method output_type 5, // [5:7] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name } func init() { file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_init() } func file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_init() { if File_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto != nil { return } file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes[4].OneofWrappers = []any{ (*StepMeta_OperationMeta)(nil), (*StepMeta_CompositionMeta)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDesc), len(file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_rawDesc)), NumEnums: 0, NumMessages: 7, NumExtensions: 0, NumServices: 1, }, GoTypes: file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_goTypes, DependencyIndexes: file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_depIdxs, MessageInfos: file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_msgTypes, }.Build() File_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto = out.File file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_goTypes = nil file_apis_pipelineinspector_proto_v1alpha1_pipeline_inspector_proto_depIdxs = nil } ================================================ FILE: apis/pipelineinspector/proto/v1alpha1/pipeline_inspector.proto ================================================ /* Copyright 2026 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ syntax = "proto3"; //buf:lint:ignore PACKAGE_DIRECTORY_MATCH package crossplane.pipeline.v1alpha1; import "google/protobuf/timestamp.proto"; option go_package = "github.com/crossplane/crossplane-runtime/v2/apis/pipelineinspector/proto/v1alpha1"; // PipelineInspectorService receives pipeline execution data from Crossplane. // This service is implemented by a sidecar that captures function pipeline // execution data for debugging and observability purposes. service PipelineInspectorService { // EmitRequest receives the function request before execution. // This is a fire-and-forget call; errors do not affect pipeline execution. rpc EmitRequest(EmitRequestRequest) returns (EmitRequestResponse) {} // EmitResponse receives the function response after execution. // This is a fire-and-forget call; errors do not affect pipeline execution. rpc EmitResponse(EmitResponseRequest) returns (EmitResponseResponse) {} } // EmitRequestRequest wraps the function request with correlation metadata. message EmitRequestRequest { // The original function request as JSON bytes (with credentials stripped for security). // This allows consumers to parse the request without needing the proto schema. bytes request = 1; // Metadata for correlation and identification. StepMeta meta = 2; } // EmitRequestResponse is empty - this is a fire-and-forget call. message EmitRequestResponse {} // EmitResponseRequest wraps the function response with correlation metadata. message EmitResponseRequest { // The function response as JSON bytes, empty if there was an error. // This allows consumers to parse the response without needing the proto schema. bytes response = 1; // Error message if the function call failed. string error = 2; // Metadata for correlation and identification. // Must match the meta from the corresponding EmitRequest. StepMeta meta = 3; } // EmitResponseResponse is empty - this is a fire-and-forget call. message EmitResponseResponse {} // StepMeta contains metadata for correlating and identifying a function // invocation within a pipeline execution. message StepMeta { // Timestamp when this step was executed. google.protobuf.Timestamp timestamp = 1; // ID identifying the entire pipeline execution (all steps in one reconciliation). // All function invocations within a single reconciliation share the same trace_id. string trace_id = 2; // ID identifying this specific function invocation. string span_id = 3; // Zero-based index of this step in the function pipeline. int32 step_index = 4; // Name of this step in the function pipeline. string step_name = 5; // Per-step counter incremented when a function requests additional resources and // needs to be re-run, starting from 0. int32 iteration = 6; // Name of the function being invoked. string function_name = 7; // Only one of these can be set - identifies the pipeline context. oneof context { OperationMeta operation_meta = 8; CompositionMeta composition_meta = 9; } } // CompositionMeta contains metadata about the Composition and Composite Resource message CompositionMeta { // Name of the Composition defining this pipeline. string composition_name = 1; // UID of the composite resource being reconciled. string composite_resource_uid = 2; // Name of the composite resource being reconciled. string composite_resource_name = 3; // Namespace of the composite resource (empty for cluster-scoped resources). string composite_resource_namespace = 4; // API version of the composite resource (e.g., "example.org/v1"). string composite_resource_api_version = 5; // Kind of the composite resource (e.g., "XDatabase"). string composite_resource_kind = 6; } // OperationMeta contains metadata about the Operation being performed. message OperationMeta { // Name of the Operation. string operation_name = 1; // UID of the Operation. string operation_uid = 2; } ================================================ FILE: apis/pipelineinspector/proto/v1alpha1/pipeline_inspector_grpc.pb.go ================================================ // //Copyright 2026 The Crossplane Authors. // //Licensed under the Apache License, Version 2.0 (the "License"); //you may not use this file except in compliance with the License. //You may obtain a copy of the License at // //http://www.apache.org/licenses/LICENSE-2.0 // //Unless required by applicable law or agreed to in writing, software //distributed under the License is distributed on an "AS IS" BASIS, //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //See the License for the specific language governing permissions and //limitations under the License. // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 // - protoc (unknown) // source: apis/pipelineinspector/proto/v1alpha1/pipeline_inspector.proto //buf:lint:ignore PACKAGE_DIRECTORY_MATCH package v1alpha1 import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.64.0 or later. const _ = grpc.SupportPackageIsVersion9 const ( PipelineInspectorService_EmitRequest_FullMethodName = "/crossplane.pipeline.v1alpha1.PipelineInspectorService/EmitRequest" PipelineInspectorService_EmitResponse_FullMethodName = "/crossplane.pipeline.v1alpha1.PipelineInspectorService/EmitResponse" ) // PipelineInspectorServiceClient is the client API for PipelineInspectorService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // // PipelineInspectorService receives pipeline execution data from Crossplane. // This service is implemented by a sidecar that captures function pipeline // execution data for debugging and observability purposes. type PipelineInspectorServiceClient interface { // EmitRequest receives the function request before execution. // This is a fire-and-forget call; errors do not affect pipeline execution. EmitRequest(ctx context.Context, in *EmitRequestRequest, opts ...grpc.CallOption) (*EmitRequestResponse, error) // EmitResponse receives the function response after execution. // This is a fire-and-forget call; errors do not affect pipeline execution. EmitResponse(ctx context.Context, in *EmitResponseRequest, opts ...grpc.CallOption) (*EmitResponseResponse, error) } type pipelineInspectorServiceClient struct { cc grpc.ClientConnInterface } func NewPipelineInspectorServiceClient(cc grpc.ClientConnInterface) PipelineInspectorServiceClient { return &pipelineInspectorServiceClient{cc} } func (c *pipelineInspectorServiceClient) EmitRequest(ctx context.Context, in *EmitRequestRequest, opts ...grpc.CallOption) (*EmitRequestResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(EmitRequestResponse) err := c.cc.Invoke(ctx, PipelineInspectorService_EmitRequest_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *pipelineInspectorServiceClient) EmitResponse(ctx context.Context, in *EmitResponseRequest, opts ...grpc.CallOption) (*EmitResponseResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(EmitResponseResponse) err := c.cc.Invoke(ctx, PipelineInspectorService_EmitResponse_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } // PipelineInspectorServiceServer is the server API for PipelineInspectorService service. // All implementations must embed UnimplementedPipelineInspectorServiceServer // for forward compatibility. // // PipelineInspectorService receives pipeline execution data from Crossplane. // This service is implemented by a sidecar that captures function pipeline // execution data for debugging and observability purposes. type PipelineInspectorServiceServer interface { // EmitRequest receives the function request before execution. // This is a fire-and-forget call; errors do not affect pipeline execution. EmitRequest(context.Context, *EmitRequestRequest) (*EmitRequestResponse, error) // EmitResponse receives the function response after execution. // This is a fire-and-forget call; errors do not affect pipeline execution. EmitResponse(context.Context, *EmitResponseRequest) (*EmitResponseResponse, error) mustEmbedUnimplementedPipelineInspectorServiceServer() } // UnimplementedPipelineInspectorServiceServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. type UnimplementedPipelineInspectorServiceServer struct{} func (UnimplementedPipelineInspectorServiceServer) EmitRequest(context.Context, *EmitRequestRequest) (*EmitRequestResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method EmitRequest not implemented") } func (UnimplementedPipelineInspectorServiceServer) EmitResponse(context.Context, *EmitResponseRequest) (*EmitResponseResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method EmitResponse not implemented") } func (UnimplementedPipelineInspectorServiceServer) mustEmbedUnimplementedPipelineInspectorServiceServer() { } func (UnimplementedPipelineInspectorServiceServer) testEmbeddedByValue() {} // UnsafePipelineInspectorServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to PipelineInspectorServiceServer will // result in compilation errors. type UnsafePipelineInspectorServiceServer interface { mustEmbedUnimplementedPipelineInspectorServiceServer() } func RegisterPipelineInspectorServiceServer(s grpc.ServiceRegistrar, srv PipelineInspectorServiceServer) { // If the following call pancis, it indicates UnimplementedPipelineInspectorServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } s.RegisterService(&PipelineInspectorService_ServiceDesc, srv) } func _PipelineInspectorService_EmitRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(EmitRequestRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(PipelineInspectorServiceServer).EmitRequest(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: PipelineInspectorService_EmitRequest_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(PipelineInspectorServiceServer).EmitRequest(ctx, req.(*EmitRequestRequest)) } return interceptor(ctx, in, info, handler) } func _PipelineInspectorService_EmitResponse_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(EmitResponseRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(PipelineInspectorServiceServer).EmitResponse(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: PipelineInspectorService_EmitResponse_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(PipelineInspectorServiceServer).EmitResponse(ctx, req.(*EmitResponseRequest)) } return interceptor(ctx, in, info, handler) } // PipelineInspectorService_ServiceDesc is the grpc.ServiceDesc for PipelineInspectorService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var PipelineInspectorService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "crossplane.pipeline.v1alpha1.PipelineInspectorService", HandlerType: (*PipelineInspectorServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "EmitRequest", Handler: _PipelineInspectorService_EmitRequest_Handler, }, { MethodName: "EmitResponse", Handler: _PipelineInspectorService_EmitResponse_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "apis/pipelineinspector/proto/v1alpha1/pipeline_inspector.proto", } ================================================ FILE: apis/proto/v1alpha1/ess.pb.go ================================================ // //Copyright 2023 The Crossplane Authors. //Licensed under the Apache License, Version 2.0 (the "License"); //you may not use this file except in compliance with the License. //You may obtain a copy of the License at //http://www.apache.org/licenses/LICENSE-2.0 //Unless required by applicable law or agreed to in writing, software //distributed under the License is distributed on an "AS IS" BASIS, //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //See the License for the specific language governing permissions and //limitations under the License. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc (unknown) // source: apis/proto/v1alpha1/ess.proto // buf:lint:ignore PACKAGE_DIRECTORY_MATCH package v1alpha1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // ConfigReference is used to refer a StoreConfig object. type ConfigReference struct { state protoimpl.MessageState `protogen:"open.v1"` ApiVersion string `protobuf:"bytes,1,opt,name=api_version,json=apiVersion,proto3" json:"api_version,omitempty"` Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ConfigReference) Reset() { *x = ConfigReference{} mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ConfigReference) String() string { return protoimpl.X.MessageStringOf(x) } func (*ConfigReference) ProtoMessage() {} func (x *ConfigReference) ProtoReflect() protoreflect.Message { mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ConfigReference.ProtoReflect.Descriptor instead. func (*ConfigReference) Descriptor() ([]byte, []int) { return file_apis_proto_v1alpha1_ess_proto_rawDescGZIP(), []int{0} } func (x *ConfigReference) GetApiVersion() string { if x != nil { return x.ApiVersion } return "" } func (x *ConfigReference) GetKind() string { if x != nil { return x.Kind } return "" } func (x *ConfigReference) GetName() string { if x != nil { return x.Name } return "" } // Secret defines the structure of a secret. type Secret struct { state protoimpl.MessageState `protogen:"open.v1"` ScopedName string `protobuf:"bytes,1,opt,name=scoped_name,json=scopedName,proto3" json:"scoped_name,omitempty"` Metadata map[string]string `protobuf:"bytes,2,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` Data map[string][]byte `protobuf:"bytes,3,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Secret) Reset() { *x = Secret{} mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Secret) String() string { return protoimpl.X.MessageStringOf(x) } func (*Secret) ProtoMessage() {} func (x *Secret) ProtoReflect() protoreflect.Message { mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Secret.ProtoReflect.Descriptor instead. func (*Secret) Descriptor() ([]byte, []int) { return file_apis_proto_v1alpha1_ess_proto_rawDescGZIP(), []int{1} } func (x *Secret) GetScopedName() string { if x != nil { return x.ScopedName } return "" } func (x *Secret) GetMetadata() map[string]string { if x != nil { return x.Metadata } return nil } func (x *Secret) GetData() map[string][]byte { if x != nil { return x.Data } return nil } // GetSecretRequest requests secret from the secret store. type GetSecretRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Config *ConfigReference `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` Secret *Secret `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetSecretRequest) Reset() { *x = GetSecretRequest{} mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GetSecretRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetSecretRequest) ProtoMessage() {} func (x *GetSecretRequest) ProtoReflect() protoreflect.Message { mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetSecretRequest.ProtoReflect.Descriptor instead. func (*GetSecretRequest) Descriptor() ([]byte, []int) { return file_apis_proto_v1alpha1_ess_proto_rawDescGZIP(), []int{2} } func (x *GetSecretRequest) GetConfig() *ConfigReference { if x != nil { return x.Config } return nil } func (x *GetSecretRequest) GetSecret() *Secret { if x != nil { return x.Secret } return nil } // GetSecretResponse returns the secret from the secret store. type GetSecretResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Secret *Secret `protobuf:"bytes,1,opt,name=secret,proto3" json:"secret,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetSecretResponse) Reset() { *x = GetSecretResponse{} mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GetSecretResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetSecretResponse) ProtoMessage() {} func (x *GetSecretResponse) ProtoReflect() protoreflect.Message { mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetSecretResponse.ProtoReflect.Descriptor instead. func (*GetSecretResponse) Descriptor() ([]byte, []int) { return file_apis_proto_v1alpha1_ess_proto_rawDescGZIP(), []int{3} } func (x *GetSecretResponse) GetSecret() *Secret { if x != nil { return x.Secret } return nil } // ApplySecretRequest applies the secret data update to the secret store. type ApplySecretRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Config *ConfigReference `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` Secret *Secret `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ApplySecretRequest) Reset() { *x = ApplySecretRequest{} mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ApplySecretRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ApplySecretRequest) ProtoMessage() {} func (x *ApplySecretRequest) ProtoReflect() protoreflect.Message { mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ApplySecretRequest.ProtoReflect.Descriptor instead. func (*ApplySecretRequest) Descriptor() ([]byte, []int) { return file_apis_proto_v1alpha1_ess_proto_rawDescGZIP(), []int{4} } func (x *ApplySecretRequest) GetConfig() *ConfigReference { if x != nil { return x.Config } return nil } func (x *ApplySecretRequest) GetSecret() *Secret { if x != nil { return x.Secret } return nil } // ApplySecretResponse returns if the secret is changed or not. type ApplySecretResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Changed bool `protobuf:"varint,1,opt,name=changed,proto3" json:"changed,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ApplySecretResponse) Reset() { *x = ApplySecretResponse{} mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ApplySecretResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ApplySecretResponse) ProtoMessage() {} func (x *ApplySecretResponse) ProtoReflect() protoreflect.Message { mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ApplySecretResponse.ProtoReflect.Descriptor instead. func (*ApplySecretResponse) Descriptor() ([]byte, []int) { return file_apis_proto_v1alpha1_ess_proto_rawDescGZIP(), []int{5} } func (x *ApplySecretResponse) GetChanged() bool { if x != nil { return x.Changed } return false } // DeleteKeysRequest deletes the secret from the secret store. type DeleteKeysRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Config *ConfigReference `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` Secret *Secret `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeleteKeysRequest) Reset() { *x = DeleteKeysRequest{} mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DeleteKeysRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteKeysRequest) ProtoMessage() {} func (x *DeleteKeysRequest) ProtoReflect() protoreflect.Message { mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteKeysRequest.ProtoReflect.Descriptor instead. func (*DeleteKeysRequest) Descriptor() ([]byte, []int) { return file_apis_proto_v1alpha1_ess_proto_rawDescGZIP(), []int{6} } func (x *DeleteKeysRequest) GetConfig() *ConfigReference { if x != nil { return x.Config } return nil } func (x *DeleteKeysRequest) GetSecret() *Secret { if x != nil { return x.Secret } return nil } // DeleteKeysResponse is returned if the secret is deleted. type DeleteKeysResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeleteKeysResponse) Reset() { *x = DeleteKeysResponse{} mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DeleteKeysResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteKeysResponse) ProtoMessage() {} func (x *DeleteKeysResponse) ProtoReflect() protoreflect.Message { mi := &file_apis_proto_v1alpha1_ess_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteKeysResponse.ProtoReflect.Descriptor instead. func (*DeleteKeysResponse) Descriptor() ([]byte, []int) { return file_apis_proto_v1alpha1_ess_proto_rawDescGZIP(), []int{7} } var File_apis_proto_v1alpha1_ess_proto protoreflect.FileDescriptor const file_apis_proto_v1alpha1_ess_proto_rawDesc = "" + "\n" + "\x1dapis/proto/v1alpha1/ess.proto\x12\x12ess.proto.v1alpha1\"Z\n" + "\x0fConfigReference\x12\x1f\n" + "\vapi_version\x18\x01 \x01(\tR\n" + "apiVersion\x12\x12\n" + "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x12\n" + "\x04name\x18\x03 \x01(\tR\x04name\"\x9f\x02\n" + "\x06Secret\x12\x1f\n" + "\vscoped_name\x18\x01 \x01(\tR\n" + "scopedName\x12D\n" + "\bmetadata\x18\x02 \x03(\v2(.ess.proto.v1alpha1.Secret.MetadataEntryR\bmetadata\x128\n" + "\x04data\x18\x03 \x03(\v2$.ess.proto.v1alpha1.Secret.DataEntryR\x04data\x1a;\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a7\n" + "\tDataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"\x83\x01\n" + "\x10GetSecretRequest\x12;\n" + "\x06config\x18\x01 \x01(\v2#.ess.proto.v1alpha1.ConfigReferenceR\x06config\x122\n" + "\x06secret\x18\x02 \x01(\v2\x1a.ess.proto.v1alpha1.SecretR\x06secret\"G\n" + "\x11GetSecretResponse\x122\n" + "\x06secret\x18\x01 \x01(\v2\x1a.ess.proto.v1alpha1.SecretR\x06secret\"\x85\x01\n" + "\x12ApplySecretRequest\x12;\n" + "\x06config\x18\x01 \x01(\v2#.ess.proto.v1alpha1.ConfigReferenceR\x06config\x122\n" + "\x06secret\x18\x02 \x01(\v2\x1a.ess.proto.v1alpha1.SecretR\x06secret\"/\n" + "\x13ApplySecretResponse\x12\x18\n" + "\achanged\x18\x01 \x01(\bR\achanged\"\x84\x01\n" + "\x11DeleteKeysRequest\x12;\n" + "\x06config\x18\x01 \x01(\v2#.ess.proto.v1alpha1.ConfigReferenceR\x06config\x122\n" + "\x06secret\x18\x02 \x01(\v2\x1a.ess.proto.v1alpha1.SecretR\x06secret\"\x14\n" + "\x12DeleteKeysResponse2\xbf\x02\n" + " ExternalSecretStorePluginService\x12Z\n" + "\tGetSecret\x12$.ess.proto.v1alpha1.GetSecretRequest\x1a%.ess.proto.v1alpha1.GetSecretResponse\"\x00\x12`\n" + "\vApplySecret\x12&.ess.proto.v1alpha1.ApplySecretRequest\x1a'.ess.proto.v1alpha1.ApplySecretResponse\"\x00\x12]\n" + "\n" + "DeleteKeys\x12%.ess.proto.v1alpha1.DeleteKeysRequest\x1a&.ess.proto.v1alpha1.DeleteKeysResponse\"\x00BAZ?github.com/crossplane/crossplane-runtime/v2/apis/proto/v1alpha1b\x06proto3" var ( file_apis_proto_v1alpha1_ess_proto_rawDescOnce sync.Once file_apis_proto_v1alpha1_ess_proto_rawDescData []byte ) func file_apis_proto_v1alpha1_ess_proto_rawDescGZIP() []byte { file_apis_proto_v1alpha1_ess_proto_rawDescOnce.Do(func() { file_apis_proto_v1alpha1_ess_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_apis_proto_v1alpha1_ess_proto_rawDesc), len(file_apis_proto_v1alpha1_ess_proto_rawDesc))) }) return file_apis_proto_v1alpha1_ess_proto_rawDescData } var file_apis_proto_v1alpha1_ess_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_apis_proto_v1alpha1_ess_proto_goTypes = []any{ (*ConfigReference)(nil), // 0: ess.proto.v1alpha1.ConfigReference (*Secret)(nil), // 1: ess.proto.v1alpha1.Secret (*GetSecretRequest)(nil), // 2: ess.proto.v1alpha1.GetSecretRequest (*GetSecretResponse)(nil), // 3: ess.proto.v1alpha1.GetSecretResponse (*ApplySecretRequest)(nil), // 4: ess.proto.v1alpha1.ApplySecretRequest (*ApplySecretResponse)(nil), // 5: ess.proto.v1alpha1.ApplySecretResponse (*DeleteKeysRequest)(nil), // 6: ess.proto.v1alpha1.DeleteKeysRequest (*DeleteKeysResponse)(nil), // 7: ess.proto.v1alpha1.DeleteKeysResponse nil, // 8: ess.proto.v1alpha1.Secret.MetadataEntry nil, // 9: ess.proto.v1alpha1.Secret.DataEntry } var file_apis_proto_v1alpha1_ess_proto_depIdxs = []int32{ 8, // 0: ess.proto.v1alpha1.Secret.metadata:type_name -> ess.proto.v1alpha1.Secret.MetadataEntry 9, // 1: ess.proto.v1alpha1.Secret.data:type_name -> ess.proto.v1alpha1.Secret.DataEntry 0, // 2: ess.proto.v1alpha1.GetSecretRequest.config:type_name -> ess.proto.v1alpha1.ConfigReference 1, // 3: ess.proto.v1alpha1.GetSecretRequest.secret:type_name -> ess.proto.v1alpha1.Secret 1, // 4: ess.proto.v1alpha1.GetSecretResponse.secret:type_name -> ess.proto.v1alpha1.Secret 0, // 5: ess.proto.v1alpha1.ApplySecretRequest.config:type_name -> ess.proto.v1alpha1.ConfigReference 1, // 6: ess.proto.v1alpha1.ApplySecretRequest.secret:type_name -> ess.proto.v1alpha1.Secret 0, // 7: ess.proto.v1alpha1.DeleteKeysRequest.config:type_name -> ess.proto.v1alpha1.ConfigReference 1, // 8: ess.proto.v1alpha1.DeleteKeysRequest.secret:type_name -> ess.proto.v1alpha1.Secret 2, // 9: ess.proto.v1alpha1.ExternalSecretStorePluginService.GetSecret:input_type -> ess.proto.v1alpha1.GetSecretRequest 4, // 10: ess.proto.v1alpha1.ExternalSecretStorePluginService.ApplySecret:input_type -> ess.proto.v1alpha1.ApplySecretRequest 6, // 11: ess.proto.v1alpha1.ExternalSecretStorePluginService.DeleteKeys:input_type -> ess.proto.v1alpha1.DeleteKeysRequest 3, // 12: ess.proto.v1alpha1.ExternalSecretStorePluginService.GetSecret:output_type -> ess.proto.v1alpha1.GetSecretResponse 5, // 13: ess.proto.v1alpha1.ExternalSecretStorePluginService.ApplySecret:output_type -> ess.proto.v1alpha1.ApplySecretResponse 7, // 14: ess.proto.v1alpha1.ExternalSecretStorePluginService.DeleteKeys:output_type -> ess.proto.v1alpha1.DeleteKeysResponse 12, // [12:15] is the sub-list for method output_type 9, // [9:12] is the sub-list for method input_type 9, // [9:9] is the sub-list for extension type_name 9, // [9:9] is the sub-list for extension extendee 0, // [0:9] is the sub-list for field type_name } func init() { file_apis_proto_v1alpha1_ess_proto_init() } func file_apis_proto_v1alpha1_ess_proto_init() { if File_apis_proto_v1alpha1_ess_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_apis_proto_v1alpha1_ess_proto_rawDesc), len(file_apis_proto_v1alpha1_ess_proto_rawDesc)), NumEnums: 0, NumMessages: 10, NumExtensions: 0, NumServices: 1, }, GoTypes: file_apis_proto_v1alpha1_ess_proto_goTypes, DependencyIndexes: file_apis_proto_v1alpha1_ess_proto_depIdxs, MessageInfos: file_apis_proto_v1alpha1_ess_proto_msgTypes, }.Build() File_apis_proto_v1alpha1_ess_proto = out.File file_apis_proto_v1alpha1_ess_proto_goTypes = nil file_apis_proto_v1alpha1_ess_proto_depIdxs = nil } ================================================ FILE: apis/proto/v1alpha1/ess.proto ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ syntax = "proto3"; // buf:lint:ignore PACKAGE_DIRECTORY_MATCH package ess.proto.v1alpha1; option go_package = "github.com/crossplane/crossplane-runtime/v2/apis/proto/v1alpha1"; // ExternalSecretStorePluginService defines the APIs for an External Secret Store plugin. service ExternalSecretStorePluginService { rpc GetSecret(GetSecretRequest) returns (GetSecretResponse) {} rpc ApplySecret(ApplySecretRequest) returns (ApplySecretResponse) {} rpc DeleteKeys(DeleteKeysRequest) returns (DeleteKeysResponse) {} } // ConfigReference is used to refer a StoreConfig object. message ConfigReference { string api_version = 1; string kind = 2; string name = 3; } // Secret defines the structure of a secret. message Secret { string scoped_name = 1; map metadata = 2; map data = 3; } // GetSecretRequest requests secret from the secret store. message GetSecretRequest { ConfigReference config = 1; Secret secret = 2; } // GetSecretResponse returns the secret from the secret store. message GetSecretResponse { Secret secret = 1; } // ApplySecretRequest applies the secret data update to the secret store. message ApplySecretRequest { ConfigReference config = 1; Secret secret = 2; } // ApplySecretResponse returns if the secret is changed or not. message ApplySecretResponse { bool changed = 1; } // DeleteKeysRequest deletes the secret from the secret store. message DeleteKeysRequest { ConfigReference config = 1; Secret secret = 2; } // DeleteKeysResponse is returned if the secret is deleted. message DeleteKeysResponse {} ================================================ FILE: apis/proto/v1alpha1/ess_grpc.pb.go ================================================ // //Copyright 2023 The Crossplane Authors. //Licensed under the Apache License, Version 2.0 (the "License"); //you may not use this file except in compliance with the License. //You may obtain a copy of the License at //http://www.apache.org/licenses/LICENSE-2.0 //Unless required by applicable law or agreed to in writing, software //distributed under the License is distributed on an "AS IS" BASIS, //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //See the License for the specific language governing permissions and //limitations under the License. // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 // - protoc (unknown) // source: apis/proto/v1alpha1/ess.proto // buf:lint:ignore PACKAGE_DIRECTORY_MATCH package v1alpha1 import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.64.0 or later. const _ = grpc.SupportPackageIsVersion9 const ( ExternalSecretStorePluginService_GetSecret_FullMethodName = "/ess.proto.v1alpha1.ExternalSecretStorePluginService/GetSecret" ExternalSecretStorePluginService_ApplySecret_FullMethodName = "/ess.proto.v1alpha1.ExternalSecretStorePluginService/ApplySecret" ExternalSecretStorePluginService_DeleteKeys_FullMethodName = "/ess.proto.v1alpha1.ExternalSecretStorePluginService/DeleteKeys" ) // ExternalSecretStorePluginServiceClient is the client API for ExternalSecretStorePluginService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // // ExternalSecretStorePluginService defines the APIs for an External Secret Store plugin. type ExternalSecretStorePluginServiceClient interface { GetSecret(ctx context.Context, in *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error) ApplySecret(ctx context.Context, in *ApplySecretRequest, opts ...grpc.CallOption) (*ApplySecretResponse, error) DeleteKeys(ctx context.Context, in *DeleteKeysRequest, opts ...grpc.CallOption) (*DeleteKeysResponse, error) } type externalSecretStorePluginServiceClient struct { cc grpc.ClientConnInterface } func NewExternalSecretStorePluginServiceClient(cc grpc.ClientConnInterface) ExternalSecretStorePluginServiceClient { return &externalSecretStorePluginServiceClient{cc} } func (c *externalSecretStorePluginServiceClient) GetSecret(ctx context.Context, in *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetSecretResponse) err := c.cc.Invoke(ctx, ExternalSecretStorePluginService_GetSecret_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *externalSecretStorePluginServiceClient) ApplySecret(ctx context.Context, in *ApplySecretRequest, opts ...grpc.CallOption) (*ApplySecretResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ApplySecretResponse) err := c.cc.Invoke(ctx, ExternalSecretStorePluginService_ApplySecret_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *externalSecretStorePluginServiceClient) DeleteKeys(ctx context.Context, in *DeleteKeysRequest, opts ...grpc.CallOption) (*DeleteKeysResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteKeysResponse) err := c.cc.Invoke(ctx, ExternalSecretStorePluginService_DeleteKeys_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } // ExternalSecretStorePluginServiceServer is the server API for ExternalSecretStorePluginService service. // All implementations must embed UnimplementedExternalSecretStorePluginServiceServer // for forward compatibility. // // ExternalSecretStorePluginService defines the APIs for an External Secret Store plugin. type ExternalSecretStorePluginServiceServer interface { GetSecret(context.Context, *GetSecretRequest) (*GetSecretResponse, error) ApplySecret(context.Context, *ApplySecretRequest) (*ApplySecretResponse, error) DeleteKeys(context.Context, *DeleteKeysRequest) (*DeleteKeysResponse, error) mustEmbedUnimplementedExternalSecretStorePluginServiceServer() } // UnimplementedExternalSecretStorePluginServiceServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. type UnimplementedExternalSecretStorePluginServiceServer struct{} func (UnimplementedExternalSecretStorePluginServiceServer) GetSecret(context.Context, *GetSecretRequest) (*GetSecretResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetSecret not implemented") } func (UnimplementedExternalSecretStorePluginServiceServer) ApplySecret(context.Context, *ApplySecretRequest) (*ApplySecretResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ApplySecret not implemented") } func (UnimplementedExternalSecretStorePluginServiceServer) DeleteKeys(context.Context, *DeleteKeysRequest) (*DeleteKeysResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteKeys not implemented") } func (UnimplementedExternalSecretStorePluginServiceServer) mustEmbedUnimplementedExternalSecretStorePluginServiceServer() { } func (UnimplementedExternalSecretStorePluginServiceServer) testEmbeddedByValue() {} // UnsafeExternalSecretStorePluginServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ExternalSecretStorePluginServiceServer will // result in compilation errors. type UnsafeExternalSecretStorePluginServiceServer interface { mustEmbedUnimplementedExternalSecretStorePluginServiceServer() } func RegisterExternalSecretStorePluginServiceServer(s grpc.ServiceRegistrar, srv ExternalSecretStorePluginServiceServer) { // If the following call pancis, it indicates UnimplementedExternalSecretStorePluginServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } s.RegisterService(&ExternalSecretStorePluginService_ServiceDesc, srv) } func _ExternalSecretStorePluginService_GetSecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetSecretRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ExternalSecretStorePluginServiceServer).GetSecret(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: ExternalSecretStorePluginService_GetSecret_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ExternalSecretStorePluginServiceServer).GetSecret(ctx, req.(*GetSecretRequest)) } return interceptor(ctx, in, info, handler) } func _ExternalSecretStorePluginService_ApplySecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ApplySecretRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ExternalSecretStorePluginServiceServer).ApplySecret(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: ExternalSecretStorePluginService_ApplySecret_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ExternalSecretStorePluginServiceServer).ApplySecret(ctx, req.(*ApplySecretRequest)) } return interceptor(ctx, in, info, handler) } func _ExternalSecretStorePluginService_DeleteKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteKeysRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ExternalSecretStorePluginServiceServer).DeleteKeys(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: ExternalSecretStorePluginService_DeleteKeys_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ExternalSecretStorePluginServiceServer).DeleteKeys(ctx, req.(*DeleteKeysRequest)) } return interceptor(ctx, in, info, handler) } // ExternalSecretStorePluginService_ServiceDesc is the grpc.ServiceDesc for ExternalSecretStorePluginService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var ExternalSecretStorePluginService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "ess.proto.v1alpha1.ExternalSecretStorePluginService", HandlerType: (*ExternalSecretStorePluginServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetSecret", Handler: _ExternalSecretStorePluginService_GetSecret_Handler, }, { MethodName: "ApplySecret", Handler: _ExternalSecretStorePluginService_ApplySecret_Handler, }, { MethodName: "DeleteKeys", Handler: _ExternalSecretStorePluginService_DeleteKeys_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "apis/proto/v1alpha1/ess.proto", } ================================================ FILE: buf.gen.yaml ================================================ # This file contains configuration for the `buf generate` command. # See generate.go for more details. version: v1 plugins: - plugin: go out: . opt: paths=source_relative - plugin: go-grpc out: . opt: paths=source_relative ================================================ FILE: buf.yaml ================================================ version: v1 name: buf.build/crossplane/crossplane-runtime breaking: use: - WIRE_JSON lint: use: - DEFAULT allow_comment_ignores: true ================================================ FILE: flake.nix ================================================ # New to Nix? Start here: # Language basics: https://nix.dev/tutorials/nix-language # Flakes intro: https://zero-to-nix.com/concepts/flakes { description = "Crossplane Runtime - Go library for building Crossplane providers and controllers"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; # TODO(negz): Unpin once https://github.com/nix-community/gomod2nix/pull/231 is released. gomod2nix = { url = "github:nix-community/gomod2nix/49662a44272806ff785df2990a420edaaca15db4"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { self, nixpkgs, nixpkgs-unstable, gomod2nix, }: let # Systems where Nix runs (dev machines, CI). supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; # Helpers for per-system outputs. forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: forSystem system f); forSystem = system: f: f { inherit system; pkgs = import nixpkgs { inherit system; overlays = [ gomod2nix.overlays.default (_final: _prev: { go = nixpkgs-unstable.legacyPackages.${system}.go_1_25; inherit (nixpkgs-unstable.legacyPackages.${system}) go_1_25; }) ]; }; }; in { # CI checks (nix flake check). checks = forAllSystems ( { pkgs, ... }: let checks = import ./nix/checks.nix { inherit pkgs self; }; in { test = checks.test { }; generate = checks.generate { }; go-lint = checks.goLint { }; nix-lint = checks.nixLint { }; } ); # Development commands (nix run .#). apps = forAllSystems ( { pkgs, ... }: let apps = import ./nix/apps.nix { inherit pkgs; }; in { test = apps.test { }; lint = apps.lint { fix = true; }; generate = apps.generate { }; tidy = apps.tidy { }; } ); # Development shell (nix develop). devShells = forAllSystems ( { pkgs, ... }: { default = pkgs.mkShell { buildInputs = [ pkgs.coreutils pkgs.gnused pkgs.ncurses pkgs.go pkgs.golangci-lint pkgs.gomod2nix # Code generation pkgs.buf pkgs.protoc-gen-go pkgs.protoc-gen-go-grpc pkgs.kubernetes-controller-tools # Nix pkgs.nixfmt-rfc-style ]; shellHook = '' export PS1='\[\033[38;2;243;128;123m\][cros\[\033[38;2;255;205;60m\]spla\[\033[38;2;53;208;186m\]ne-rt]\[\033[0m\] \w \$ ' echo "Crossplane Runtime development shell ($(go version | cut -d' ' -f3))" echo "" echo " nix run .#test nix run .#generate" echo " nix run .#lint nix run .#tidy" echo "" echo " nix flake check" echo "" ''; }; } ); }; } ================================================ FILE: generate.go ================================================ //go:build generate // +build generate /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generation tools (controller-gen, buf, etc.) must be in your $PATH. Use // './nix.sh develop' or './nix.sh run .#generate' to ensure they are. // Generate deepcopy methodsets //go:generate controller-gen object:headerFile=./hack/boilerplate.go.txt paths=./apis/... // Generate gRPC types and stubs. See buf.gen.yaml for buf's configuration. // The protoc-gen-go and protoc-gen-go-grpc plugins must be in $PATH. // Note that the vendor dir does temporarily exist during a Nix build. //go:generate buf generate --exclude-path vendor package generate ================================================ FILE: go.mod ================================================ module github.com/crossplane/crossplane-runtime/v2 go 1.25.9 require ( dario.cat/mergo v1.0.2 github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 github.com/Masterminds/semver/v3 v3.4.0 github.com/crossplane/crossplane/apis/v2 v2.0.0-20260424160951-8f231230ebb6 github.com/evanphx/json-patch v5.9.11+incompatible github.com/go-logr/logr v1.4.3 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.7 github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20230919002926-dbcd01c402b2 github.com/in-toto/attestation v1.1.2 github.com/in-toto/in-toto-golang v0.11.0 github.com/prometheus/client_golang v1.23.2 github.com/sigstore/cosign/v3 v3.0.5 github.com/sigstore/sigstore v1.10.5 github.com/sirupsen/logrus v1.9.4 github.com/spf13/afero v1.15.0 golang.org/x/time v0.15.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 k8s.io/api v0.35.1 k8s.io/apiextensions-apiserver v0.35.0 k8s.io/apimachinery v0.35.1 k8s.io/client-go v0.35.1 k8s.io/component-base v0.35.0 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20260108192941-914a6e750570 sigs.k8s.io/controller-runtime v0.23.1 sigs.k8s.io/controller-tools v0.20.0 sigs.k8s.io/yaml v1.6.0 ) require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.30 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 // indirect github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.2 // indirect github.com/Azure/go-autorest/tracing v0.6.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2 v1.41.4 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3 // indirect github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.10 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect github.com/aws/smithy-go v1.24.2 // indirect github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.12.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/docker/cli v29.4.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.24.3 // indirect github.com/go-openapi/errors v0.22.7 // indirect github.com/go-openapi/jsonpointer v0.22.5 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/loads v0.23.3 // indirect github.com/go-openapi/runtime v0.29.3 // indirect github.com/go-openapi/spec v0.22.4 // indirect github.com/go-openapi/strfmt v0.26.1 // indirect github.com/go-openapi/swag v0.25.5 // indirect github.com/go-openapi/swag/cmdutils v0.25.5 // indirect github.com/go-openapi/swag/conv v0.25.5 // indirect github.com/go-openapi/swag/fileutils v0.25.5 // indirect github.com/go-openapi/swag/jsonname v0.25.5 // indirect github.com/go-openapi/swag/jsonutils v0.25.5 // indirect github.com/go-openapi/swag/loading v0.25.5 // indirect github.com/go-openapi/swag/mangling v0.25.5 // indirect github.com/go-openapi/swag/netutils v0.25.5 // indirect github.com/go-openapi/swag/stringutils v0.25.5 // indirect github.com/go-openapi/swag/typeutils v0.25.5 // indirect github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-openapi/validate v0.25.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobuffalo/flect v1.0.3 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.1 // indirect github.com/google/certificate-transparency-go v1.3.3 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20250225234217-098045d5e61f // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/letsencrypt/boulder v0.20260223.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect github.com/sigstore/rekor v1.5.1 // indirect github.com/sigstore/rekor-tiles/v2 v2.2.1 // indirect github.com/sigstore/sigstore-go v1.1.4 // indirect github.com/sigstore/timestamp-authority/v2 v2.0.6 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/theupdateframework/go-tuf/v2 v2.4.1 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect go.opentelemetry.io/otel v1.42.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/term v0.42.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/tools v0.44.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/code-generator v0.35.0 // indirect k8s.io/gengo/v2 v2.0.0-20251215205346-5ee0d033ba5b // indirect k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect ) ================================================ FILE: go.sum ================================================ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 h1:Q9R3utmFg9K1B4OYtAZ7ZUUvIUdzQt7G2MN5Hi/d670= github.com/Azure/go-autorest/autorest/azure/cli v0.4.7/go.mod h1:bVrAueELJ0CKLBpUHDIvD516TwmHmzqwCpvONWRsw3s= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3 h1:RtGctYMmkTerGClvdY6bHXdtly4FeYw9wz/NPz62LF8= github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3/go.mod h1:vBfBu24Ka3/5UZtepbTV0gnc9VPLT8ok+0oDDaYAzn4= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.10 h1:1A/sI3LNMi3fhRI5TFLMwwo7ALAALSFVCSGvFlr1Iys= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.10/go.mod h1:Diyyyz0b43X13pdi1mVMqlTwDjOmRbJMvDsqnduUYWM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY= github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw= github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.12.0 h1:JFWXO6QPihCknDdnL6VaQE57km4ZKheHIGd9YiOGcTo= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.12.0/go.mod h1:046/oLyFlYdAghYQE2yHXi/E//VM5Cf3/dFmA+3CZ0c= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/crossplane/crossplane/apis/v2 v2.0.0-20260424160951-8f231230ebb6 h1:9ki6AJQgBJIcLNjK+scUZp2ZDenuAo18d0JSNOlkY2Y= github.com/crossplane/crossplane/apis/v2 v2.0.0-20260424160951-8f231230ebb6/go.mod h1:h7KE74Z4TFs1L/FFv3RdsiG9Uax7L56oHpcggSZnONg= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/docker/cli v29.4.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM= github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20230919002926-dbcd01c402b2 h1:ChuUQ1y5Vf+Eev+UgEed/ljibTIcWY7mYPtWYLK7fxU= github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20230919002926-dbcd01c402b2/go.mod h1:Ek+8PQrShkA7aHEj3/zSW33wU0V/Bx3zW/gFh7l21xY= github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20250225234217-098045d5e61f h1:GJRzEBoJv/A/E7JbTekq1Q0jFtAfY7TIxUFAK89Mmic= github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20250225234217-098045d5e61f/go.mod h1:ZT74/OE6eosKneM9/LQItNxIMBV6CI5S46EXAnvkTBI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e h1:FJta/0WsADCe1r9vQjdHbd3KuiLPu7Y9WlyLGwMUNyE= github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js= github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA= github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMeKfVoWsWeFMDCMtw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/letsencrypt/boulder v0.20260223.0 h1:xdS2OnJNUasR6TgVIOpqqcvdkOu47+PQQMBk9ThuWBw= github.com/letsencrypt/boulder v0.20260223.0/go.mod h1:r3aTSA7UZ7dbDfiGK+HLHJz0bWNbHk6YSPiXgzl23sA= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= github.com/secure-systems-lab/go-securesystemslib v0.10.0 h1:l+H5ErcW0PAehBNrBxoGv1jjNpGYdZ9RcheFkB2WI14= github.com/secure-systems-lab/go-securesystemslib v0.10.0/go.mod h1:MRKONWmRoFzPNQ9USRF9i1mc7MvAVvF1LlW8X5VWDvk= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/sigstore/cosign/v3 v3.0.5 h1:c1zPqjU+H4wmirgysC+AkWMg7a7fykyOYF/m+F1150I= github.com/sigstore/cosign/v3 v3.0.5/go.mod h1:ble1vMvJagCFyTIDkibCq6MIHiWDw00JNYl0f9rB4T4= github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= github.com/sigstore/rekor v1.5.1 h1:Ca1egHRWRuDvXV4tZu9aXEXc3Gej9FG+HKeapV9OAMQ= github.com/sigstore/rekor v1.5.1/go.mod h1:gTLDuZuo3SyQCuZvKqwRPA79Qo/2rw39/WtLP/rZjUQ= github.com/sigstore/rekor-tiles/v2 v2.2.1 h1:UmV1CBQ3SjxxPGpFmwDoOhoIwiKpM2Qm1pU5tPGmvNk= github.com/sigstore/rekor-tiles/v2 v2.2.1/go.mod h1:z8n6l6oidpaLjjE6rJERuQqY9X38ulnHZCXyL+DEL7U= github.com/sigstore/sigstore v1.10.5 h1:KqrOjDhNOVY+uOzQFat2FrGLClPPCb3uz8pK3wuI+ow= github.com/sigstore/sigstore v1.10.5/go.mod h1:k/mcVVXw3I87dYG/iCVTSW2xTrW7vPzxxGic4KqsqXs= github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.5 h1:aqHRubTITULckG9JAcq2FEhtKkT/RRE8oErfuV3smSI= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.5/go.mod h1:h9eK9QyPqpFskF/ewFkRLtwh4/Q3FLc2/DXbym4IHN8= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.5 h1:+9C6CUkv+J4iT67Lx+H1EGBfAdoAHqXumHadeIj9jA4= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.5/go.mod h1:myZsg7wRiy/vf102g5uUAitYhtXCwepmAGxgHG1VHuE= github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.5 h1:BpQx6AhjwIN9LmlO4ypkcMcHiWiepgZQGSw5U69frHU= github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.5/go.mod h1:ejMD/17lMJ4HykQRPdj5NNr+OQYIEZto8HjDKghVMOA= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.5 h1:OFwQZgWkB/6J6W5sy3SkXE4pJnhNRnE2cJd8ySXmHpo= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.5/go.mod h1:Ee/enmyxi/RFLVlajbnjgH2wOWQwlJ0wY8qZrk43hEw= github.com/sigstore/timestamp-authority/v2 v2.0.6 h1:1Vh7/SdmLsVLG6Br6/bisd1SnlicfDm0MJYiA+D7Ppw= github.com/sigstore/timestamp-authority/v2 v2.0.6/go.mod h1:Nk5ucGBDyH0tXAIMZ0prf6xn8qfTnbJhSq+CDabYcfc= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= github.com/theupdateframework/go-tuf/v2 v2.4.1 h1:K6ewW064rKZCPkRo1W/CTbTtm/+IB4+coG1iNURAGCw= github.com/theupdateframework/go-tuf/v2 v2.4.1/go.mod h1:Nex2enPVYDFCklrnbTzl3OVwD7fgIAj0J5++z/rvCj8= github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0 h1:j+S+WKBQ5ya26A5EM/uXoVe+a2IaPQN8KgBJZ22cJ+4= github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0/go.mod h1:OCKJIujnTzDq7f+73NhVs99oA2c1TR6nsOpuasYM6Yo= github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8= go.step.sm/crypto v0.77.2/go.mod h1:W0YJb9onM5l78qgkXIJ2Up6grnwW8EtpCKIza/NCg0o= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js= google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts= google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= k8s.io/code-generator v0.35.0 h1:TvrtfKYZTm9oDF2z+veFKSCcgZE3Igv0svY+ehCmjHQ= k8s.io/code-generator v0.35.0/go.mod h1:iS1gvVf3c/T71N5DOGYO+Gt3PdJ6B9LYSvIyQ4FHzgc= k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/gengo/v2 v2.0.0-20251215205346-5ee0d033ba5b h1:0YkdvW3rX2vaBWsqCGZAekxPRwaI5NuYNprOsMNVLns= k8s.io/gengo/v2 v2.0.0-20251215205346-5ee0d033ba5b/go.mod h1:yvyl3l9E+UxlqOMUULdKTAYB0rEhsmjr7+2Vb/1pCSo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/controller-tools v0.20.0 h1:VWZF71pwSQ2lZZCt7hFGJsOfDc5dVG28/IysjjMWXL8= sigs.k8s.io/controller-tools v0.20.0/go.mod h1:b4qPmjGU3iZwqn34alUU5tILhNa9+VXK+J3QV0fT/uU= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= ================================================ FILE: gomod2nix.toml ================================================ schema = 3 [mod] [mod."cloud.google.com/go/compute/metadata"] version = "v0.9.0" hash = "sha256-VFqQwLJKyH1zReR/XtygEHP5UkI01T9BHEL0hvXtauo=" [mod."dario.cat/mergo"] version = "v1.0.2" hash = "sha256-p6jdiHlLEfZES8vJnDywG4aVzIe16p0CU6iglglIweA=" [mod."github.com/AdaLogics/go-fuzz-headers"] version = "v0.0.0-20240806141605-e8a1dd7889d6" hash = "sha256-4h0bcF0eg/Y9DnurD91SbGAzLz0DwffJsF//YJGfPJg=" [mod."github.com/Azure/azure-sdk-for-go"] version = "v68.0.0+incompatible" hash = "sha256-xaa9LgrrLjgbOh/XM1dZMWH/sZeGMb59gK0CTB6JUUI=" [mod."github.com/Azure/go-ansiterm"] version = "v0.0.0-20250102033503-faa5f7b0171c" hash = "sha256-4WYKJtxjnm3egDAh9ocTR+gy5UUqVoY3knHy9c17XIY=" [mod."github.com/Azure/go-autorest"] version = "v14.2.0+incompatible" hash = "sha256-dvWOcudtx0NP6U2RDt40hwtELFRdYdLEklRWYterRN0=" [mod."github.com/Azure/go-autorest/autorest"] version = "v0.11.30" hash = "sha256-CykvDRDHHCyhIZOxbvpT/a0VEhuJmIwKw/MEjS3hmEs=" [mod."github.com/Azure/go-autorest/autorest/adal"] version = "v0.9.24" hash = "sha256-itnCV0BJlMi5MHFlxePRUA/XPwofDzTksUVh7jcqarE=" [mod."github.com/Azure/go-autorest/autorest/azure/auth"] version = "v0.5.13" hash = "sha256-s901woJ0T3B+1QUUOMcjz0ops2pXzZQ+x7/XEuC91Ko=" [mod."github.com/Azure/go-autorest/autorest/azure/cli"] version = "v0.4.7" hash = "sha256-ljC1ag2fX8jLdlgr1wgLx66QdRHYa9VdOu0r9RFDtLo=" [mod."github.com/Azure/go-autorest/autorest/date"] version = "v0.3.1" hash = "sha256-DqCnDxzYgcAPEpnlHqa+eL3msZvbkYNSMq6ftSEMSQo=" [mod."github.com/Azure/go-autorest/logger"] version = "v0.2.2" hash = "sha256-fmbHaafgS17KXIXpqqChOF8qqi+lfJHZM4o+i0pmNSs=" [mod."github.com/Azure/go-autorest/tracing"] version = "v0.6.1" hash = "sha256-nstDZC8Btx78yzqIR4clfu+R93rebUOZalEW1ZaQfIY=" [mod."github.com/Masterminds/semver/v3"] version = "v3.4.0" hash = "sha256-75kRraVwYVjYLWZvuSlts4Iu28Eh3SpiF0GHc7vCYHI=" [mod."github.com/antlr4-go/antlr/v4"] version = "v4.13.1" hash = "sha256-beAuxHNRUuhzcSJUh/8ztVf1zCUiaT72fg2Jvx0AuNQ=" [mod."github.com/asaskevich/govalidator"] version = "v0.0.0-20230301143203-a9d515a09cc2" hash = "sha256-UCENzt1c1tFgsAzK2TNq5s2g0tQMQ5PxFaQKe8hTL/A=" [mod."github.com/aws/aws-sdk-go-v2"] version = "v1.41.4" hash = "sha256-k9xv4f8YPSzZ1yR3/zuyNDGenZKk0DD4lceL713yXtc=" [mod."github.com/aws/aws-sdk-go-v2/config"] version = "v1.32.12" hash = "sha256-aTkdSRe8KPmVZdsunU8j/hZQLhGw1ckKpLN/ryRBZM0=" [mod."github.com/aws/aws-sdk-go-v2/credentials"] version = "v1.19.12" hash = "sha256-xEIT1ARA9RYrQtLZIus71E6niNHIOVM1J7mUnA5AhJQ=" [mod."github.com/aws/aws-sdk-go-v2/feature/ec2/imds"] version = "v1.18.20" hash = "sha256-dCTpdKZheVCSt+R+NnFOnlS0bCt4gPavlDh15Kl/sMQ=" [mod."github.com/aws/aws-sdk-go-v2/internal/configsources"] version = "v1.4.20" hash = "sha256-aATIk4oLd7aaV66ereBdjINLMDwmIHxu+NNsgKWH1t4=" [mod."github.com/aws/aws-sdk-go-v2/internal/endpoints/v2"] version = "v2.7.20" hash = "sha256-G6266uj64sgfDTJ9V1UY1sQs3UmryB0CFgxzmbjjChY=" [mod."github.com/aws/aws-sdk-go-v2/internal/ini"] version = "v1.8.6" hash = "sha256-oIRPqu99vnGINAWKnCEytpv7N0gRWO7S72tb1r8oxvk=" [mod."github.com/aws/aws-sdk-go-v2/service/ecr"] version = "v1.55.3" hash = "sha256-J9v9A2bMBTPM0K/aHM3TrS0nBkuTNFVQyqtnc1ZwE7w=" [mod."github.com/aws/aws-sdk-go-v2/service/ecrpublic"] version = "v1.38.10" hash = "sha256-uJtfhtkG4pfehKHyc2dsqafvt6fKMnMgMe/uPemJrPY=" [mod."github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding"] version = "v1.13.7" hash = "sha256-AfYJdpmnW01Bk/jfHATlNU6lddjqcigFkHw/zcT9WO4=" [mod."github.com/aws/aws-sdk-go-v2/service/internal/presigned-url"] version = "v1.13.20" hash = "sha256-a5TifKunIoqKd2uAceYh6F1LvMHMyEQcWvJf0sxKhPM=" [mod."github.com/aws/aws-sdk-go-v2/service/signin"] version = "v1.0.8" hash = "sha256-o4pWg3yMZHxdI94x5Z6qbiRg7gpmzbpJnJWsR1BOc44=" [mod."github.com/aws/aws-sdk-go-v2/service/sso"] version = "v1.30.13" hash = "sha256-V277a0ikm/H0paIeDLtPGEyav2a69Kdb9d5bh+JLAeY=" [mod."github.com/aws/aws-sdk-go-v2/service/ssooidc"] version = "v1.35.17" hash = "sha256-r5V5DoCIR4yzN1Ttg+dIA85GVkWMPgeD6Zu0rWGqNJE=" [mod."github.com/aws/aws-sdk-go-v2/service/sts"] version = "v1.41.9" hash = "sha256-I15uxeoKxDURsZrEVDzCRtVIu/HE756M1Rt7PPpdZ7c=" [mod."github.com/aws/smithy-go"] version = "v1.24.2" hash = "sha256-v0y+Lir61fgdCwdVoca5mK+FcGh9OD3cTEwHIfLytcI=" [mod."github.com/awslabs/amazon-ecr-credential-helper/ecr-login"] version = "v0.12.0" hash = "sha256-PHoHWGX9RVkAQNQF2fz5Hv4JEaGBvLhcuUwlu5IQjU0=" [mod."github.com/beorn7/perks"] version = "v1.0.1" hash = "sha256-h75GUqfwJKngCJQVE5Ao5wnO3cfKD9lSIteoLp/3xJ4=" [mod."github.com/blang/semver"] version = "v3.5.1+incompatible" hash = "sha256-vmoIH5J0esVFmLDT2ecwtalvJqRRoLwomysyvlIRmo8=" [mod."github.com/blang/semver/v4"] version = "v4.0.0" hash = "sha256-dJC22MjnfT5WqJ7x7Tc3Bvpw9tFnBn9HqfWFiM57JVc=" [mod."github.com/cenkalti/backoff/v5"] version = "v5.0.3" hash = "sha256-bKq43PPD8RM6e7HePxHaO27traqm76bkvHcTVTQ+jeY=" [mod."github.com/cespare/xxhash/v2"] version = "v2.3.0" hash = "sha256-7hRlwSR+fos1kx4VZmJ/7snR7zHh8ZFKX+qqqqGcQpY=" [mod."github.com/chrismellard/docker-credential-acr-env"] version = "v0.0.0-20230304212654-82a0ddb27589" hash = "sha256-EWyO62fm/zhWdo4/96bscr3POG/5tKsWXYqp5mTwP0Y=" [mod."github.com/containerd/stargz-snapshotter/estargz"] version = "v0.18.2" hash = "sha256-6KS9ObQ1tKXkvvKQy1BmxJ59aisDGvEtqhj1Oo54IRY=" [mod."github.com/coreos/go-oidc/v3"] version = "v3.17.0" hash = "sha256-b9dCq5GN5ac64UG23Rijv1qcmUZNcxb8DJQycAa96EQ=" [mod."github.com/crossplane/crossplane/apis/v2"] version = "v2.0.0-20260424160951-8f231230ebb6" hash = "sha256-wtcG/nMC4A9nebFxsfSlWZjdupAQ6IjHinFhvFB6KNk=" [mod."github.com/cyberphone/json-canonicalization"] version = "v0.0.0-20241213102144-19d51d7fe467" hash = "sha256-eqH3UKAZ9eOlZjYdN7nWuJ1hFm2JAP1PVbJInQk6OLw=" [mod."github.com/davecgh/go-spew"] version = "v1.1.2-0.20180830191138-d8f796af33cc" hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc=" [mod."github.com/digitorus/pkcs7"] version = "v0.0.0-20230818184609-3a137a874352" hash = "sha256-zhgLL+kS2vkOhiK3kkI6yMhr71JOYo/uuxDo1dsC2k0=" [mod."github.com/digitorus/timestamp"] version = "v0.0.0-20231217203849-220c5c2851b7" hash = "sha256-uNkyMBsdbLN1PiDLHAGWUYf6sZ08ENbxpv9RkNtzaW0=" [mod."github.com/dimchansky/utfbom"] version = "v1.1.1" hash = "sha256-w8KEprK54zJkMat78T6zldjDwvhbc/O8s6pVFzfmg1I=" [mod."github.com/docker/cli"] version = "v29.4.0+incompatible" hash = "sha256-mUN7Fu9e4ahtUJBUvCHUk+ICFq1d6vs7MoJf0/cw+mA=" [mod."github.com/docker/distribution"] version = "v2.8.3+incompatible" hash = "sha256-XhRURCGNpJC83QZTtgCxHHFL76HaxIxjt70HwUa847E=" [mod."github.com/docker/docker-credential-helpers"] version = "v0.9.5" hash = "sha256-7fm66H8bvqjiEssTy/oiAMmQd7T15aVS+EANrw+4H4U=" [mod."github.com/dustin/go-humanize"] version = "v1.0.1" hash = "sha256-yuvxYYngpfVkUg9yAmG99IUVmADTQA0tMbBXe0Fq0Mc=" [mod."github.com/emicklei/go-restful/v3"] version = "v3.13.0" hash = "sha256-lB2Z29RDLiVQE5NrsV1s2iHeQ4ciGwNj5OG1zJxwZV8=" [mod."github.com/evanphx/json-patch"] version = "v5.9.11+incompatible" hash = "sha256-1iyZpBaeBLmNkJ3T4A9fAEXEYB9nk9V02ug4pwl5dy0=" [mod."github.com/evanphx/json-patch/v5"] version = "v5.9.11" hash = "sha256-DaWzRi5dIr3U7kJlV3Qm1DWoKh5W+FI2BW/ATXT40J4=" [mod."github.com/fatih/color"] version = "v1.18.0" hash = "sha256-pP5y72FSbi4j/BjyVq/XbAOFjzNjMxZt2R/lFFxGWvY=" [mod."github.com/fsnotify/fsnotify"] version = "v1.9.0" hash = "sha256-WtpE1N6dpHwEvIub7Xp/CrWm0fd6PX7MKA4PV44rp2g=" [mod."github.com/fxamacker/cbor/v2"] version = "v2.9.0" hash = "sha256-/IZK76MRCrz9XCiilieH5tKaLnIWyPJhwxDoVKB8dFc=" [mod."github.com/go-chi/chi/v5"] version = "v5.2.5" hash = "sha256-Y1+17ky94849aqk3iKf30F1u+G6K3nzZzLOBSeqIUow=" [mod."github.com/go-jose/go-jose/v4"] version = "v4.1.4" hash = "sha256-MKoJKXup1jfwOyN8mHXu1CQ8fvFJTaEf3K2LVtNSRhc=" [mod."github.com/go-logr/logr"] version = "v1.4.3" hash = "sha256-Nnp/dEVNMxLp3RSPDHZzGbI8BkSNuZMX0I0cjWKXXLA=" [mod."github.com/go-logr/stdr"] version = "v1.2.2" hash = "sha256-rRweAP7XIb4egtT1f2gkz4sYOu7LDHmcJ5iNsJUd0sE=" [mod."github.com/go-openapi/analysis"] version = "v0.24.3" hash = "sha256-jBLHbyhrdLwc0x/P3MlUik0xZPV6xOaKAa5aPV77sBY=" [mod."github.com/go-openapi/errors"] version = "v0.22.7" hash = "sha256-Iy5ieFbpjjbVEQ+UWXIeI8jzN/TjtsOoEX5IB6/v6gc=" [mod."github.com/go-openapi/jsonpointer"] version = "v0.22.5" hash = "sha256-btK8c3hxbO9mAlvRYuAaNdjaHNYyLvNl/RfNxPsqQuw=" [mod."github.com/go-openapi/jsonreference"] version = "v0.21.5" hash = "sha256-fBeVESt+JKLthLDTyuABzPV/hOmg4t8dPtwvAs1Ojog=" [mod."github.com/go-openapi/loads"] version = "v0.23.3" hash = "sha256-KeSh5BVDAkNBw50Yoyi19lEzODOrTcASyjtnpcrcZ10=" [mod."github.com/go-openapi/runtime"] version = "v0.29.3" hash = "sha256-nid3RStsHKEZgCNPU1+4NkqDsIOAc52JOF68zH3/bC4=" [mod."github.com/go-openapi/spec"] version = "v0.22.4" hash = "sha256-nwNLmtrjR3w+vFGlBFSq4vM2ZsDQS4RDBQGYmlGv7BY=" [mod."github.com/go-openapi/strfmt"] version = "v0.26.1" hash = "sha256-HcLytF6mvc2I+ReaKTjL5/LJzKfdwhb0iIpxjjV9+6M=" [mod."github.com/go-openapi/swag"] version = "v0.25.5" hash = "sha256-ptIgtll6FVI7viEVxM/imWUjgaeLP/oViAarM/EhsWU=" [mod."github.com/go-openapi/swag/cmdutils"] version = "v0.25.5" hash = "sha256-sEGS7K9gzBuKgkoIiHn5Mgv7+SvPqJ1iFZRsXrso/2M=" [mod."github.com/go-openapi/swag/conv"] version = "v0.25.5" hash = "sha256-+yLC40AK2pyn62zStk7Q13Bsb4/HDsJUKTTNBSWSTvg=" [mod."github.com/go-openapi/swag/fileutils"] version = "v0.25.5" hash = "sha256-zYxEpqJuZ97vFLQxfYwegDQhffKhpsDtYF1xhOVxL4c=" [mod."github.com/go-openapi/swag/jsonname"] version = "v0.25.5" hash = "sha256-ypcI24qrUOd0lbZUJcFByQr07U7WtQvIu/YhuewUWDo=" [mod."github.com/go-openapi/swag/jsonutils"] version = "v0.25.5" hash = "sha256-e6OOoTIH/zrI/unpNIu3foYEvSWKb7Jvf+6E6/nvpMg=" [mod."github.com/go-openapi/swag/loading"] version = "v0.25.5" hash = "sha256-gwy+xJkF3PHT5YMYnXgSX9XhuvwwOVpH60QTLsAh6/E=" [mod."github.com/go-openapi/swag/mangling"] version = "v0.25.5" hash = "sha256-SXSdvYE+wIm95KHRUPYjPEdFU6hc85/7H5rJH7bdTSM=" [mod."github.com/go-openapi/swag/netutils"] version = "v0.25.5" hash = "sha256-FzjcovD9ZGR/dNyU019KC/CRVn/OJ6XUJ3hS5J4w6go=" [mod."github.com/go-openapi/swag/stringutils"] version = "v0.25.5" hash = "sha256-Ze2Y056Imqyq6kHPcACuqHt992WbXfC9LDziSCFuO/c=" [mod."github.com/go-openapi/swag/typeutils"] version = "v0.25.5" hash = "sha256-A1mGLvoaLCT0iORn4tiyKWB8L69dMJzFjBFpt80Xzkg=" [mod."github.com/go-openapi/swag/yamlutils"] version = "v0.25.5" hash = "sha256-+EumuV+qkhYn08XfR1ngIKMh79Mkj8vItpg0y0spX+c=" [mod."github.com/go-openapi/validate"] version = "v0.25.2" hash = "sha256-jH7GfH+JyC1tD2Ejz8ioI5U7IKYqQbllU381qSo5D30=" [mod."github.com/go-viper/mapstructure/v2"] version = "v2.5.0" hash = "sha256-LbrCBANBprVI84M0CWrXc7rriJL5ac5VKbh58LBTw7U=" [mod."github.com/gobuffalo/flect"] version = "v1.0.3" hash = "sha256-gpA1fe9XTjZ9r+yYCysCgXKo1AmYNuNFwWn7ZQ4Ky1M=" [mod."github.com/golang-jwt/jwt/v4"] version = "v4.5.2" hash = "sha256-rTSqYEPooi8Uu4aXMW6k9dynOV+URYTGzVmbG3EQ7uo=" [mod."github.com/golang/snappy"] version = "v0.0.4" hash = "sha256-Umx+5xHAQCN/Gi4HbtMhnDCSPFAXSsjVbXd8n5LhjAA=" [mod."github.com/google/btree"] version = "v1.1.3" hash = "sha256-/6Us2eNRFi2IIp7p5uPUXLridilAdk4SmZhcTYR0csw=" [mod."github.com/google/cel-go"] version = "v0.26.1" hash = "sha256-XVL+pNGjmLrQefpB7qj419zWl1qhPVHmVxsP/Ro1AtE=" [mod."github.com/google/certificate-transparency-go"] version = "v1.3.3" hash = "sha256-CdAOfBmZ7xs51YUxLhg5edjxeCQqE2Kw0jOe+jHgvfA=" [mod."github.com/google/gnostic-models"] version = "v0.7.1" hash = "sha256-dfSFaYzgD4HrdL6ZsN8V9w0SMzx0WXl38dIy4dnjhhc=" [mod."github.com/google/go-cmp"] version = "v0.7.0" hash = "sha256-JbxZFBFGCh/Rj5XZ1vG94V2x7c18L8XKB0N9ZD5F2rM=" [mod."github.com/google/go-containerregistry"] version = "v0.20.7" hash = "sha256-IkvePl7PwjCaHPealmpkxpqeNo7Cn1I/xzHyHV1x18c=" [mod."github.com/google/go-containerregistry/pkg/authn/k8schain"] version = "v0.0.0-20230919002926-dbcd01c402b2" hash = "sha256-y/xHODMYpIsday3XuwTS8bO5+1CMjgazalC2fijnC6c=" [mod."github.com/google/go-containerregistry/pkg/authn/kubernetes"] version = "v0.0.0-20250225234217-098045d5e61f" hash = "sha256-UZyDwMt9qQw5XHHDOlTyYMRvG1BiDfBHeZLmoMzunB4=" [mod."github.com/google/uuid"] version = "v1.6.0" hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw=" [mod."github.com/grpc-ecosystem/grpc-gateway/v2"] version = "v2.27.7" hash = "sha256-5TAHtwMLAbyk4iRQ574kb+EPEGhcZ0ZsoLQplck7zFA=" [mod."github.com/hashicorp/go-cleanhttp"] version = "v0.5.2" hash = "sha256-N9GOKYo7tK6XQUFhvhImtL7PZW/mr4C4Manx/yPVvcQ=" [mod."github.com/hashicorp/go-retryablehttp"] version = "v0.7.8" hash = "sha256-4LZwKaFBbpKi9lSq5y6lOlYHU6WMnQdGNMxTd33rN80=" [mod."github.com/in-toto/attestation"] version = "v1.1.2" hash = "sha256-BdRbWCnzMCMyZmo8lkovtvGWQq2qCB7S2XBZWClJ6TM=" [mod."github.com/in-toto/in-toto-golang"] version = "v0.11.0" hash = "sha256-Rcp+UkWKBYomxPpoJqVINL26AhYNd88npXl2ryNqZ+k=" [mod."github.com/inconshreveable/mousetrap"] version = "v1.1.0" hash = "sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE=" [mod."github.com/jedisct1/go-minisign"] version = "v0.0.0-20230811132847-661be99b8267" hash = "sha256-tWufMmbfSlJRLsD1/ye5H+9b/uEQnBCQwORLJ1KwRh8=" [mod."github.com/json-iterator/go"] version = "v1.1.12" hash = "sha256-To8A0h+lbfZ/6zM+2PpRpY3+L6725OPC66lffq6fUoM=" [mod."github.com/klauspost/compress"] version = "v1.18.5" hash = "sha256-H9b5iFJf4XbEnkGQCjGQAJ3aYhVDiolKrDewTbhuzQo=" [mod."github.com/letsencrypt/boulder"] version = "v0.20260223.0" hash = "sha256-p/AuDyJr7chBqbXT+LLa3ShKX96aC3SsfzR2ekb2+xM=" [mod."github.com/mattn/go-colorable"] version = "v0.1.14" hash = "sha256-JC60PjKj7MvhZmUHTZ9p372FV72I9Mxvli3fivTbxuA=" [mod."github.com/mattn/go-isatty"] version = "v0.0.20" hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" [mod."github.com/mitchellh/go-homedir"] version = "v1.1.0" hash = "sha256-oduBKXHAQG8X6aqLEpqZHs5DOKe84u6WkBwi4W6cv3k=" [mod."github.com/moby/term"] version = "v0.5.2" hash = "sha256-/G20jUZKx36ktmPU/nEw/gX7kRTl1Dbu7zvNBYNt4xU=" [mod."github.com/modern-go/concurrent"] version = "v0.0.0-20180306012644-bacd9c7ef1dd" hash = "sha256-OTySieAgPWR4oJnlohaFTeK1tRaVp/b0d1rYY8xKMzo=" [mod."github.com/modern-go/reflect2"] version = "v1.0.3-0.20250322232337-35a7c28c31ee" hash = "sha256-0pkWWZRB3lGFyzmlxxrm0KWVQo9HNXNafaUu3k+rE1g=" [mod."github.com/munnerz/goautoneg"] version = "v0.0.0-20191010083416-a7dc8b61c822" hash = "sha256-79URDDFenmGc9JZu+5AXHToMrtTREHb3BC84b/gym9Q=" [mod."github.com/nozzle/throttler"] version = "v0.0.0-20180817012639-2ea982251481" hash = "sha256-pufLisYZW//uJXtCkobaU0Etnu+ZPQCqaRzRItx65hk=" [mod."github.com/oklog/ulid/v2"] version = "v2.1.1" hash = "sha256-kPNLaZMGwGc7ngPCivf/n4Bis219yOkGAaa6mt7+yTY=" [mod."github.com/opencontainers/go-digest"] version = "v1.0.0" hash = "sha256-cfVDjHyWItmUGZ2dzQhCHgmOmou8v7N+itDkLZVkqkQ=" [mod."github.com/opencontainers/image-spec"] version = "v1.1.1" hash = "sha256-bxBjtl+6846Ed3QHwdssOrNvlHV6b+Dn17zPISSQGP8=" [mod."github.com/pkg/browser"] version = "v0.0.0-20240102092130-5ac0b6a4141c" hash = "sha256-9iaSHHpcA1fXVF5f8RlKyo1DSoHx7eGXIC2/4LFaoBY=" [mod."github.com/pkg/errors"] version = "v0.9.1" hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw=" [mod."github.com/pmezard/go-difflib"] version = "v1.0.1-0.20181226105442-5d4384ee4fb2" hash = "sha256-XA4Oj1gdmdV/F/+8kMI+DBxKPthZ768hbKsO3d9Gx90=" [mod."github.com/prometheus/client_golang"] version = "v1.23.2" hash = "sha256-3GD4fBFa1tJu8MS4TNP6r2re2eViUE+kWUaieIOQXCg=" [mod."github.com/prometheus/client_model"] version = "v0.6.2" hash = "sha256-q6Fh6v8iNJN9ypD47LjWmx66YITa3FyRjZMRsuRTFeQ=" [mod."github.com/prometheus/common"] version = "v0.67.5" hash = "sha256-pDzmYsAANsaIf3W9HxpbgRnZ4BkPhJBBwzKq2E58FRw=" [mod."github.com/prometheus/procfs"] version = "v0.19.2" hash = "sha256-PJW21pew9v+XA7Miow8JVPct+FPIHmQHphwO+g2kNWA=" [mod."github.com/sassoftware/relic"] version = "v7.2.1+incompatible" hash = "sha256-vHyTdLRh6OlfoGzVgvx7I0+E6tpE7V43lCQaHD/e8J4=" [mod."github.com/secure-systems-lab/go-securesystemslib"] version = "v0.10.0" hash = "sha256-KY68WNnb3tgNTi0QWsmirkPfmU0xyaP23QVuSuawtHQ=" [mod."github.com/shibumi/go-pathspec"] version = "v1.3.0" hash = "sha256-ZHLft/o+xyJrUlaCwnCDqbjkPj6iIxlOuA0fFBuwVvM=" [mod."github.com/sigstore/cosign/v3"] version = "v3.0.5" hash = "sha256-wN5iAfcBCDTvhbvSar4DBw7w1sxIFWcMKv8qkx07mfo=" [mod."github.com/sigstore/protobuf-specs"] version = "v0.5.0" hash = "sha256-nImiBItjCQwskGHqYYthBjUfHHxy8VnVwSMWkK6GiNo=" [mod."github.com/sigstore/rekor"] version = "v1.5.1" hash = "sha256-4+wM/pNyOtFZM9WQDSLubScfbquWmvDRY93vHjIrf9k=" [mod."github.com/sigstore/rekor-tiles/v2"] version = "v2.2.1" hash = "sha256-mRnRvIp0UE7o5CUJiG8hs5xFJABuEAWWnjvTwz/4cKo=" [mod."github.com/sigstore/sigstore"] version = "v1.10.5" hash = "sha256-t9oup+yS4jWxEoVbYLjUrI+Hu1XlWe+bu7KVOCIf+aE=" [mod."github.com/sigstore/sigstore-go"] version = "v1.1.4" hash = "sha256-EsPVloCbJXMXOUKsNU00WQzzR2DfkbmCGYGdYgnH94I=" [mod."github.com/sigstore/timestamp-authority/v2"] version = "v2.0.6" hash = "sha256-k1LVuwm+cgCotsNxZbGI+c8jmMTI0itBvXc5TGVu27I=" [mod."github.com/sirupsen/logrus"] version = "v1.9.4" hash = "sha256-ltRvmtM3XTCAFwY0IesfRqYIivyXPPuvkFjL4ARh1wg=" [mod."github.com/spf13/afero"] version = "v1.15.0" hash = "sha256-LhcezbOqfuBzacytbqck0hNUxi6NbWNhifUc5/9uHQ8=" [mod."github.com/spf13/cobra"] version = "v1.10.2" hash = "sha256-nbRCTFiDCC2jKK7AHi79n7urYCMP5yDZnWtNVJrDi+k=" [mod."github.com/spf13/pflag"] version = "v1.0.10" hash = "sha256-uDPnWjHpSrzXr17KEYEA1yAbizfcsfo5AyztY2tS6ZU=" [mod."github.com/stoewer/go-strcase"] version = "v1.3.1" hash = "sha256-yptboRvbZtX+OEdlTgJnrCT+bvaMgCJp5B9ifEaSfw0=" [mod."github.com/syndtr/goleveldb"] version = "v1.0.1-0.20220721030215-126854af5e6d" hash = "sha256-z7HzuNVmpAJalsebJ+X7jdXq7BykcOyfzhFT8os+euM=" [mod."github.com/theupdateframework/go-tuf"] version = "v0.7.0" hash = "sha256-YwQTq6V20iI46KufNAi+1P1qrn0ldZxsFRY7dhXbO1s=" [mod."github.com/theupdateframework/go-tuf/v2"] version = "v2.4.1" hash = "sha256-v9ULpLPiK+0wBn+36zwA2ci8bX1ugO+qf3+1nd7xI4g=" [mod."github.com/titanous/rocacheck"] version = "v0.0.0-20171023193734-afe73141d399" hash = "sha256-r5XUB1A/doHNd5pu1cL0J8Jwy5IBtc8gQtG5NmKEYPU=" [mod."github.com/transparency-dev/formats"] version = "v0.0.0-20251017110053-404c0d5b696c" hash = "sha256-IaDd91Eeh6DasW5UcQaUpYobBwSNJO2nC64rySBs4wI=" [mod."github.com/transparency-dev/merkle"] version = "v0.0.2" hash = "sha256-4KsqpIqgXlypi1X88PekMRfWJ/Y8tuww6DAuXar2+FY=" [mod."github.com/vbatts/tar-split"] version = "v0.12.2" hash = "sha256-6gOHl4puCV9T2EWpFpqMCkV9N2PEPSiWbNZNp20q7iM=" [mod."github.com/x448/float16"] version = "v0.8.4" hash = "sha256-VKzMTMS9pIB/cwe17xPftCSK9Mf4Y6EuBEJlB4by5mE=" [mod."go.opentelemetry.io/auto/sdk"] version = "v1.2.1" hash = "sha256-73bFYhnxNf4SfeQ52ebnwOWywdQbqc9lWawCcSgofvE=" [mod."go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"] version = "v0.64.0" hash = "sha256-xilqHTTNmvhxDFfTnE8ZytsrYq5gYFD77/glakljCy0=" [mod."go.opentelemetry.io/otel"] version = "v1.42.0" hash = "sha256-7dck3F+khfda+U8PLSJTVETFl/M/mjXboBOsDCtF5aQ=" [mod."go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"] version = "v1.39.0" hash = "sha256-7pfSAaoIS1fbtVd9CCx6J4/DHBsmReon6r9Hocb2CCU=" [mod."go.opentelemetry.io/otel/metric"] version = "v1.42.0" hash = "sha256-OabGeuj05yLHN917j30E0QO8x8fHuZoNeotgfbzIWIA=" [mod."go.opentelemetry.io/otel/trace"] version = "v1.42.0" hash = "sha256-WowP4UqkV5BDYVlw4pivWf3DPANLgTlVbwzpED371zw=" [mod."go.uber.org/multierr"] version = "v1.11.0" hash = "sha256-Lb6rHHfR62Ozg2j2JZy3MKOMKdsfzd1IYTR57r3Mhp0=" [mod."go.uber.org/zap"] version = "v1.27.1" hash = "sha256-bn/MMu7X3GkUuW12Xwn9JYbOJeEu9+yoQtkmO+36xlQ=" [mod."go.yaml.in/yaml/v2"] version = "v2.4.3" hash = "sha256-WqfrOUQFvfuORgl1yyVOcsEXU/vwWQHkcVWx3vCxvaw=" [mod."go.yaml.in/yaml/v3"] version = "v3.0.4" hash = "sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4=" [mod."golang.org/x/crypto"] version = "v0.50.0" hash = "sha256-vC1BJT7+3UBWLyEE5n3to0NKhMo6m2HGow2HiFgpQLo=" [mod."golang.org/x/exp"] version = "v0.0.0-20260112195511-716be5621a96" hash = "sha256-rWqwXzLvvhcI/ZkOQMqCXMKI5FAuHd9YNoKTXujmboA=" [mod."golang.org/x/mod"] version = "v0.35.0" hash = "sha256-ICEQxokHywOFInDPqoP+go9l1tZSz3roknF5SXPtNV4=" [mod."golang.org/x/net"] version = "v0.53.0" hash = "sha256-G9gKLmyaf6lIV429NKX+YlL6oUPJwlv+BrG6qGhzvmU=" [mod."golang.org/x/oauth2"] version = "v0.36.0" hash = "sha256-evS7WkMrpgonmTcqtWFpC5rSKZN8O+vnAhNUs1MS9kw=" [mod."golang.org/x/sync"] version = "v0.20.0" hash = "sha256-ybcjhCfK6lroUM0yswUvWooW8MOQZBXyiSqoxG6Uy0Y=" [mod."golang.org/x/sys"] version = "v0.43.0" hash = "sha256-aDQXqSTZES2l/132PBxhZN4ywldpPyfm7LByYCHzzwM=" [mod."golang.org/x/term"] version = "v0.42.0" hash = "sha256-FCiDvAfq7dgBGQuiDYDFJbj/JPawhrmPF2qdUEftQ1c=" [mod."golang.org/x/text"] version = "v0.36.0" hash = "sha256-/0t9C6Mc8kYjxweFB0us2lGKo8GovHhBiq5nlMOppC0=" [mod."golang.org/x/time"] version = "v0.15.0" hash = "sha256-5D24A65wn7k93Jj3+918UKjB9ccmGHPBEqjD2XDB92E=" [mod."golang.org/x/tools"] version = "v0.44.0" hash = "sha256-xuj5FLtSJsAojLLTLXtPdLAIFNTKoVFbDMuqRXmj2W4=" [mod."gomodules.xyz/jsonpatch/v2"] version = "v2.5.0" hash = "sha256-L3Xy24GTtcDHmMgc9rlgUm3GrxFO7XQKJhfYIr3li1s=" [mod."google.golang.org/genproto/googleapis/api"] version = "v0.0.0-20260316180232-0b37fe3546d5" hash = "sha256-OmiDhqoKu+PLEn3hxnYqSmjPfCs6cgObHkky8ENgUOk=" [mod."google.golang.org/genproto/googleapis/rpc"] version = "v0.0.0-20260316180232-0b37fe3546d5" hash = "sha256-pnGJ+eFKWgDoT8oYp88FEOaafs+WJe3vsZHyZLSj5pU=" [mod."google.golang.org/grpc"] version = "v1.79.3" hash = "sha256-mO9ZI8ONBMEBTlrwIPeQMfb3C9ru5zPb26P+uG/pIzY=" [mod."google.golang.org/protobuf"] version = "v1.36.11" hash = "sha256-7W+6jntfI/awWL3JP6yQedxqP5S9o3XvPgJ2XxxsIeE=" [mod."gopkg.in/evanphx/json-patch.v4"] version = "v4.13.0" hash = "sha256-1iyZpBaeBLmNkJ3T4A9fAEXEYB9nk9V02ug4pwl5dy0=" [mod."gopkg.in/inf.v0"] version = "v0.9.1" hash = "sha256-z84XlyeWLcoYOvWLxPkPFgLkpjyb2Y4pdeGMyySOZQI=" [mod."gopkg.in/yaml.v2"] version = "v2.4.0" hash = "sha256-uVEGglIedjOIGZzHW4YwN1VoRSTK8o0eGZqzd+TNdd0=" [mod."gopkg.in/yaml.v3"] version = "v3.0.1" hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU=" [mod."k8s.io/api"] version = "v0.35.1" hash = "sha256-lOfx98TObjLpO9Xg+PcR7G567x5pY/iiChXFhbpRclQ=" [mod."k8s.io/apiextensions-apiserver"] version = "v0.35.0" hash = "sha256-RZdGkV4SoCTY022pIzQbeVwaxIYhIpXapLZrD593gh8=" [mod."k8s.io/apimachinery"] version = "v0.35.1" hash = "sha256-+dplbHUOfaCaD2E9IS4F3lnjSCr/a4LjTgdB9de92Pw=" [mod."k8s.io/client-go"] version = "v0.35.1" hash = "sha256-QEQ7TLUviAXDbvp2s6tT3HZtXy7pLjj5qSDu89iC9ek=" [mod."k8s.io/code-generator"] version = "v0.35.0" hash = "sha256-0F8vNVdF/quBPGxOpxBL/GhupnkMoTE8dcZYX57RZKk=" [mod."k8s.io/component-base"] version = "v0.35.0" hash = "sha256-fIAmKs3/T8oHrXBX3sMWr/D1DGhyCsgM+6ZFdaA8BXU=" [mod."k8s.io/gengo/v2"] version = "v2.0.0-20251215205346-5ee0d033ba5b" hash = "sha256-FxD4b+cOzKuXsGI4NpsaUK/YTOMxugMGAh2jY3od3p8=" [mod."k8s.io/klog/v2"] version = "v2.130.1" hash = "sha256-n5vls1o1a0V0KYv+3SULq4q3R2Is15K8iDHhFlsSH4o=" [mod."k8s.io/kube-openapi"] version = "v0.0.0-20260127142750-a19766b6e2d4" hash = "sha256-NS8NvGTX3Ycoc4JU/jwLgtNlD5OOQ5zk2hzvFFSD/jM=" [mod."k8s.io/utils"] version = "v0.0.0-20260108192941-914a6e750570" hash = "sha256-eFcd1fZT9M7wc/foeEAUj3jgHJfEY9U3lVqQTkIVJ44=" [mod."sigs.k8s.io/apiserver-network-proxy/konnectivity-client"] version = "v0.34.0" hash = "sha256-98ScvhhmVxEVAGv9rhk10ceYUadNOuBERtCcdaIb42s=" [mod."sigs.k8s.io/controller-runtime"] version = "v0.23.1" hash = "sha256-iOaYAJgy/Q1Hi6afs5mLtP8K5J8Cs/MlDoGp8wE1GOY=" [mod."sigs.k8s.io/controller-tools"] version = "v0.20.0" hash = "sha256-1/v8hCCykTDMjhx4jM84/v+WJlQ8M1whdgpGfgmSRRU=" [mod."sigs.k8s.io/json"] version = "v0.0.0-20250730193827-2d320260d730" hash = "sha256-y3vUPJYL6oxu/8c0j4vgX6fzqHtVPSCjfyuWkZYf6+I=" [mod."sigs.k8s.io/randfill"] version = "v1.0.0" hash = "sha256-xldQxDwW84hmlihdSOFfjXyauhxEWV9KmIDLZMTcYNo=" [mod."sigs.k8s.io/structured-merge-diff/v6"] version = "v6.3.2-0.20260122202528-d9cc6641c482" hash = "sha256-4ZUkeHKvdhsZmFSSZKAL/1GZdPO6735BY6FqRc+8tog=" [mod."sigs.k8s.io/yaml"] version = "v1.6.0" hash = "sha256-49hg7IVPzwxeovp+HTMiWa/10NMMTSTjAdCmIv6p9dw=" ================================================ FILE: hack/boilerplate.go.txt ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ ================================================ FILE: hack/linter-violation.tmpl ================================================ `{{violation.rule}}`: {{violation.message}} Refer to Crossplane's [coding style documentation](https://github.com/crossplane/crossplane/blob/main/CONTRIBUTING.md#coding-style-and-linting) for more information. ================================================ FILE: nix/apps.nix ================================================ # Interactive development commands for Crossplane Runtime. # # Apps run outside the Nix sandbox with full filesystem and network access. # They're designed for local development where Go modules are already available. # # All apps are builder functions that take an attrset of arguments and return a # complete app definition ({ type, meta.description, program }). Most use # writeShellApplication to create the program. The text block is preprocessed: # # ${somePkg}/bin/foo -> /nix/store/.../bin/foo (Nix store path) # ''${SOME_VAR} -> ${SOME_VAR} (shell variable, escaped) # # Each app declares its tool dependencies via runtimeInputs, with inheritPath # set to false. This ensures apps only use explicitly declared tools. { pkgs }: { # Run Go unit tests. test = _: { type = "app"; meta.description = "Run unit tests"; program = pkgs.lib.getExe ( pkgs.writeShellApplication { name = "crossplane-runtime-test"; runtimeInputs = [ pkgs.go ]; inheritPath = false; text = '' export CGO_ENABLED=0 go test ./apis/... ./pkg/... "$@" ''; } ); }; # Run golangci-lint. lint = { fix ? false, }: { type = "app"; meta.description = "Run golangci-lint" + (if fix then " with auto-fix" else ""); program = pkgs.lib.getExe ( pkgs.writeShellApplication { name = "crossplane-runtime-lint"; runtimeInputs = [ pkgs.go pkgs.golangci-lint ]; inheritPath = false; text = '' export CGO_ENABLED=0 export GOLANGCI_LINT_CACHE="''${XDG_CACHE_HOME:-$HOME/.cache}/golangci-lint" golangci-lint run ${if fix then "--fix" else ""} "$@" ''; } ); }; # Run code generation. generate = _: { type = "app"; meta.description = "Run code generation"; program = pkgs.lib.getExe ( pkgs.writeShellApplication { name = "crossplane-runtime-generate"; runtimeInputs = [ pkgs.coreutils pkgs.go pkgs.buf pkgs.protoc-gen-go pkgs.protoc-gen-go-grpc pkgs.kubernetes-controller-tools ]; inheritPath = false; text = '' export CGO_ENABLED=0 echo "Running go generate..." go generate -tags 'generate' ./... echo "Done" ''; } ); }; # Run go mod tidy and regenerate gomod2nix.toml. tidy = _: { type = "app"; meta.description = "Run go mod tidy and regenerate gomod2nix.toml"; program = pkgs.lib.getExe ( pkgs.writeShellApplication { name = "crossplane-runtime-tidy"; runtimeInputs = [ pkgs.go pkgs.gomod2nix ]; inheritPath = false; text = '' export CGO_ENABLED=0 echo "Running go mod tidy..." go mod tidy echo "Running go mod verify..." go mod verify echo "Regenerating gomod2nix.toml..." gomod2nix generate echo "Done" ''; } ); }; } ================================================ FILE: nix/checks.nix ================================================ # CI check builders for Crossplane Runtime. # # Checks run inside the Nix sandbox without network or filesystem access. This # makes them fully reproducible but means Go modules must come from gomod2nix. # # Most checks use buildGoApplication, which sets up the Go environment with # modules from gomod2nix.toml. This is different from apps, which run outside # the sandbox and can access Go modules normally. # # All checks are builder functions that take an attrset of arguments and return # a derivation. The actual check definitions live in flake.nix. { pkgs, self }: { # Run Go unit tests with coverage. test = _: pkgs.buildGoApplication { pname = "crossplane-runtime-test"; version = "0.0.0"; src = self; pwd = self; modules = ../gomod2nix.toml; CGO_ENABLED = "0"; dontBuild = true; checkPhase = '' runHook preCheck export HOME=$TMPDIR go test -covermode=count -coverprofile=coverage.txt ./apis/... ./pkg/... runHook postCheck ''; installPhase = '' mkdir -p $out cp coverage.txt $out/ ''; }; # Run golangci-lint (without --fix, since source is read-only). goLint = _: pkgs.buildGoApplication { pname = "crossplane-runtime-go-lint"; version = "0.0.0"; src = self; pwd = self; modules = ../gomod2nix.toml; CGO_ENABLED = "0"; nativeBuildInputs = [ pkgs.golangci-lint ]; dontBuild = true; checkPhase = '' runHook preCheck export HOME=$TMPDIR export GOLANGCI_LINT_CACHE=$TMPDIR/.cache/golangci-lint golangci-lint run runHook postCheck ''; installPhase = '' mkdir -p $out touch $out/.lint-passed ''; }; # Verify generated code matches committed code. generate = _: pkgs.buildGoApplication { pname = "crossplane-runtime-generate-check"; version = "0.0.0"; src = self; pwd = self; modules = ../gomod2nix.toml; CGO_ENABLED = "0"; nativeBuildInputs = [ pkgs.buf pkgs.protoc-gen-go pkgs.protoc-gen-go-grpc pkgs.kubernetes-controller-tools ]; dontBuild = true; checkPhase = '' runHook preCheck export HOME=$TMPDIR echo "Running go generate..." go generate -tags generate ./... echo "Comparing against committed source..." if ! diff -rq . ${self} --exclude=vendor > /dev/null 2>&1; then echo "ERROR: Generated code is out of date. Run 'nix run .#generate' and commit." diff -r . ${self} --exclude=vendor || true exit 1 fi runHook postCheck ''; installPhase = '' mkdir -p $out touch $out/.generate-passed ''; }; # Run Nix linters (statix, deadnix, nixfmt). nixLint = _: pkgs.runCommand "crossplane-runtime-nix-lint" { nativeBuildInputs = [ pkgs.statix pkgs.deadnix pkgs.nixfmt-rfc-style ]; } '' statix check ${self} deadnix --fail ${self}/flake.nix ${self}/nix nixfmt --check ${self}/flake.nix ${self}/nix/*.nix mkdir -p $out touch $out/.nix-lint-passed ''; } ================================================ FILE: nix.sh ================================================ #!/usr/bin/env bash # nix.sh - Run Nix commands via Docker without installing Nix locally. # # Usage: ./nix.sh # # Run './nix.sh flake show' for available apps and packages, or see flake.nix. # Examples: ./nix.sh run .#test, ./nix.sh build, ./nix.sh develop # # The first run downloads dependencies into /nix/store (cached in a Docker # volume). Subsequent runs reuse the cache. To reset: docker volume rm crossplane-nix set -e # When NIX_SH_CONTAINER is set, we're running inside the Docker container. # This script re-executes itself inside the container to avoid sh -c quoting. if [ "${NIX_SH_CONTAINER:-}" = "1" ]; then # Install tools this entrypoint script needs. It needs rsync to copy build # the build result (cp doesn't work well on MacOS volumes). Installed # packages persist across runs thanks to the crossplane-nix volume. command -v rsync &>/dev/null || nix-env -iA nixpkgs.rsync # The container runs as root, but the bind-mounted /crossplane-runtime is # owned by the host user. Git refuses to operate in directories owned by # other users. git config --global --add safe.directory /crossplane-runtime # Record the current time. After nix runs, we'll find files newer than this # marker and chown them to the host user. marker=$(mktemp) # If result (i.e. the build output) is a directory, remove it so nix build can # create its symlink. We only remove directories, not symlinks (which might be # from a host Nix install). if [ -d result ] && [ ! -L result ]; then rm -rf result fi nix "${@}" # Nix build makes result/ a symlink to a directory in the Nix store. That # directory only exists inside the container, but it creates the symlink in # /crossplane-runtime, which is shared with the host. We use this rsync trick # to make result/ a directory of regular files. if [ -L result ] && readlink result | grep -q '^/nix/store/' && [ -e result ]; then rsync -rL --chmod=u+w result/ result.tmp rm result mv result.tmp result fi # Fix ownership of any files nix created or modified. The container runs as # root, so without this, generated files would be root-owned on the host. # Using -newer is surgical - we only chown files touched during this run. find /crossplane-runtime -newer "${marker}" -exec chown "${HOST_UID}:${HOST_GID}" {} + 2>/dev/null || true rm -f "${marker}" exit 0 fi # When running on the host, launch a Docker container and re-execute this # script inside it. # Nix configuration, equivalent to /etc/nix/nix.conf. NIX_CONFIG=" # Flakes are Nix's modern project format - a flake.nix file plus a flake.lock # that pins all dependencies. This is still marked 'experimental' but is stable # and widely used. experimental-features = nix-command flakes # Build multiple derivations in parallel. A derivation is Nix's build unit, # like a Makefile target. 'auto' uses one job per CPU core. max-jobs = auto # Sandbox builds to prevent access to undeclared dependencies. Requires --privileged. sandbox = true # Cachix is a binary cache service. Our GitHub Actions CI pushes there, so if CI # has recently built the commit you're on Nix will download stuff instead of # rebuilding it locally. extra-substituters = https://crossplane.cachix.org extra-trusted-public-keys = crossplane.cachix.org-1:NJluVUN9TX0rY/zAxHYaT19Y5ik4ELH4uFuxje+62d4= " # Only allocate a TTY if stdout is a terminal. TTY mode corrupts binary output # (e.g., when piping stream-image to docker load). The -i flag keeps stdin open # for interactive commands like 'nix develop'. INTERACTIVE_FLAGS="" if [ -t 1 ]; then INTERACTIVE_FLAGS="-it" fi # Run with --privileged for sandboxed builds. docker run --rm --privileged --cgroupns=host ${INTERACTIVE_FLAGS} \ -v "$(pwd):/crossplane-runtime" \ -v "crossplane-nix:/nix" \ -w /crossplane-runtime \ -e "NIX_SH_CONTAINER=1" \ -e "NIX_CONFIG=${NIX_CONFIG}" \ -e "GOMODCACHE=/nix/go-mod-cache" \ -e "GOCACHE=/nix/go-build-cache" \ -e "HOST_UID=$(id -u)" \ -e "HOST_GID=$(id -g)" \ -e "TERM=${TERM:-xterm}" \ nixos/nix \ /crossplane-runtime/nix.sh "${@}" ================================================ FILE: pkg/certificates/certificates.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package certificates loads TLS certificates from a given folder. package certificates import ( "crypto/tls" "crypto/x509" "os" "path/filepath" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) const ( errLoadCert = "cannot load certificate" errLoadCA = "cannot load CA certificate" errInvalidCA = "invalid CA certificate" ) // LoadMTLSConfig loads TLS certificates in the given folder using well-defined filenames for certificates in a Kubernetes environment. func LoadMTLSConfig(caPath, certPath, keyPath string, isServer bool) (*tls.Config, error) { tlsCertFilePath := filepath.Clean(certPath) tlsKeyFilePath := filepath.Clean(keyPath) certificate, err := tls.LoadX509KeyPair(tlsCertFilePath, tlsKeyFilePath) if err != nil { return nil, errors.Wrap(err, errLoadCert) } caCertFilePath := filepath.Clean(caPath) ca, err := os.ReadFile(caCertFilePath) if err != nil { return nil, errors.Wrap(err, errLoadCA) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(ca) { return nil, errors.New(errInvalidCA) } tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{certificate}, } if isServer { tlsConfig.ClientCAs = pool tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } else { tlsConfig.RootCAs = pool } return tlsConfig, nil } ================================================ FILE: pkg/certificates/certificates_test.go ================================================ package certificates import ( "crypto/tls" "path/filepath" "testing" "github.com/google/go-cmp/cmp" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var ( errNoSuchFile = errors.New("open invalid/path/tls.crt: no such file or directory") errNoCAFile = errors.New("open test-data/no-ca/ca.crt: no such file or directory") ) const ( caCertFileName = "ca.crt" tlsCertFileName = "tls.crt" tlsKeyFileName = "tls.key" ) func TestLoad(t *testing.T) { type args struct { certsFolderPath string requireClientValidation bool } type want struct { err error out *tls.Config } cases := map[string]struct { reason string args want }{ "LoadCertError": { reason: "Should return a proper error if certificates do not exist.", args: args{ certsFolderPath: "invalid/path", }, want: want{ err: errors.Wrap(errNoSuchFile, errLoadCert), out: nil, }, }, "LoadCAError": { reason: "Should return a proper error if CA certificate does not exist.", args: args{ certsFolderPath: "test-data/no-ca", }, want: want{ err: errors.Wrap(errNoCAFile, errLoadCA), out: nil, }, }, "InvalidCAError": { reason: "Should return a proper error if CA certificate is not valid.", args: args{ certsFolderPath: "test-data/invalid-certs/", }, want: want{ err: errors.New(errInvalidCA), out: nil, }, }, "NoError": { reason: "Should not return an error after loading certificates.", args: args{ certsFolderPath: "test-data/certs/", }, want: want{ err: nil, out: &tls.Config{}, }, }, "NoErrorWithClientValidation": { reason: "Should not return an error after loading certificates.", args: args{ certsFolderPath: "test-data/certs/", requireClientValidation: true, }, want: want{ err: nil, out: &tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { certsFolderPath := tc.certsFolderPath requireClient := tc.requireClientValidation cfg, err := LoadMTLSConfig(filepath.Join(certsFolderPath, caCertFileName), filepath.Join(certsFolderPath, tlsCertFileName), filepath.Join(certsFolderPath, tlsKeyFileName), requireClient) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nLoad(...): -want error, +got error:\n%s", tc.reason, diff) } if requireClient { if diff := cmp.Diff(tc.out.ClientAuth, cfg.ClientAuth); diff != "" { t.Errorf("\n%s\nLoad(...): -want, +got:\n%s", tc.reason, diff) } } }) } } ================================================ FILE: pkg/certificates/test-data/certs/ca.crt ================================================ -----BEGIN CERTIFICATE----- MIIBejCCASGgAwIBAgIIOGozHYTTZu4wCgYIKoZIzj0EAwIwETEPMA0GA1UEAxMG Um9vdENBMCAXDTE5MTIyMzA4NTYzN1oYDzIxMTkxMTI5MDkwMTM3WjARMQ8wDQYD VQQDEwZSb290Q0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQmKXRMMLbjn8ur DaO/rNa8VXq32FHt7wr8+xXf0OhaCimQHxWmCHXmierP+UWs4TwZ5/NTyHZ8OOCj sSEGgA1ao2EwXzAOBgNVHQ8BAf8EBAMCAaYwHQYDVR0lBBYwFAYIKwYBBQUHAwEG CCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNQ5LeIUMgDmha6m HlW5Yte2trnyMAoGCCqGSM49BAMCA0cAMEQCIACPtB0wO8CGBjdANqnHOnREgEqu KieHeY3sYL2H+7YfAiAmfLtMe3hPdI3+sDPVZTPDe8HYFher8yWb/DCBZCT1Ww== -----END CERTIFICATE----- ================================================ FILE: pkg/certificates/test-data/certs/tls.crt ================================================ -----BEGIN CERTIFICATE----- MIIBxDCCAWmgAwIBAgIUVkhaF0okPcEJaKYKJRyTHU+aQMwwCgYIKoZIzj0EAwIw ETEPMA0GA1UEAxMGUm9vdENBMCAXDTE5MTIyMzA4NTkwMFoYDzIxMTkxMTI5MDg1 OTAwWjARMQ8wDQYDVQQDEwZjbGllbnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC AASyDjp+6zyn0W2MWtX07u3iudcahyLtTD51DzTIdplcT/bezWBWxLnP0JzzGORS f/Uf59PjMCbE66fFSNCQpcdlo4GcMIGZMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE FjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU rRNJVmij3xwiQyNfzKuhcCKnAtAwHwYDVR0jBBgwFoAU1Dkt4hQyAOaFrqYeVbli 17a2ufIwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAoGCCqGSM49BAMCA0kA MEYCIQCpZppRb5t2kjyILMnLhJ/cHKsvXpAWcO8FrDx/VBoP1wIhALtw1B73X2bj EPps3Or2UzJNxNroBNRgqIo7XkaKQRe8 -----END CERTIFICATE----- ================================================ FILE: pkg/certificates/test-data/certs/tls.key ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIDcpnLnAoOvR+q7rEKEY4zEWTicMkPaHJ1iC8lHEy9v8oAoGCCqGSM49 AwEHoUQDQgAEsg46fus8p9FtjFrV9O7t4rnXGoci7Uw+dQ80yHaZXE/23s1gVsS5 z9Cc8xjkUn/1H+fT4zAmxOunxUjQkKXHZQ== -----END EC PRIVATE KEY----- ================================================ FILE: pkg/certificates/test-data/invalid-certs/ca.crt ================================================ MIIBejCCASGgAwIBAgIIOGozHYTTZu4wCgYIKoZIzj0EAwIwETEPMA0GA1UEAxMG Um9vdENBMCAXDTE5MTIyMzA4NTYzN1oYDzIxMTkxMTI5MDkwMTM3WjARMQ8wDQYD VQQDEwZSb290Q0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQmKXRMMLbjn8ur DaO/rNa8VXq32FHt7wr8+xXf0OhaCimQHxWmCHXmierP+UWs4TwZ5/NTyHZ8OOCj sSEGgA1ao2EwXzAOBgNVHQ8BAf8EBAMCAaYwHQYDVR0lBBYwFAYIKwYBBQUHAwEG CCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNQ5LeIUMgDmha6m HlW5Yte2trnyMAoGCCqGSM49BAMCA0cAMEQCIACPtB0wO8CGBjdANqnHOnREgEqu KieHeY3sYL2H+7YfAiAmfLtMe3hPdI3+sDPVZTPDe8HYFher8yWb/DCBZCT1Ww== ================================================ FILE: pkg/certificates/test-data/invalid-certs/tls.crt ================================================ -----BEGIN CERTIFICATE----- MIIBxDCCAWmgAwIBAgIUVkhaF0okPcEJaKYKJRyTHU+aQMwwCgYIKoZIzj0EAwIw ETEPMA0GA1UEAxMGUm9vdENBMCAXDTE5MTIyMzA4NTkwMFoYDzIxMTkxMTI5MDg1 OTAwWjARMQ8wDQYDVQQDEwZjbGllbnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC AASyDjp+6zyn0W2MWtX07u3iudcahyLtTD51DzTIdplcT/bezWBWxLnP0JzzGORS f/Uf59PjMCbE66fFSNCQpcdlo4GcMIGZMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE FjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU rRNJVmij3xwiQyNfzKuhcCKnAtAwHwYDVR0jBBgwFoAU1Dkt4hQyAOaFrqYeVbli 17a2ufIwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAoGCCqGSM49BAMCA0kA MEYCIQCpZppRb5t2kjyILMnLhJ/cHKsvXpAWcO8FrDx/VBoP1wIhALtw1B73X2bj EPps3Or2UzJNxNroBNRgqIo7XkaKQRe8 -----END CERTIFICATE----- ================================================ FILE: pkg/certificates/test-data/invalid-certs/tls.key ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIDcpnLnAoOvR+q7rEKEY4zEWTicMkPaHJ1iC8lHEy9v8oAoGCCqGSM49 AwEHoUQDQgAEsg46fus8p9FtjFrV9O7t4rnXGoci7Uw+dQ80yHaZXE/23s1gVsS5 z9Cc8xjkUn/1H+fT4zAmxOunxUjQkKXHZQ== -----END EC PRIVATE KEY----- ================================================ FILE: pkg/certificates/test-data/no-ca/tls.crt ================================================ -----BEGIN CERTIFICATE----- MIIBxDCCAWmgAwIBAgIUVkhaF0okPcEJaKYKJRyTHU+aQMwwCgYIKoZIzj0EAwIw ETEPMA0GA1UEAxMGUm9vdENBMCAXDTE5MTIyMzA4NTkwMFoYDzIxMTkxMTI5MDg1 OTAwWjARMQ8wDQYDVQQDEwZjbGllbnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC AASyDjp+6zyn0W2MWtX07u3iudcahyLtTD51DzTIdplcT/bezWBWxLnP0JzzGORS f/Uf59PjMCbE66fFSNCQpcdlo4GcMIGZMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE FjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU rRNJVmij3xwiQyNfzKuhcCKnAtAwHwYDVR0jBBgwFoAU1Dkt4hQyAOaFrqYeVbli 17a2ufIwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAoGCCqGSM49BAMCA0kA MEYCIQCpZppRb5t2kjyILMnLhJ/cHKsvXpAWcO8FrDx/VBoP1wIhALtw1B73X2bj EPps3Or2UzJNxNroBNRgqIo7XkaKQRe8 -----END CERTIFICATE----- ================================================ FILE: pkg/certificates/test-data/no-ca/tls.key ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIDcpnLnAoOvR+q7rEKEY4zEWTicMkPaHJ1iC8lHEy9v8oAoGCCqGSM49 AwEHoUQDQgAEsg46fus8p9FtjFrV9O7t4rnXGoci7Uw+dQ80yHaZXE/23s1gVsS5 z9Cc8xjkUn/1H+fT4zAmxOunxUjQkKXHZQ== -----END EC PRIVATE KEY----- ================================================ FILE: pkg/conditions/manager.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package conditions enables consistent interactions with an object's status conditions. package conditions import ( xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) // ObjectWithConditions is the interface definition that allows. type ObjectWithConditions interface { resource.Object resource.Conditioned } // Manager is an interface for a stateless factory-like object that produces ConditionSet objects. type Manager interface { // For returns an implementation of a ConditionSet to operate on a specific ObjectWithConditions. For(o ObjectWithConditions) ConditionSet } // ConditionSet holds operations for interacting with an object's conditions. type ConditionSet interface { // MarkConditions adds or updates the conditions onto the managed resource object. Unlike a "Set" method, this also // can add contextual updates to the condition such as propagating the correct observedGeneration to the conditions // being changed. MarkConditions(condition ...xpv2.Condition) } // ObservedGenerationPropagationManager is the top level factor for producing a ConditionSet // on behalf of a ObjectWithConditions resource, the ConditionSet is only currently concerned with // propagating observedGeneration to conditions that are being updated. // observedGenerationPropagationManager implements Manager. type ObservedGenerationPropagationManager struct{} // For implements Manager.For. func (m ObservedGenerationPropagationManager) For(o ObjectWithConditions) ConditionSet { return &observedGenerationPropagationConditionSet{o: o} } // observedGenerationPropagationConditionSet propagates the meta.generation of the given object // to the observedGeneration of any condition being set via the `MarkConditions` method. type observedGenerationPropagationConditionSet struct { o ObjectWithConditions } // MarkConditions implements ConditionSet.MarkConditions. func (c *observedGenerationPropagationConditionSet) MarkConditions(condition ...xpv2.Condition) { if c == nil || c.o == nil { return } // Foreach condition we have been sent to mark, update the observed generation. for i := range condition { condition[i].ObservedGeneration = c.o.GetGeneration() } c.o.SetConditions(condition...) } ================================================ FILE: pkg/conditions/manager_test.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package conditions import ( "reflect" "testing" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) // Check that conditionsImpl implements ConditionManager. var _ Manager = (*ObservedGenerationPropagationManager)(nil) // Check that conditionSet implements ConditionSet. var _ ConditionSet = (*observedGenerationPropagationConditionSet)(nil) func TestOGConditionSetMark(t *testing.T) { manager := new(ObservedGenerationPropagationManager) tests := map[string]struct { reason string start []xpv2.Condition mark []xpv2.Condition want []xpv2.Condition }{ "ProvideNoConditions": { reason: "If updating a resource without conditions with no new conditions, conditions should remain empty.", start: nil, mark: nil, want: nil, }, "EmptyAppendCondition": { reason: "If starting with a resource without conditions, and we mark a condition, it should propagate to conditions with the correct generation.", start: nil, mark: []xpv2.Condition{xpv2.ReconcileSuccess()}, want: []xpv2.Condition{xpv2.ReconcileSuccess().WithObservedGeneration(42)}, }, "ExistingMarkNothing": { reason: "If the resource has a condition and we update nothing, nothing should change.", start: []xpv2.Condition{xpv2.Available().WithObservedGeneration(1)}, mark: nil, want: []xpv2.Condition{xpv2.Available().WithObservedGeneration(1)}, }, "ExistingUpdated": { reason: "If a resource starts with a condition, and we update it, we should see the observedGeneration be updated", start: []xpv2.Condition{xpv2.ReconcileSuccess().WithObservedGeneration(1)}, mark: []xpv2.Condition{xpv2.ReconcileSuccess()}, want: []xpv2.Condition{xpv2.ReconcileSuccess().WithObservedGeneration(42)}, }, "ExistingAppended": { reason: "If a resource has an existing condition and we make another condition, the new condition should merge into the conditions list.", start: []xpv2.Condition{xpv2.Available().WithObservedGeneration(1)}, mark: []xpv2.Condition{xpv2.ReconcileSuccess()}, want: []xpv2.Condition{xpv2.Available().WithObservedGeneration(1), xpv2.ReconcileSuccess().WithObservedGeneration(42)}, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { ut := newManaged(42, tt.start...) c := manager.For(ut) c.MarkConditions(tt.mark...) if diff := cmp.Diff(tt.want, ut.Conditions, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { t.Errorf("\nReason: %s\n-want, +got:\n%s", tt.reason, diff) } }) } t.Run("ManageNilObject", func(t *testing.T) { c := manager.For(nil) if c == nil { t.Errorf("manager.For(nil) = %v, want non-nil", c) } // Test that Marking on a Manager that has a nil object does not end up panicking. c.MarkConditions(xpv2.ReconcileSuccess()) // Success! }) } func TestOGManagerFor(t *testing.T) { tests := map[string]struct { reason string o ObjectWithConditions want ConditionSet }{ "NilObject": { reason: "Even if an object is nil, the manager should return a non-nil ConditionSet", want: &observedGenerationPropagationConditionSet{}, }, "Object": { reason: "Object propagates into manager.", o: &fake.Managed{}, want: &observedGenerationPropagationConditionSet{ o: &fake.Managed{}, }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { m := &ObservedGenerationPropagationManager{} if got := m.For(tt.o); !reflect.DeepEqual(got, tt.want) { t.Errorf("\nReason: %s\nFor() = %v, want %v", tt.reason, got, tt.want) } }) } } func newManaged(generation int64, conditions ...xpv2.Condition) *fake.Managed { mg := &fake.Managed{} mg.Generation = generation mg.SetConditions(conditions...) return mg } ================================================ FILE: pkg/controller/gate.go ================================================ package controller import ( "k8s.io/apimachinery/pkg/runtime/schema" ) // A Gate is an interface to allow reconcilers to delay a callback until a set of GVKs are set to true inside the gate. type Gate interface { // Register to call a callback function when all given GVKs are marked true. If the callback is unblocked, the // registration is removed. Register(callback func(), gvks ...schema.GroupVersionKind) // Set marks the associated condition to the given value. If the condition is already set as // that value, then this is a no-op. Returns true if there was an update detected. Set(gvk schema.GroupVersionKind, ready bool) bool } ================================================ FILE: pkg/controller/options.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package controller configures controller options. package controller import ( "crypto/tls" "time" "sigs.k8s.io/controller-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/v2/pkg/event" "github.com/crossplane/crossplane-runtime/v2/pkg/feature" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/v2/pkg/statemetrics" ) // DefaultOptions returns a functional set of options with conservative // defaults. func DefaultOptions() Options { return Options{ Logger: logging.NewNopLogger(), GlobalRateLimiter: ratelimiter.NewGlobal(1), PollInterval: 1 * time.Minute, MaxConcurrentReconciles: 1, Features: &feature.Flags{}, EventFilterFunctions: []event.FilterFn{}, } } // Options frequently used by most Crossplane controllers. type Options struct { // The Logger controllers should use. Logger logging.Logger // The GlobalRateLimiter used by this controller manager. The rate of // reconciles across all controllers will be subject to this limit. GlobalRateLimiter ratelimiter.RateLimiter // PollInterval at which each controller should speculatively poll to // determine whether it has work to do. PollInterval time.Duration // MaxConcurrentReconciles for each controller. MaxConcurrentReconciles int // Features that should be enabled. Features *feature.Flags // ESSOptions for External Secret Stores. ESSOptions *ESSOptions // MetricOptions for recording metrics. MetricOptions *MetricOptions // ChangeLogOptions for recording change logs. ChangeLogOptions *ChangeLogOptions // Gate implements a gated function callback pattern. Gate Gate // EventFilterFunctions used to filter events emitted by the controllers. EventFilterFunctions []event.FilterFn } // ForControllerRuntime extracts options for controller-runtime. func (o Options) ForControllerRuntime() controller.Options { recoverPanic := true return controller.Options{ MaxConcurrentReconciles: o.MaxConcurrentReconciles, RateLimiter: ratelimiter.NewController(), RecoverPanic: &recoverPanic, } } // ESSOptions for External Secret Stores. type ESSOptions struct { TLSConfig *tls.Config TLSSecretName *string } // MetricOptions for recording metrics. type MetricOptions struct { // PollStateMetricInterval at which each controller should record state PollStateMetricInterval time.Duration // MetricsRecorder to use for recording metrics. MRMetrics managed.MetricRecorder // MRStateMetrics to use for recording state metrics. MRStateMetrics *statemetrics.MRStateMetrics } // ChangeLogOptions for recording changes to managed resources into the change // logs. type ChangeLogOptions struct { ChangeLogger managed.ChangeLogger } ================================================ FILE: pkg/errors/errors.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package errors is a github.com/pkg/errors compatible API for native errors. // It includes only the subset of the github.com/pkg/errors API that is used by // the Crossplane project. package errors import ( "errors" "fmt" kerrors "k8s.io/apimachinery/pkg/util/errors" ) // New returns an error that formats as the given text. Each call to New returns // a distinct error value even if the text is identical. func New(text string) error { return errors.New(text) } // Is reports whether any error in err's chain matches target. // // The chain consists of err itself followed by the sequence of errors obtained // by repeatedly calling Unwrap. // // An error is considered to match a target if it is equal to that target or if // it implements a method Is(error) bool such that Is(target) returns true. // // An error type might provide an Is method so it can be treated as equivalent // to an existing error. For example, if MyError defines // // func (m MyError) Is(target error) bool { return target == fs.ErrExist } // // then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for // an example in the standard library. func Is(err, target error) bool { return errors.Is(err, target) } // As finds the first error in err's chain that matches target, and if so, sets // target to that error value and returns true. Otherwise, it returns false. // // The chain consists of err itself followed by the sequence of errors obtained // by repeatedly calling Unwrap. // // An error matches target if the error's concrete value is assignable to the // value pointed to by target, or if the error has a method As(any) bool // such that As(target) returns true. In the latter case, the As method is // responsible for setting target. // // An error type might provide an As method so it can be treated as if it were a // different error type. // // As panics if target is not a non-nil pointer to either a type that implements // error, or to any interface type. func As(err error, target any) bool { return errors.As(err, target) } // Unwrap returns the result of calling the Unwrap method on err, if err's type // contains an Unwrap method returning error. Otherwise, Unwrap returns nil. func Unwrap(err error) error { return errors.Unwrap(err) } // Errorf formats according to a format specifier and returns the string as a // value that satisfies error. // // If the format specifier includes a %w verb with an error operand, the // returned error will implement an Unwrap method returning the operand. It is // invalid to include more than one %w verb or to supply it with an operand that // does not implement the error interface. The %w verb is otherwise a synonym // for %v. func Errorf(format string, a ...any) error { return fmt.Errorf(format, a...) } // WithMessage annotates err with a new message. If err is nil, WithMessage // returns nil. func WithMessage(err error, message string) error { if err == nil { return nil } return fmt.Errorf("%s: %w", message, err) } // WithMessagef annotates err with the format specifier. If err is nil, // WithMessagef returns nil. func WithMessagef(err error, format string, args ...any) error { if err == nil { return nil } return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err) } // Wrap is an alias for WithMessage. func Wrap(err error, message string) error { return WithMessage(err, message) } // Wrapf is an alias for WithMessagef. func Wrapf(err error, format string, args ...any) error { return WithMessagef(err, format, args...) } // Cause calls Unwrap on each error it finds. It returns the first error it // finds that does not have an Unwrap method - i.e. the first error that was not // the result of a Wrap call, a Wrapf call, or an Errorf call with %w wrapping. func Cause(err error) error { type wrapped interface { Unwrap() error } for err != nil { w, ok := err.(wrapped) if !ok { return err } err = w.Unwrap() } return err } // MultiError is an error that wraps multiple errors. type MultiError interface { error Unwrap() []error } // Join returns an error that wraps the given errors. Any nil error values are // discarded. Join returns nil if errs contains no non-nil values. The error // formats as the concatenation of the strings obtained by calling the Error // method of each element of errs and formatting like: // // [first error, second error, third error] // // Note: aggregating errors should not be the default. Usually, return only the // first error, and only aggregate if there is clear value to the user. func Join(errs ...error) MultiError { err := kerrors.NewAggregate(errs) if err == nil { return nil } return multiError{aggregate: err} } type multiError struct { aggregate kerrors.Aggregate } func (m multiError) Error() string { return m.aggregate.Error() } func (m multiError) Unwrap() []error { return m.aggregate.Errors() } ================================================ FILE: pkg/errors/errors_test.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "testing" "github.com/google/go-cmp/cmp" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestWrap(t *testing.T) { type args struct { err error message string } cases := map[string]struct { args args want error }{ "NilError": { args: args{ err: nil, message: "very useful context", }, want: nil, }, "NonNilError": { args: args{ err: New("boom"), message: "very useful context", }, want: Errorf("very useful context: %w", New("boom")), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := Wrap(tc.args.err, tc.args.message) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("Wrap(...): -want, +got:\n%s", diff) } }) } } func TestWrapf(t *testing.T) { type args struct { err error message string args []any } cases := map[string]struct { args args want error }{ "NilError": { args: args{ err: nil, message: "very useful context", }, want: nil, }, "NonNilError": { args: args{ err: New("boom"), message: "very useful context about %s", args: []any{"ducks"}, }, want: Errorf("very useful context about %s: %w", "ducks", New("boom")), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := Wrapf(tc.args.err, tc.args.message, tc.args.args...) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("Wrapf(...): -want, +got:\n%s", diff) } }) } } func TestCause(t *testing.T) { cases := map[string]struct { err error want error }{ "NilError": { err: nil, want: nil, }, "BareError": { err: New("boom"), want: New("boom"), }, "WrappedError": { err: Wrap(Wrap(New("boom"), "interstitial context"), "very important context"), want: New("boom"), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := Cause(tc.err) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("Cause(...): -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/errors/reconcile.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "context" kerrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // SilentlyRequeueOnConflict returns a requeue result and silently drops the // error if it is a Kubernetes conflict error from the optimistic concurrency // protocol. func SilentlyRequeueOnConflict(result reconcile.Result, err error) (reconcile.Result, error) { if kerrors.IsConflict(Cause(err)) { return reconcile.Result{Requeue: true}, nil } return result, err } // WithSilentRequeueOnConflict wraps a Reconciler and silently drops conflict // errors and requeues instead. func WithSilentRequeueOnConflict(r reconcile.Reconciler) reconcile.Reconciler { return &silentlyRequeueOnConflict{Reconciler: r} } type silentlyRequeueOnConflict struct { reconcile.Reconciler } func (r *silentlyRequeueOnConflict) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { result, err := r.Reconciler.Reconcile(ctx, req) return SilentlyRequeueOnConflict(result, err) } ================================================ FILE: pkg/errors/reconcile_test.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "testing" "time" "github.com/google/go-cmp/cmp" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestSilentlyRequeueOnConflict(t *testing.T) { type args struct { result reconcile.Result err error } type want struct { result reconcile.Result err error } tests := []struct { reason string args args want want }{ { reason: "nil error", args: args{ result: reconcile.Result{RequeueAfter: time.Second}, }, want: want{ result: reconcile.Result{RequeueAfter: time.Second}, }, }, { reason: "other error", args: args{ result: reconcile.Result{RequeueAfter: time.Second}, err: New("some other error"), }, want: want{ result: reconcile.Result{RequeueAfter: time.Second}, err: New("some other error"), }, }, { reason: "conflict error", args: args{ result: reconcile.Result{RequeueAfter: time.Second}, err: kerrors.NewConflict(schema.GroupResource{Group: "nature", Resource: "stones"}, "foo", New("nested error")), }, want: want{ result: reconcile.Result{Requeue: true}, }, }, { reason: "nested conflict error", args: args{ result: reconcile.Result{RequeueAfter: time.Second}, err: Wrap( kerrors.NewConflict(schema.GroupResource{Group: "nature", Resource: "stones"}, "foo", New("nested error")), "outer error"), }, want: want{ result: reconcile.Result{Requeue: true}, }, }, } for _, tt := range tests { t.Run(tt.reason, func(t *testing.T) { got, err := SilentlyRequeueOnConflict(tt.args.result, tt.args.err) if diff := cmp.Diff(tt.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIgnoreConflict(...): -want error, +got error:\n%s", tt.reason, diff) } if diff := cmp.Diff(tt.want.result, got); diff != "" { t.Errorf("\n%s\nIgnoreConflict(...): -want result, +got result:\n%s", tt.reason, diff) } }) } } ================================================ FILE: pkg/event/event.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package event records Kubernetes events. package event import ( "maps" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" ) // A Type of event. type Type string // Event types. See below for valid types. // https://godoc.org/k8s.io/client-go/tools/record#EventRecorder const ( TypeNormal Type = "Normal" TypeWarning Type = "Warning" ) // Reason an event occurred. type Reason string // An Event relating to a Crossplane resource. type Event struct { Type Type Reason Reason Message string Annotations map[string]string } // Normal returns a normal, informational event. func Normal(r Reason, message string, keysAndValues ...string) Event { e := Event{ Type: TypeNormal, Reason: r, Message: message, Annotations: map[string]string{}, } sliceMap(keysAndValues, e.Annotations) return e } // Warning returns a warning event, typically due to an error. func Warning(r Reason, err error, keysAndValues ...string) Event { e := Event{ Type: TypeWarning, Reason: r, Message: err.Error(), Annotations: map[string]string{}, } sliceMap(keysAndValues, e.Annotations) return e } // A Recorder records Kubernetes events. type Recorder interface { Event(obj runtime.Object, e Event) WithAnnotations(keysAndValues ...string) Recorder } // An APIRecorder records Kubernetes events to an API server. type APIRecorder struct { kube record.EventRecorder annotations map[string]string filterFns []FilterFn } // FilterFn is a function used to filter events. // It should return false when events should not be sent. type FilterFn func(obj runtime.Object, e Event) bool // NewAPIRecorder returns an APIRecorder that records Kubernetes events to an // APIServer using the supplied EventRecorder. func NewAPIRecorder(r record.EventRecorder, fns ...FilterFn) *APIRecorder { return &APIRecorder{kube: r, annotations: map[string]string{}, filterFns: fns} } // Event records the supplied event. func (r *APIRecorder) Event(obj runtime.Object, e Event) { for _, filter := range r.filterFns { if filter(obj, e) { return } } r.kube.AnnotatedEventf(obj, r.annotations, string(e.Type), string(e.Reason), "%s", e.Message) } // WithAnnotations returns a new *APIRecorder that includes the supplied // annotations with all recorded events. func (r *APIRecorder) WithAnnotations(keysAndValues ...string) Recorder { ar := NewAPIRecorder(r.kube) maps.Copy(ar.annotations, r.annotations) sliceMap(keysAndValues, ar.annotations) return ar } func sliceMap(from []string, to map[string]string) { for i := 0; i+1 < len(from); i += 2 { k, v := from[i], from[i+1] to[k] = v } } // A NopRecorder does nothing. type NopRecorder struct{} // NewNopRecorder returns a Recorder that does nothing. func NewNopRecorder() *NopRecorder { return &NopRecorder{} } // Event does nothing. func (r *NopRecorder) Event(_ runtime.Object, _ Event) {} // WithAnnotations does nothing. func (r *NopRecorder) WithAnnotations(_ ...string) Recorder { return r } ================================================ FILE: pkg/event/event_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package event import ( "testing" "github.com/google/go-cmp/cmp" ) func TestSliceMap(t *testing.T) { type args struct { from []string to map[string]string } cases := map[string]struct { reason string args args want map[string]string }{ "OnePair": { reason: "One key value pair should be added.", args: args{ from: []string{"key", "val"}, to: map[string]string{}, }, want: map[string]string{"key": "val"}, }, "TwoPairs": { reason: "Two key value pairs should be added.", args: args{ from: []string{ "key", "val", "another", "value", }, to: map[string]string{}, }, want: map[string]string{ "key": "val", "another": "value", }, }, "NoValue": { reason: "Two key value pairs should be added.", args: args{ from: []string{"key"}, to: map[string]string{}, }, want: map[string]string{}, }, "ExtraneousKey": { reason: "One key value pair should be added.", args: args{ from: []string{ "key", "val", "extraneous", }, to: map[string]string{}, }, want: map[string]string{"key": "val"}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { sliceMap(tc.args.from, tc.args.to) if diff := cmp.Diff(tc.want, tc.args.to); diff != "" { t.Errorf("%s\nsliceMap(...): -want, +got:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/feature/feature.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package feature contains utilities for managing Crossplane features. package feature import ( "sync" ) // A Flag enables a particular feature. type Flag string // Flags that are enabled. The zero value - i.e. &feature.Flags{} - is usable. type Flags struct { m sync.RWMutex enabled map[Flag]bool } // Enable a feature flag. func (fs *Flags) Enable(f Flag) { fs.m.Lock() if fs.enabled == nil { fs.enabled = make(map[Flag]bool) } fs.enabled[f] = true fs.m.Unlock() } // Enabled returns true if the supplied feature flag is enabled. func (fs *Flags) Enabled(f Flag) bool { if fs == nil { return false } fs.m.RLock() defer fs.m.RUnlock() return fs.enabled[f] } ================================================ FILE: pkg/feature/feature_test.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package feature import ( "testing" "github.com/google/go-cmp/cmp" ) func TestEnable(t *testing.T) { var cool Flag = "cool" t.Run("EnableMutatesZeroValue", func(t *testing.T) { f := &Flags{} f.Enable(cool) want := true got := f.Enabled(cool) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("f.Enabled(...): -want, +got:\n%s", diff) } }) t.Run("EnabledOnEmptyFlagsReturnsFalse", func(t *testing.T) { f := &Flags{} want := false got := f.Enabled(cool) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("f.Enabled(...): -want, +got:\n%s", diff) } }) t.Run("EnabledOnNilReturnsFalse", func(t *testing.T) { var f *Flags want := false got := f.Enabled(cool) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("f.Enabled(...): -want, +got:\n%s", diff) } }) } ================================================ FILE: pkg/feature/features.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package feature // EnableBetaManagementPolicies enables beta support for // Management Policies. See the below design for more details. // https://github.com/crossplane/crossplane/pull/3531 const EnableBetaManagementPolicies Flag = "EnableBetaManagementPolicies" // EnableAlphaChangeLogs enables alpha support for capturing change logs during // reconciliation. See the following design for more details: // https://github.com/crossplane/crossplane/pull/5822 const EnableAlphaChangeLogs Flag = "EnableAlphaChangeLogs" ================================================ FILE: pkg/fieldpath/fieldpath.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package fieldpath provides utilities for working with field paths. // // Field paths reference a field within a Kubernetes object via a simple string. // API conventions describe the syntax as "standard JavaScript syntax for // accessing that field, assuming the JSON object was transformed into a // JavaScript object, without the leading dot, such as metadata.name". // // Valid examples: // // * metadata.name // * spec.containers[0].name // * data[.config.yml] // * metadata.annotations['crossplane.io/external-name'] // * spec.items[0][8] // * apiVersion // * [42] // // Invalid examples: // // * .metadata.name - Leading period. // * metadata..name - Double period. // * metadata.name. - Trailing period. // * spec.containers[] - Empty brackets. // * spec.containers.[0].name - Period before open bracket. // // https://github.com/kubernetes/community/blob/61f3d0/contributors/devel/sig-architecture/api-conventions.md#selecting-fields package fieldpath import ( "fmt" "strconv" "strings" "unicode/utf8" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) // A SegmentType within a field path; either a field within an object, or an // index within an array. type SegmentType int // Segment types. const ( _ SegmentType = iota SegmentField SegmentIndex ) // A Segment of a field path. type Segment struct { Type SegmentType Field string Index uint } // Segments of a field path. type Segments []Segment func (sg Segments) String() string { var b strings.Builder for _, s := range sg { switch s.Type { case SegmentField: if s.Field == wildcard || strings.ContainsRune(s.Field, period) { b.WriteString(fmt.Sprintf("[%s]", s.Field)) continue } b.WriteString(fmt.Sprintf(".%s", s.Field)) case SegmentIndex: b.WriteString(fmt.Sprintf("[%d]", s.Index)) } } return strings.TrimPrefix(b.String(), ".") } // FieldOrIndex produces a new segment from the supplied string. The segment is // considered to be an array index if the string can be interpreted as an // unsigned 32 bit integer. Anything else is interpreted as an object field // name. func FieldOrIndex(s string) Segment { // Attempt to parse the segment as an unsigned integer. If the integer is // larger than 2^32 (the limit for most JSON arrays) we presume it's too big // to be an array index, and is thus a field name. if i, err := strconv.ParseUint(s, 10, 32); err == nil { return Segment{Type: SegmentIndex, Index: uint(i)} } // If the segment is not a valid unsigned integer we presume it's // a string field name. return Field(s) } // Field produces a new segment from the supplied string. The segment is always // considered to be an object field name. func Field(s string) Segment { return Segment{Type: SegmentField, Field: strings.Trim(s, "'\"")} } // Parse the supplied path into a slice of Segments. func Parse(path string) (Segments, error) { l := &lexer{input: path, items: make(chan item)} go l.run() segments := make(Segments, 0, 1) for i := range l.items { switch i.typ { //nolint:exhaustive // We're only worried about names, not separators. case itemField: segments = append(segments, Field(i.val)) case itemFieldOrIndex: segments = append(segments, FieldOrIndex(i.val)) case itemError: return nil, errors.Errorf("%s at position %d", i.val, i.pos) } } return segments, nil } const ( period = '.' leftBracket = '[' rightBracket = ']' wildcard = "*" ) type itemType int const ( itemError itemType = iota itemPeriod itemLeftBracket itemRightBracket itemField itemFieldOrIndex itemEOL ) type item struct { typ itemType pos int val string } type stateFn func(*lexer) stateFn // A simplified version of the text/template lexer. // https://github.com/golang/go/blob/6396bc9d/src/text/template/parse/lex.go#L108 type lexer struct { input string pos int start int items chan item } func (l *lexer) run() { for state := lexField; state != nil; { state = state(l) } close(l.items) } func (l *lexer) emit(t itemType) { // Don't emit empty values. if l.pos <= l.start { return } l.items <- item{typ: t, pos: l.start, val: l.input[l.start:l.pos]} l.start = l.pos } func (l *lexer) errorf(pos int, format string, args ...any) stateFn { l.items <- item{typ: itemError, pos: pos, val: fmt.Sprintf(format, args...)} return nil } func lexField(l *lexer) stateFn { for i, r := range l.input[l.pos:] { switch r { // A right bracket may not appear in an object field name. case rightBracket: return l.errorf(l.pos+i, "unexpected %q", rightBracket) // A left bracket indicates the end of the field name. case leftBracket: l.pos += i l.emit(itemField) return lexLeftBracket // A period indicates the end of the field name. case period: l.pos += i l.emit(itemField) return lexPeriod } } // The end of the input indicates the end of the field name. l.pos = len(l.input) l.emit(itemField) l.emit(itemEOL) return nil } func lexPeriod(l *lexer) stateFn { // A period may not appear at the beginning or the end of the input. if l.pos == 0 || l.pos == len(l.input)-1 { return l.errorf(l.pos, "unexpected %q", period) } l.pos += utf8.RuneLen(period) l.emit(itemPeriod) // A period may only be followed by a field name. We defer checking for // right brackets to lexField, where they are invalid. r, _ := utf8.DecodeRuneInString(l.input[l.pos:]) if r == period { return l.errorf(l.pos, "unexpected %q", period) } if r == leftBracket { return l.errorf(l.pos, "unexpected %q", leftBracket) } return lexField } func lexLeftBracket(l *lexer) stateFn { // A right bracket must appear before the input ends. if !strings.ContainsRune(l.input[l.pos:], rightBracket) { return l.errorf(l.pos, "unterminated %q", leftBracket) } l.pos += utf8.RuneLen(leftBracket) l.emit(itemLeftBracket) return lexFieldOrIndex } // Strings between brackets may be either a field name or an array index. // Periods have no special meaning in this context. func lexFieldOrIndex(l *lexer) stateFn { // We know a right bracket exists before EOL thanks to the preceding // lexLeftBracket. rbi := strings.IndexRune(l.input[l.pos:], rightBracket) // A right bracket may not immediately follow a left bracket. if rbi == 0 { return l.errorf(l.pos, "unexpected %q", rightBracket) } // A left bracket may not appear before the next right bracket. if lbi := strings.IndexRune(l.input[l.pos:l.pos+rbi], leftBracket); lbi > -1 { return l.errorf(l.pos+lbi, "unexpected %q", leftBracket) } // Periods are not considered field separators when we're inside brackets. l.pos += rbi l.emit(itemFieldOrIndex) return lexRightBracket } func lexRightBracket(l *lexer) stateFn { l.pos += utf8.RuneLen(rightBracket) l.emit(itemRightBracket) return lexField } ================================================ FILE: pkg/fieldpath/fieldpath_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fieldpath import ( "math" "strconv" "testing" "github.com/google/go-cmp/cmp" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestSegments(t *testing.T) { cases := map[string]struct { s Segments want string }{ "SingleField": { s: Segments{Field("spec")}, want: "spec", }, "SingleIndex": { s: Segments{FieldOrIndex("0")}, want: "[0]", }, "FieldsAndIndex": { s: Segments{ Field("spec"), Field("containers"), FieldOrIndex("0"), Field("name"), }, want: "spec.containers[0].name", }, "PeriodsInField": { s: Segments{ Field("data"), Field(".config.yml"), }, want: "data[.config.yml]", }, "Wildcard": { s: Segments{ Field("spec"), Field("containers"), FieldOrIndex("*"), Field("name"), }, want: "spec.containers[*].name", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { if diff := cmp.Diff(tc.want, tc.s.String()); diff != "" { t.Errorf("s.String(): -want, +got:\n %s", diff) } }) } } func TestFieldOrIndex(t *testing.T) { cases := map[string]struct { reason string s string want Segment }{ "Field": { reason: "An unambiguous string should be interpreted as a field segment", s: "coolField", want: Segment{Type: SegmentField, Field: "coolField"}, }, "QuotedField": { reason: "A quoted string should be interpreted as a field segment with the quotes removed", s: "'coolField'", want: Segment{Type: SegmentField, Field: "coolField"}, }, "QuotedFieldWithPeriods": { reason: "A quoted string with periods should be interpreted as a field segment with the quotes removed", s: "'cool.Field'", want: Segment{Type: SegmentField, Field: "cool.Field"}, }, "Index": { reason: "An unambiguous integer should be interpreted as an index segment", s: "3", want: Segment{Type: SegmentIndex, Index: 3}, }, "Negative": { reason: "A negative integer should be interpreted as an field segment", s: "-3", want: Segment{Type: SegmentField, Field: "-3"}, }, "Float": { reason: "A float should be interpreted as an field segment", s: "3.0", want: Segment{Type: SegmentField, Field: "3.0"}, }, "Overflow": { reason: "A very big integer will be interpreted as a field segment", s: strconv.Itoa(math.MaxUint32 + 1), want: Segment{Type: SegmentField, Field: strconv.Itoa(math.MaxUint32 + 1)}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := FieldOrIndex(tc.s) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nFieldOrIndex(...): %s: -want, +got:\n%s", tc.reason, diff) } }) } } func TestParse(t *testing.T) { type want struct { s Segments err error } cases := map[string]struct { reason string path string want want }{ "SingleField": { reason: "A path with no brackets or periods should be interpreted as a single field segment", path: "spec", want: want{ s: Segments{Field("spec")}, }, }, "SingleIndex": { reason: "An integer surrounded by brackets should be interpreted as an index", path: "[0]", want: want{ s: Segments{FieldOrIndex("0")}, }, }, "TwoFields": { reason: "A path with one period should be interpreted as two field segments", path: "metadata.name", want: want{ s: Segments{Field("metadata"), Field("name")}, }, }, "APIConventionsExample": { reason: "The example given by the Kubernetes API convention should be parse correctly", path: "fields[1].state.current", want: want{ s: Segments{ Field("fields"), FieldOrIndex("1"), Field("state"), Field("current"), }, }, }, "SimpleIndex": { reason: "Indexing an object field that is an array should result in a field and an index", path: "items[0]", want: want{ s: Segments{Field("items"), FieldOrIndex("0")}, }, }, "FieldsAndIndex": { reason: "A path with periods and braces should be interpreted as fields and indices", path: "spec.containers[0].name", want: want{ s: Segments{ Field("spec"), Field("containers"), FieldOrIndex("0"), Field("name"), }, }, }, "NestedArray": { reason: "A nested array should result in two consecutive index fields", path: "nested[0][1].name", want: want{ s: Segments{ Field("nested"), FieldOrIndex("0"), FieldOrIndex("1"), Field("name"), }, }, }, "BracketStyleField": { reason: "A field name can be specified using brackets rather than a period", path: "spec[containers][0].name", want: want{ s: Segments{ Field("spec"), Field("containers"), FieldOrIndex("0"), Field("name"), }, }, }, "BracketFieldWithPeriod": { reason: "A field name specified using brackets can include a period", path: "data[.config.yml]", want: want{ s: Segments{ Field("data"), FieldOrIndex(".config.yml"), }, }, }, "QuotedFieldWithPeriodInBracket": { reason: "A field name specified using quote and in bracket can include a period", path: "metadata.labels['app.hash']", want: want{ s: Segments{ Field("metadata"), Field("labels"), FieldOrIndex("app.hash"), }, }, }, "LeadingPeriod": { reason: "A path may not start with a period (unlike a JSON path)", path: ".metadata.name", want: want{ err: errors.New("unexpected '.' at position 0"), }, }, "TrailingPeriod": { reason: "A path may not end with a period", path: "metadata.name.", want: want{ err: errors.New("unexpected '.' at position 13"), }, }, "BracketsFollowingPeriod": { reason: "Brackets may not follow a period", path: "spec.containers.[0].name", want: want{ err: errors.New("unexpected '[' at position 16"), }, }, "DoublePeriod": { reason: "A path may not include two consecutive periods", path: "metadata..name", want: want{ err: errors.New("unexpected '.' at position 9"), }, }, "DanglingRightBracket": { reason: "A right bracket may not appear in a field name", path: "metadata.]name", want: want{ err: errors.New("unexpected ']' at position 9"), }, }, "DoubleOpenBracket": { reason: "Brackets may not be nested", path: "spec[bracketed[name]]", want: want{ err: errors.New("unexpected '[' at position 14"), }, }, "DanglingLeftBracket": { reason: "A left bracket must be closed", path: "spec[name", want: want{ err: errors.New("unterminated '[' at position 4"), }, }, "EmptyBracket": { reason: "Brackets may not be empty", path: "spec[]", want: want{ err: errors.New("unexpected ']' at position 5"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := Parse(tc.path) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\nParse(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.s, got); diff != "" { t.Errorf("\nParse(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) } }) } } ================================================ FILE: pkg/fieldpath/merge.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fieldpath import ( "reflect" "dario.cat/mergo" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) const ( errInvalidMerge = "failed to merge values" ) // MergeValue of the receiver p at the specified field path with the supplied // value according to supplied merge options. func (p *Paved) MergeValue(path string, value any, mo *MergeOptions) error { dst, err := p.GetValue(path) if IsNotFound(err) || mo == nil { dst = nil } else if err != nil { return err } dst, err = merge(dst, value, mo) if err != nil { return err } return p.SetValue(path, dst) } // merges the given src onto the given dst. // dst and src must have the same type. // If a nil merge options is supplied, the default behavior is MergeOptions' // default behavior. If dst or src is nil, src is returned // (i.e., dst replaced by src). func merge(dst, src any, mergeOptions *MergeOptions) (any, error) { // because we are merging values of a field, which can be a slice, and // because mergo currently supports merging only maps or structs, // we wrap the argument to be passed to mergo.Merge in a map. const keyArg = "arg" argWrap := func(arg any) map[string]any { return map[string]any{ keyArg: arg, } } if dst == nil || src == nil { return src, nil // no merge, replace } // TODO(aru): we may provide an extra MergeOption to also append duplicates of slice elements // but, by default, do not append duplicate slice items if MergeOptions.AppendSlice is set if mergeOptions.isAppendSlice() { src = removeSourceDuplicates(dst, src) } mDst := argWrap(dst) // use merge semantics with the configured merge options to obtain the target dst value if err := mergo.Merge(&mDst, argWrap(src), mergeOptions.mergoConfiguration()...); err != nil { return nil, errors.Wrap(err, errInvalidMerge) } return mDst[keyArg], nil } func removeSourceDuplicates(dst, src any) any { sliceDst, sliceSrc := reflect.ValueOf(dst), reflect.ValueOf(src) if sliceDst.Kind() == reflect.Ptr { sliceDst = sliceDst.Elem() } if sliceSrc.Kind() == reflect.Ptr { sliceSrc = sliceSrc.Elem() } if sliceDst.Kind() != reflect.Slice || sliceSrc.Kind() != reflect.Slice { return src } result := reflect.New(sliceSrc.Type()).Elem() // we will not modify src for i := range sliceSrc.Len() { itemSrc := sliceSrc.Index(i) found := false for j := 0; j < sliceDst.Len() && !found; j++ { // if src item is found in the dst array if reflect.DeepEqual(itemSrc.Interface(), sliceDst.Index(j).Interface()) { found = true } } if !found { // then put src item into result result = reflect.Append(result, itemSrc) } } return result.Interface() } // MergeOptions Specifies merge options on a field path. type MergeOptions struct { // TODO(aru): add more options that control merging behavior // Specifies that already existing values in a merged map should be preserved // +optional KeepMapValues *bool `json:"keepMapValues,omitempty"` // Specifies that already existing elements in a merged slice should be preserved // +optional AppendSlice *bool `json:"appendSlice,omitempty"` } // mergoConfiguration the default behavior is to replace maps and slices. func (mo *MergeOptions) mergoConfiguration() []func(*mergo.Config) { config := []func(*mergo.Config){mergo.WithOverride} if mo == nil { return config } if mo.KeepMapValues != nil && *mo.KeepMapValues { config = config[:0] } if mo.AppendSlice != nil && *mo.AppendSlice { config = append(config, mergo.WithAppendSlice) } return config } // isAppendSlice returns true if mo.AppendSlice is set to true. func (mo *MergeOptions) isAppendSlice() bool { return mo != nil && mo.AppendSlice != nil && *mo.AppendSlice } ================================================ FILE: pkg/fieldpath/merge_test.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fieldpath import ( "fmt" "reflect" "strings" "testing" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/util/json" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestMergeValue(t *testing.T) { const ( pathTest = "a" valSrc = "e1-from-source" valSrc2 = "e1-from-source-2" valDst = "e1-from-destination" ) formatArr := func(arr []string) string { return fmt.Sprintf(`{"%s": ["%s"]}`, pathTest, strings.Join(arr, `", "`)) } formatMap := func(val string) string { return fmt.Sprintf(`{"%s": {"%s": "%s"}}`, pathTest, pathTest, val) } appendArr := func(dst, src []string) []string { return reflect.AppendSlice(reflect.ValueOf(dst), reflect.ValueOf(src)).Interface().([]string) } arrSrc := []string{valSrc} fnMapSrc := func() map[string]any { return map[string]any{pathTest: valSrc} } arrDst := []string{valDst} fnMapDst := func() map[string]any { return map[string]any{pathTest: map[string]any{pathTest: valDst}} } valFalse, valTrue := false, true type fields struct { object map[string]any } type args struct { path string value any mo *MergeOptions } type want struct { serialized string err error } tests := map[string]struct { reason string fields fields args args want want }{ "MergeArrayNoMergeOptions": { reason: "If no merge options are given, default is to override an array", fields: fields{ object: map[string]any{ pathTest: valDst, }, }, args: args{ path: pathTest, value: arrSrc, }, want: want{ serialized: formatArr(arrSrc), }, }, "MergeArrayNoAppend": { reason: "If MergeOptions.AppendSlice is false, an array should be overridden when merging", fields: fields{ object: map[string]any{ pathTest: arrDst, }, }, args: args{ path: pathTest, value: arrSrc, mo: &MergeOptions{ AppendSlice: &valFalse, }, }, want: want{ serialized: formatArr(arrSrc), }, }, "MergeArrayAppend": { reason: "If MergeOptions.AppendSlice is true, dst array should be merged with the src array", fields: fields{ object: map[string]any{ pathTest: arrDst, }, }, args: args{ path: pathTest, value: arrSrc, mo: &MergeOptions{ AppendSlice: &valTrue, }, }, want: want{ serialized: formatArr(appendArr(arrDst, arrSrc)), }, }, "MergeArrayAppendDuplicate": { reason: "If MergeOptions.AppendSlice is true, dst array should be merged with the src array not allowing duplicates", fields: fields{ object: map[string]any{ pathTest: []string{valDst, valSrc}, }, }, args: args{ path: pathTest, value: []string{valSrc, valSrc2}, mo: &MergeOptions{ AppendSlice: &valTrue, }, }, want: want{ serialized: formatArr([]string{valDst, valSrc, valSrc2}), }, }, "MergeMapNoMergeOptions": { reason: "If no merge options are given, default is to override a map key", fields: fields{ object: fnMapDst(), }, args: args{ path: pathTest, value: fnMapSrc(), }, want: want{ serialized: formatMap(valSrc), }, }, "MergeMapNoKeep": { reason: "If MergeOptions.KeepMapValues is false, a map key should be overridden", fields: fields{ object: fnMapDst(), }, args: args{ path: pathTest, value: fnMapSrc(), mo: &MergeOptions{ KeepMapValues: &valFalse, }, }, want: want{ serialized: formatMap(valSrc), }, }, "MergeMapKeep": { reason: "If MergeOptions.KeepMapValues is true, a dst map key should preserve its value", fields: fields{ object: fnMapDst(), }, args: args{ path: pathTest, value: fnMapSrc(), mo: &MergeOptions{ KeepMapValues: &valTrue, }, }, want: want{ serialized: formatMap(valDst), }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { want := make(map[string]any) if err := json.Unmarshal([]byte(tc.want.serialized), &want); err != nil { t.Fatalf("Test case error: Unable to unmarshall JSON doc: %v", err) } p := &Paved{ object: tc.fields.object, } err := p.MergeValue(tc.args.path, tc.args.value, tc.args.mo) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.MergeValue(%s, %v): %s: -want error, +got error:\n%s", tc.args.path, tc.args.value, tc.reason, diff) } if diff := cmp.Diff(want, p.object); diff != "" { t.Fatalf("\np.MergeValue(%s, %v): %s: -want, +got:\n%s", tc.args.path, tc.args.value, tc.reason, diff) } }) } } ================================================ FILE: pkg/fieldpath/paved.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fieldpath import ( "strconv" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/json" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) // DefaultMaxFieldPathIndex is the max allowed index in a field path. const DefaultMaxFieldPathIndex = 1024 type notFoundError struct { error } func (e notFoundError) IsNotFound() bool { return true } // IsNotFound returns true if the supplied error indicates a field path was not // found, for example because a field did not exist within an object or an // index was out of bounds in an array. func IsNotFound(err error) bool { cause := errors.Cause(err) _, ok := cause.(interface { IsNotFound() bool }) return ok } // A Paved JSON object supports getting and setting values by their field path. type Paved struct { object map[string]any maxFieldPathIndex uint } // PavedOption can be used to configure a Paved behavior. type PavedOption func(paved *Paved) // PaveObject paves a runtime.Object, making it possible to get and set values // by field path. o must be a non-nil pointer to an object. func PaveObject(o runtime.Object, opts ...PavedOption) (*Paved, error) { u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o) return Pave(u, opts...), errors.Wrap(err, "cannot convert object to unstructured data") } // Pave a JSON object, making it possible to get and set values by field path. func Pave(object map[string]any, opts ...PavedOption) *Paved { p := &Paved{object: object, maxFieldPathIndex: DefaultMaxFieldPathIndex} for _, opt := range opts { opt(p) } return p } // WithMaxFieldPathIndex returns a PavedOption that sets the max allowed index for field paths, 0 means no limit. func WithMaxFieldPathIndex(maxIndex uint) PavedOption { return func(paved *Paved) { paved.maxFieldPathIndex = maxIndex } } func (p *Paved) maxFieldPathIndexEnabled() bool { return p.maxFieldPathIndex > 0 } // MarshalJSON to the underlying object. func (p Paved) MarshalJSON() ([]byte, error) { return json.Marshal(p.object) } // UnmarshalJSON from the underlying object. func (p *Paved) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &p.object) } // UnstructuredContent returns the JSON serialisable content of this Paved. func (p *Paved) UnstructuredContent() map[string]any { if p.object == nil { return make(map[string]any) } return p.object } // SetUnstructuredContent sets the JSON serialisable content of this Paved. func (p *Paved) SetUnstructuredContent(content map[string]any) { p.object = content } func (p *Paved) getValue(s Segments) (any, error) { return getValueFromInterface(p.object, s) } func getValueFromInterface(it any, s Segments) (any, error) { for i, current := range s { final := i == len(s)-1 switch current.Type { case SegmentIndex: array, ok := it.([]any) if !ok { return nil, errors.Errorf("%s: not an array", s[:i]) } if current.Index >= uint(len(array)) { return nil, notFoundError{errors.Errorf("%s: no such element", s[:i+1])} } if final { return array[current.Index], nil } it = array[current.Index] case SegmentField: switch object := it.(type) { case map[string]any: v, ok := object[current.Field] if !ok { return nil, notFoundError{errors.Errorf("%s: no such field", s[:i+1])} } if final { return v, nil } it = object[current.Field] case nil: return nil, notFoundError{errors.Errorf("%s: expected map, got nil", s[:i])} default: return nil, errors.Errorf("%s: not an object", s[:i]) } } } // This should be unreachable. return nil, nil } // ExpandWildcards expands wildcards for a given field path. It returns an // array of field paths with expanded values. Please note that expanded paths // depend on the input data which is paved.object. // // Example: // // For a Paved object with the following data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now", "debug"]}]}}`), // ExpandWildcards("spec.containers[*].args[*]") returns: // []string{"spec.containers[0].args[0]", "spec.containers[0].args[1]", "spec.containers[0].args[2]"},. func (p *Paved) ExpandWildcards(path string) ([]string, error) { segments, err := Parse(path) if err != nil { return nil, errors.Wrapf(err, "cannot parse path %q", path) } segmentsArray, err := expandWildcards(p.object, segments) if err != nil { return nil, errors.Wrapf(err, "cannot expand wildcards for segments: %q", segments) } paths := make([]string, len(segmentsArray)) for i, s := range segmentsArray { paths[i] = s.String() } return paths, nil } func expandWildcards(data any, segments Segments) ([]Segments, error) { //nolint:gocognit // See note below. // Even complexity turns out to be high, it is mostly because we have duplicate // logic for arrays and maps and a couple of error handling. var res []Segments it := data for i, current := range segments { // wildcards are regular fields with "*" as string if current.Type == SegmentField && current.Field == wildcard { switch mapOrArray := it.(type) { case []any: for ix := range mapOrArray { expanded := make(Segments, len(segments)) copy(expanded, segments) expanded = append(append(expanded[:i], FieldOrIndex(strconv.Itoa(ix))), expanded[i+1:]...) r, err := expandWildcards(data, expanded) if err != nil { return nil, errors.Wrapf(err, "%q: cannot expand wildcards", expanded) } res = append(res, r...) } case map[string]any: for k := range mapOrArray { expanded := make(Segments, len(segments)) copy(expanded, segments) expanded = append(append(expanded[:i], Field(k)), expanded[i+1:]...) r, err := expandWildcards(data, expanded) if err != nil { return nil, errors.Wrapf(err, "%q: cannot expand wildcards", expanded) } res = append(res, r...) } case nil: return nil, notFoundError{errors.Errorf("wildcard field %q is not found in the path", segments[:i])} default: return nil, errors.Errorf("%q: unexpected wildcard usage", segments[:i]) } return res, nil } var err error it, err = getValueFromInterface(data, segments[:i+1]) if IsNotFound(err) { return nil, nil } if err != nil { return nil, err } } return append(res, segments), nil } // GetValue of the supplied field path. func (p *Paved) GetValue(path string) (any, error) { segments, err := Parse(path) if err != nil { return nil, errors.Wrapf(err, "cannot parse path %q", path) } return p.getValue(segments) } // GetValueInto the supplied type. func (p *Paved) GetValueInto(path string, out any) error { val, err := p.GetValue(path) if err != nil { return err } js, err := json.Marshal(val) if err != nil { return errors.Wrap(err, "cannot marshal value to JSON") } return errors.Wrap(json.Unmarshal(js, out), "cannot unmarshal value from JSON") } // GetString value of the supplied field path. func (p *Paved) GetString(path string) (string, error) { v, err := p.GetValue(path) if err != nil { return "", err } s, ok := v.(string) if !ok { return "", errors.Errorf("%s: not a string", path) } return s, nil } // GetStringArray value of the supplied field path. func (p *Paved) GetStringArray(path string) ([]string, error) { v, err := p.GetValue(path) if err != nil { return nil, err } a, ok := v.([]any) if !ok { return nil, errors.Errorf("%s: not an array", path) } sa := make([]string, len(a)) for i := range a { s, ok := a[i].(string) if !ok { return nil, errors.Errorf("%s: not an array of strings", path) } sa[i] = s } return sa, nil } // GetStringObject value of the supplied field path. func (p *Paved) GetStringObject(path string) (map[string]string, error) { v, err := p.GetValue(path) if err != nil { return nil, err } o, ok := v.(map[string]any) if !ok { return nil, errors.Errorf("%s: not an object", path) } so := make(map[string]string) for k, in := range o { s, ok := in.(string) if !ok { return nil, errors.Errorf("%s: not an object with string field values", path) } so[k] = s } return so, nil } // GetBool value of the supplied field path. func (p *Paved) GetBool(path string) (bool, error) { v, err := p.GetValue(path) if err != nil { return false, err } b, ok := v.(bool) if !ok { return false, errors.Errorf("%s: not a bool", path) } return b, nil } // GetInteger value of the supplied field path. func (p *Paved) GetInteger(path string) (int64, error) { v, err := p.GetValue(path) if err != nil { return 0, err } f, ok := v.(int64) if !ok { return 0, errors.Errorf("%s: not a (int64) number", path) } return f, nil } func (p *Paved) setValue(s Segments, value any) error { // We expect p.object to look like JSON data that was unmarshalled into an // any per https://golang.org/pkg/encoding/json/#Unmarshal. We // marshal our value to JSON and unmarshal it into an any to ensure // it meets these criteria before setting it within p.object. v, err := toValidJSON(value) if err != nil { return err } if err := p.validateSegments(s); err != nil { return err } var in any = p.object for i, current := range s { final := i == len(s)-1 switch current.Type { case SegmentIndex: array, ok := in.([]any) if !ok { return errors.Errorf("%s is not an array", s[:i]) } if final { array[current.Index] = v return nil } prepareElement(array, current, s[i+1]) in = array[current.Index] case SegmentField: object, ok := in.(map[string]any) if !ok { return errors.Errorf("%s is not an object", s[:i]) } if final { object[current.Field] = v return nil } prepareField(object, current, s[i+1]) in = object[current.Field] } } return nil } func toValidJSON(value any) (any, error) { var v any j, err := json.Marshal(value) if err != nil { return nil, errors.Wrap(err, "cannot marshal value to JSON") } if err := json.Unmarshal(j, &v); err != nil { return nil, errors.Wrap(err, "cannot unmarshal value from JSON") } return v, nil } func prepareElement(array []any, current, next Segment) { // If this segment is not the final one and doesn't exist we need to // create it for our next segment. if array[current.Index] == nil { switch next.Type { case SegmentIndex: array[current.Index] = make([]any, next.Index+1) case SegmentField: array[current.Index] = make(map[string]any) } return } // If our next segment indexes an array that exists in our current segment's // element we must ensure the array is long enough to set the next segment. if next.Type != SegmentIndex { return } na, ok := array[current.Index].([]any) if !ok { return } if next.Index < uint(len(na)) { return } array[current.Index] = append(na, make([]any, next.Index-uint(len(na))+1)...) } func prepareField(object map[string]any, current, next Segment) { // If this segment is not the final one and doesn't exist we need to // create it for our next segment. if _, ok := object[current.Field]; !ok { switch next.Type { case SegmentIndex: object[current.Field] = make([]any, next.Index+1) case SegmentField: object[current.Field] = make(map[string]any) } return } // If our next segment indexes an array that exists in our current segment's // field we must ensure the array is long enough to set the next segment. if next.Type != SegmentIndex { return } na, ok := object[current.Field].([]any) if !ok { return } if next.Index < uint(len(na)) { return } object[current.Field] = append(na, make([]any, next.Index-uint(len(na))+1)...) } // SetValue at the supplied field path. func (p *Paved) SetValue(path string, value any) error { segments, err := Parse(path) if err != nil { return errors.Wrapf(err, "cannot parse path %q", path) } return p.setValue(segments, value) } func (p *Paved) validateSegments(s Segments) error { if !p.maxFieldPathIndexEnabled() { return nil } for _, segment := range s { if segment.Type == SegmentIndex && segment.Index > p.maxFieldPathIndex { return errors.Errorf("index %v is greater than max allowed index %d", segment.Index, p.maxFieldPathIndex) } } return nil } // SetString value at the supplied field path. func (p *Paved) SetString(path, value string) error { return p.SetValue(path, value) } // SetBool value at the supplied field path. func (p *Paved) SetBool(path string, value bool) error { return p.SetValue(path, value) } // SetNumber value at the supplied field path. func (p *Paved) SetNumber(path string, value float64) error { return p.SetValue(path, value) } // DeleteField deletes the field from the object. // If the path points to an entry in an array, the element // on that index is removed and the next ones are pulled // back. If it is a field on a map, the field is // removed from the map. func (p *Paved) DeleteField(path string) error { segments, err := Parse(path) if err != nil { return errors.Wrapf(err, "cannot parse path %q", path) } return p.delete(segments) } func (p *Paved) delete(segments Segments) error { //nolint:gocognit // See note below. // NOTE(muvaf): I could not reduce the cyclomatic complexity // more than that without disturbing the reading flow. if len(segments) == 1 { o, err := deleteField(p.object, segments[0]) if err != nil { return errors.Wrapf(err, "cannot delete %s", segments) } p.object = o.(map[string]any) //nolint:forcetypeassert // We're deleting from the root of the paved object, which is always a map[string]any. return nil } var in any = p.object for i, current := range segments { // beforeLast is true for the element before the last one because // slices cannot be changed in place and Go does not allow // taking address of map elements which prevents us from // assigning a new array for that entry unless we have the // map available in the context, which is achieved by iterating // until the element before the last one as opposed to // Set/Get functions in this file. beforeLast := i == len(segments)-2 switch current.Type { case SegmentIndex: array, ok := in.([]any) if !ok { return errors.Errorf("%s is not an array", segments[:i]) } // It doesn't exist anyway. if uint(len(array)) <= current.Index { return nil } if beforeLast { o, err := deleteField(array[current.Index], segments[len(segments)-1]) if err != nil { return errors.Wrapf(err, "cannot delete %s", segments) } array[current.Index] = o return nil } in = array[current.Index] case SegmentField: object, ok := in.(map[string]any) if !ok { return errors.Errorf("%s is not an object", segments[:i]) } // It doesn't exist anyway. if _, ok := object[current.Field]; !ok { return nil } if beforeLast { o, err := deleteField(object[current.Field], segments[len(segments)-1]) if err != nil { return errors.Wrapf(err, "cannot delete %s", segments) } object[current.Field] = o return nil } in = object[current.Field] } } return nil } // deleteField deletes the object in obj pointed by // the given Segment and returns it. Returned object // may or may not have the same address in memory. func deleteField(obj any, s Segment) (any, error) { switch s.Type { case SegmentIndex: array, ok := obj.([]any) if !ok { return nil, errors.New("not an array") } if len(array) == 0 || uint(len(array)) <= s.Index { return array, nil } for i := s.Index; i < uint(len(array))-1; i++ { array[i] = array[i+1] } return array[:len(array)-1], nil case SegmentField: object, ok := obj.(map[string]any) if !ok { return nil, errors.New("not an object") } delete(object, s.Field) return object, nil } return nil, nil } ================================================ FILE: pkg/fieldpath/paved_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fieldpath import ( "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/json" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestIsNotFound(t *testing.T) { cases := map[string]struct { reason string err error want bool }{ "NotFound": { reason: "An error with method `IsNotFound() bool` should be considered a not found error.", err: notFoundError{errors.New("boom")}, want: true, }, "WrapsNotFound": { reason: "An error that wraps an error with method `IsNotFound() bool` should be considered a not found error.", err: errors.Wrap(notFoundError{errors.New("boom")}, "because reasons"), want: true, }, "SomethingElse": { reason: "An error without method `IsNotFound() bool` should not be considered a not found error.", err: errors.New("boom"), want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := IsNotFound(tc.err) if got != tc.want { t.Errorf("IsNotFound(...): Want %t, got %t", tc.want, got) } }) } } func TestGetValue(t *testing.T) { type want struct { value any err error } cases := map[string]struct { reason string path string data []byte want want }{ "MetadataName": { reason: "It should be possible to get a field from a nested object", path: "metadata.name", data: []byte(`{"metadata":{"name":"cool"}}`), want: want{ value: "cool", }, }, "ContainerName": { reason: "It should be possible to get a field from an object array element", path: "spec.containers[0].name", data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), want: want{ value: "cool", }, }, "NestedArray": { reason: "It should be possible to get a field from a nested array", path: "items[0][1]", data: []byte(`{"items":[["a", "b"]]}`), want: want{ value: "b", }, }, "OwnerRefController": { reason: "Requesting a boolean field path should work.", path: "metadata.ownerRefs[0].controller", data: []byte(`{"metadata":{"ownerRefs":[{"controller": true}]}}`), want: want{ value: true, }, }, "MetadataVersion": { reason: "Requesting an integer field should work", path: "metadata.version", data: []byte(`{"metadata":{"version":2}}`), want: want{ value: int64(2), }, }, "SomeFloat": { reason: "Requesting a float field should work", path: "metadata.version", data: []byte(`{"metadata":{"version":2.0}}`), want: want{ value: float64(2), }, }, "MetadataNope": { reason: "Requesting a non-existent object field should fail", path: "metadata.name", data: []byte(`{"metadata":{"nope":"cool"}}`), want: want{ err: notFoundError{errors.New("metadata.name: no such field")}, }, }, "InsufficientContainers": { reason: "Requesting a non-existent array element should fail", path: "spec.containers[1].name", data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), want: want{ err: notFoundError{errors.New("spec.containers[1]: no such element")}, }, }, "NotAnArray": { reason: "Indexing an object should fail", path: "metadata[1]", data: []byte(`{"metadata":{"nope":"cool"}}`), want: want{ err: errors.New("metadata: not an array"), }, }, "NotAnObject": { reason: "Requesting a field in an array should fail", path: "spec.containers[nope].name", data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), want: want{ err: errors.New("spec.containers: not an object"), }, }, "MalformedPath": { reason: "Requesting an invalid field path should fail", path: "spec[]", want: want{ err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), }, }, "NilParent": { reason: "Request for a path with a nil parent value", path: "spec.containers[*].name", data: []byte(`{"spec":{"containers": null}}`), want: want{ err: notFoundError{errors.Errorf("%s: expected map, got nil", "spec.containers")}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in) got, err := p.GetValue(tc.path) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.GetValue(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.value, got); diff != "" { t.Errorf("\np.GetValue(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) } }) } } func TestGetValueInto(t *testing.T) { type Struct struct { Slice []string `json:"slice"` StringField string `json:"string"` IntField int `json:"int"` } type Slice []string type args struct { path string out any } type want struct { out any err error } cases := map[string]struct { reason string data []byte args args want want }{ "Struct": { reason: "It should be possible to get a value into a struct.", data: []byte(`{"s":{"slice":["a"],"string":"b","int":1}}`), args: args{ path: "s", out: &Struct{}, }, want: want{ out: &Struct{Slice: []string{"a"}, StringField: "b", IntField: 1}, }, }, "Slice": { reason: "It should be possible to get a value into a slice.", data: []byte(`{"s": ["a", "b"]}`), args: args{ path: "s", out: &Slice{}, }, want: want{ out: &Slice{"a", "b"}, }, }, "MissingPath": { reason: "Getting a value from a fieldpath that doesn't exist should return an error.", data: []byte(`{}`), args: args{ path: "s", out: &Struct{}, }, want: want{ out: &Struct{}, err: notFoundError{errors.New("s: no such field")}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in) err := p.GetValueInto(tc.args.path, tc.args.out) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.GetValueInto(%s): %s: -want error, +got error:\n%s", tc.args.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.out, tc.args.out); diff != "" { t.Errorf("\np.GetValueInto(%s): %s: -want, +got:\n%s", tc.args.path, tc.reason, diff) } }) } } func TestGetString(t *testing.T) { type want struct { value string err error } cases := map[string]struct { reason string path string data []byte want want }{ "MetadataName": { reason: "It should be possible to get a field from a nested object", path: "metadata.name", data: []byte(`{"metadata":{"name":"cool"}}`), want: want{ value: "cool", }, }, "MalformedPath": { reason: "Requesting an invalid field path should fail", path: "spec[]", want: want{ err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), }, }, "NotAString": { reason: "Requesting an non-string field path should fail", path: "metadata.version", data: []byte(`{"metadata":{"version":2}}`), want: want{ err: errors.New("metadata.version: not a string"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in) got, err := p.GetString(tc.path) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.GetString(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.value, got); diff != "" { t.Errorf("\np.GetString(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) } }) } } func TestGetStringArray(t *testing.T) { type want struct { value []string err error } cases := map[string]struct { reason string path string data []byte want want }{ "MetadataLabels": { reason: "It should be possible to get a field from a nested object", path: "spec.containers[0].command", data: []byte(`{"spec": {"containers": [{"command": ["/bin/bash"]}]}}`), want: want{ value: []string{"/bin/bash"}, }, }, "MalformedPath": { reason: "Requesting an invalid field path should fail", path: "spec[]", want: want{ err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), }, }, "NotAnArray": { reason: "Requesting an non-object field path should fail", path: "metadata.version", data: []byte(`{"metadata":{"version":2}}`), want: want{ err: errors.New("metadata.version: not an array"), }, }, "NotAStringArray": { reason: "Requesting an non-string-object field path should fail", path: "metadata.versions", data: []byte(`{"metadata":{"versions":[1,2]}}`), want: want{ err: errors.New("metadata.versions: not an array of strings"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in) got, err := p.GetStringArray(tc.path) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.GetStringArray(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.value, got); diff != "" { t.Errorf("\np.GetStringArray(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) } }) } } func TestGetStringObject(t *testing.T) { type want struct { value map[string]string err error } cases := map[string]struct { reason string path string data []byte want want }{ "MetadataLabels": { reason: "It should be possible to get a field from a nested object", path: "metadata.labels", data: []byte(`{"metadata":{"labels":{"cool":"true"}}}`), want: want{ value: map[string]string{"cool": "true"}, }, }, "MalformedPath": { reason: "Requesting an invalid field path should fail", path: "spec[]", want: want{ err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), }, }, "NotAnObject": { reason: "Requesting an non-object field path should fail", path: "metadata.version", data: []byte(`{"metadata":{"version":2}}`), want: want{ err: errors.New("metadata.version: not an object"), }, }, "NotAStringObject": { reason: "Requesting an non-string-object field path should fail", path: "metadata.versions", data: []byte(`{"metadata":{"versions":{"a": 2}}}`), want: want{ err: errors.New("metadata.versions: not an object with string field values"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in) got, err := p.GetStringObject(tc.path) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.GetStringObject(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.value, got); diff != "" { t.Errorf("\np.GetStringObject(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) } }) } } func TestGetBool(t *testing.T) { type want struct { value bool err error } cases := map[string]struct { reason string path string data []byte want want }{ "OwnerRefController": { reason: "Requesting a boolean field path should work.", path: "metadata.ownerRefs[0].controller", data: []byte(`{"metadata":{"ownerRefs":[{"controller": true}]}}`), want: want{ value: true, }, }, "MalformedPath": { reason: "Requesting an invalid field path should fail", path: "spec[]", want: want{ err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), }, }, "NotABool": { reason: "Requesting an non-boolean field path should fail", path: "metadata.name", data: []byte(`{"metadata":{"name":"cool"}}`), want: want{ err: errors.New("metadata.name: not a bool"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in) got, err := p.GetBool(tc.path) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.GetBool(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.value, got); diff != "" { t.Errorf("\np.GetBool(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) } }) } } func TestGetInteger(t *testing.T) { type want struct { value int64 err error } cases := map[string]struct { reason string path string data []byte want want }{ "MetadataVersion": { reason: "Requesting a number field should work", path: "metadata.version", data: []byte(`{"metadata":{"version":2}}`), want: want{ value: 2, }, }, "MalformedPath": { reason: "Requesting an invalid field path should fail", path: "spec[]", want: want{ err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), }, }, "NotANumber": { reason: "Requesting an non-number field path should fail", path: "metadata.name", data: []byte(`{"metadata":{"name":"cool"}}`), want: want{ err: errors.New("metadata.name: not a (int64) number"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in) got, err := p.GetInteger(tc.path) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.GetNumber(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.value, got); diff != "" { t.Errorf("\np.GetNumber(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) } }) } } func TestSetValue(t *testing.T) { type args struct { path string value any opts []PavedOption } type want struct { object map[string]any err error } cases := map[string]struct { reason string data []byte args args want want }{ "MetadataName": { reason: "Setting an object field should work", data: []byte(`{"metadata":{"name":"lame"}}`), args: args{ path: "metadata.name", value: "cool", }, want: want{ object: map[string]any{ "metadata": map[string]any{ "name": "cool", }, }, }, }, "NonExistentMetadataName": { reason: "Setting a non-existent object field should work", data: []byte(`{}`), args: args{ path: "metadata.name", value: "cool", }, want: want{ object: map[string]any{ "metadata": map[string]any{ "name": "cool", }, }, }, }, "ContainerName": { reason: "Setting a field of an object that is an array element should work", data: []byte(`{"spec":{"containers":[{"name":"lame"}]}}`), args: args{ path: "spec.containers[0].name", value: "cool", }, want: want{ object: map[string]any{ "spec": map[string]any{ "containers": []any{ map[string]any{ "name": "cool", }, }, }, }, }, }, "NonExistentContainerName": { reason: "Setting a field of a non-existent object that is an array element should work", data: []byte(`{}`), args: args{ path: "spec.containers[0].name", value: "cool", }, want: want{ object: map[string]any{ "spec": map[string]any{ "containers": []any{ map[string]any{ "name": "cool", }, }, }, }, }, }, "NewContainer": { reason: "Growing an array object field should work", data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), args: args{ path: "spec.containers[1].name", value: "cooler", }, want: want{ object: map[string]any{ "spec": map[string]any{ "containers": []any{ map[string]any{ "name": "cool", }, map[string]any{ "name": "cooler", }, }, }, }, }, }, "NestedArray": { reason: "Setting a value in a nested array should work", data: []byte(`{}`), args: args{ path: "data[0][0]", value: "a", }, want: want{ object: map[string]any{ "data": []any{ []any{"a"}, }, }, }, }, "GrowNestedArray": { reason: "Growing then setting a value in a nested array should work", data: []byte(`{"data":[["a"]]}`), args: args{ path: "data[0][1]", value: "b", }, want: want{ object: map[string]any{ "data": []any{ []any{"a", "b"}, }, }, }, }, "GrowArrayField": { reason: "Growing then setting a value in an array field should work", data: []byte(`{"data":["a"]}`), args: args{ path: "data[2]", value: "c", }, want: want{ object: map[string]any{ "data": []any{"a", nil, "c"}, }, }, }, "RejectsHighIndexes": { reason: "Paths having indexes above the maximum default value are rejected", data: []byte(`{"data":["a"]}`), args: args{ path: fmt.Sprintf("data[%v]", DefaultMaxFieldPathIndex+1), value: "c", }, want: want{ object: map[string]any{ "data": []any{"a"}, }, err: errors.Errorf("index %v is greater than max allowed index %v", DefaultMaxFieldPathIndex+1, DefaultMaxFieldPathIndex), }, }, "NotRejectsHighIndexesIfNoDefaultOptions": { reason: "Paths having indexes above the maximum default value are not rejected if default disabled", data: []byte(`{"data":["a"]}`), args: args{ path: fmt.Sprintf("data[%v]", DefaultMaxFieldPathIndex+1), value: "c", opts: []PavedOption{WithMaxFieldPathIndex(0)}, }, want: want{ object: map[string]any{ "data": func() []any { res := make([]any, DefaultMaxFieldPathIndex+2) res[0] = "a" res[DefaultMaxFieldPathIndex+1] = "c" return res }(), }, }, }, "MapStringString": { reason: "A map of string to string should be converted to a map of string to any", data: []byte(`{"metadata":{}}`), args: args{ path: "metadata.labels", value: map[string]string{"cool": "very"}, }, want: want{ object: map[string]any{ "metadata": map[string]any{ "labels": map[string]any{"cool": "very"}, }, }, }, }, "OwnerReference": { reason: "An ObjectReference (i.e. struct) should be converted to a map of string to any", data: []byte(`{"metadata":{}}`), args: args{ path: "metadata.ownerRefs[0]", value: metav1.OwnerReference{ APIVersion: "v", Kind: "k", Name: "n", UID: types.UID("u"), }, }, want: want{ object: map[string]any{ "metadata": map[string]any{ "ownerRefs": []any{ map[string]any{ "apiVersion": "v", "kind": "k", "name": "n", "uid": "u", }, }, }, }, }, }, "NotAnArray": { reason: "Indexing an object field should fail", data: []byte(`{"data":{}}`), args: args{ path: "data[0]", }, want: want{ object: map[string]any{"data": map[string]any{}}, err: errors.New("data is not an array"), }, }, "NotAnObject": { reason: "Requesting a field in an array should fail", data: []byte(`{"data":[]}`), args: args{ path: "data.name", }, want: want{ object: map[string]any{"data": []any{}}, err: errors.New("data is not an object"), }, }, "MalformedPath": { reason: "Requesting an invalid field path should fail", args: args{ path: "spec[]", }, want: want{ object: map[string]any{}, err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in, tc.args.opts...) err := p.SetValue(tc.args.path, tc.args.value) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.SetValue(%s, %v): %s: -want error, +got error:\n%s", tc.args.path, tc.args.value, tc.reason, diff) } if diff := cmp.Diff(tc.want.object, p.object); diff != "" { t.Fatalf("\np.SetValue(%s, %v): %s: -want, +got:\n%s", tc.args.path, tc.args.value, tc.reason, diff) } }) } } func TestExpandWildcards(t *testing.T) { type want struct { expanded []string err error } cases := map[string]struct { reason string path string data []byte want want }{ "NoWildcardExisting": { reason: "It should return same path if no wildcard in an existing path", path: "password", data: []byte(`{"password":"top-secret"}`), want: want{ expanded: []string{"password"}, }, }, "NoWildcardNonExisting": { reason: "It should return no results if no wildcard in a non-existing path", path: "username", data: []byte(`{"password":"top-secret"}`), want: want{ expanded: []string{}, }, }, "NestedNoWildcardExisting": { reason: "It should return same path if no wildcard in an existing path", path: "items[0][1]", data: []byte(`{"items":[["a", "b"]]}`), want: want{ expanded: []string{"items[0][1]"}, }, }, "NestedNoWildcardNonExisting": { reason: "It should return no results if no wildcard in a non-existing path", path: "items[0][5]", data: []byte(`{"items":[["a", "b"]]}`), want: want{ expanded: []string{}, }, }, "NestedArray": { reason: "It should return all possible paths for an array", path: "items[*][*]", data: []byte(`{"items":[["a", "b", "c"], ["d"]]}`), want: want{ expanded: []string{"items[0][0]", "items[0][1]", "items[0][2]", "items[1][0]"}, }, }, "KeysOfMap": { reason: "It should return all possible paths for a map in proper syntax", path: "items[*]", data: []byte(`{"items":{ "key1": "val1", "key2.as.annotation": "val2"}}`), want: want{ expanded: []string{"items.key1", "items[key2.as.annotation]"}, }, }, "ArrayOfObjects": { reason: "It should return all possible paths for an array of objects", path: "spec.containers[*][*]", data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now"]}]}}`), want: want{ expanded: []string{"spec.containers[0].name", "spec.containers[0].image", "spec.containers[0].args"}, }, }, "MultiLayer": { reason: "It should return all possible paths for a multilayer input", path: "spec.containers[*].args[*]", data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now", "debug"]}]}}`), want: want{ expanded: []string{"spec.containers[0].args[0]", "spec.containers[0].args[1]", "spec.containers[0].args[2]"}, }, }, "WildcardInTheBeginning": { reason: "It should return all possible paths for a multilayer input with wildcard in the beginning", path: "spec.containers[*].args[1]", data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now", "debug"]}]}}`), want: want{ expanded: []string{"spec.containers[0].args[1]"}, }, }, "WildcardAtTheEnd": { reason: "It should return all possible paths for a multilayer input with wildcard at the end", path: "spec.containers[0].args[*]", data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now", "debug"]}]}}`), want: want{ expanded: []string{"spec.containers[0].args[0]", "spec.containers[0].args[1]", "spec.containers[0].args[2]"}, }, }, "NoData": { reason: "If there is no input data, no expanded fields could be found", path: "metadata[*]", data: nil, want: want{ expanded: []string{}, }, }, "InsufficientContainers": { reason: "Requesting a non-existent array element should return nothing", path: "spec.containers[1].args[*]", data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), want: want{ expanded: []string{}, }, }, "UnexpectedWildcard": { reason: "Requesting a wildcard for an object should fail", path: "spec.containers[0].name[*]", data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), want: want{ err: errors.Wrapf(errors.Errorf("%q: unexpected wildcard usage", "spec.containers[0].name"), "cannot expand wildcards for segments: %q", "spec.containers[0].name[*]"), }, }, "NotAnArray": { reason: "Indexing an object should fail", path: "metadata[1]", data: []byte(`{"metadata":{"nope":"cool"}}`), want: want{ err: errors.Wrapf(errors.New("metadata: not an array"), "cannot expand wildcards for segments: %q", "metadata[1]"), }, }, "NotAnObject": { reason: "Requesting a field in an array should fail", path: "spec.containers[nope].name", data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), want: want{ err: errors.Wrapf(errors.New("spec.containers: not an object"), "cannot expand wildcards for segments: %q", "spec.containers.nope.name"), }, }, "MalformedPath": { reason: "Requesting an invalid field path should fail", path: "spec[]", want: want{ err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), }, }, "NilValue": { reason: "Requesting a wildcard for an object that has nil value", path: "spec.containers[*].name", data: []byte(`{"spec":{"containers": null}}`), want: want{ err: errors.Wrapf(notFoundError{errors.Errorf("wildcard field %q is not found in the path", "spec.containers")}, "cannot expand wildcards for segments: %q", "spec.containers[*].name"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in) got, err := p.ExpandWildcards(tc.path) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.ExpandWildcards(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.expanded, got, cmpopts.SortSlices(func(x, y string) bool { return x < y })); diff != "" { t.Errorf("\np.ExpandWildcards(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) } }) } } func TestDeleteField(t *testing.T) { type args struct { path string } type want struct { object map[string]any err error } cases := map[string]struct { reason string data []byte args args want want }{ "MalformedPath": { reason: "Requesting an invalid field path should fail", args: args{ path: "spec[]", }, want: want{ object: map[string]any{}, err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), }, }, "IndexGivenForNonArray": { reason: "Trying to delete a numbered index from a map should fail.", data: []byte(`{"data":{}}`), args: args{ path: "data[0]", }, want: want{ object: map[string]any{"data": map[string]any{}}, err: errors.Wrap(errors.New("not an array"), "cannot delete data[0]"), }, }, "KeyGivenForNonMap": { reason: "Trying to delete a key from an array should fail.", data: []byte(`{"data":[["a"]]}`), args: args{ path: "data[0].a", }, want: want{ object: map[string]any{"data": []any{[]any{"a"}}}, err: errors.Wrap(errors.New("not an object"), "cannot delete data[0].a"), }, }, "KeyGivenForNonMapInMiddle": { reason: "If one of the segments that is a field corresponds to array, it should fail.", data: []byte(`{"data":[{"another": "field"}]}`), args: args{ path: "data.some.another", }, want: want{ object: map[string]any{"data": []any{ map[string]any{ "another": "field", }, }}, err: errors.New("data is not an object"), }, }, "IndexGivenForNonArrayInMiddle": { reason: "If one of the segments that is an index corresponds to map, it should fail.", data: []byte(`{"data":{"another": ["field"]}}`), args: args{ path: "data[0].another", }, want: want{ object: map[string]any{"data": map[string]any{ "another": []any{ "field", }, }}, err: errors.New("data is not an array"), }, }, "ObjectField": { reason: "Deleting a field from a map should work.", data: []byte(`{"metadata":{"name":"lame"}}`), args: args{ path: "metadata.name", }, want: want{ object: map[string]any{ "metadata": map[string]any{}, }, }, }, "ObjectSingleField": { reason: "Deleting a field from a map should work.", data: []byte(`{"metadata":{"name":"lame"}, "olala": {"omama": "koala"}}`), args: args{ path: "metadata", }, want: want{ object: map[string]any{ "olala": map[string]any{ "omama": "koala", }, }, }, }, "ObjectLeafField": { reason: "Deleting a field that is deep in the tree from a map should work.", data: []byte(`{"spec":{"some": {"more": "delete-me"}}}`), args: args{ path: "spec.some.more", }, want: want{ object: map[string]any{ "spec": map[string]any{ "some": map[string]any{}, }, }, }, }, "ObjectMidField": { reason: "Deleting a field that is in the middle of the tree from a map should work.", data: []byte(`{"spec":{"some": {"more": "delete-me"}}}`), args: args{ path: "spec.some", }, want: want{ object: map[string]any{ "spec": map[string]any{}, }, }, }, "ObjectInArray": { reason: "Deleting a field that is in the middle of the tree from a map should work.", data: []byte(`{"spec":[{"some": {"more": "delete-me"}}]}`), args: args{ path: "spec[0].some.more", }, want: want{ object: map[string]any{ "spec": []any{ map[string]any{ "some": map[string]any{}, }, }, }, }, }, "ArrayFirstElement": { reason: "Deleting the first element from an array should work", data: []byte(`{"items":["a", "b"]}`), args: args{ path: "items[0]", }, want: want{ object: map[string]any{ "items": []any{ "b", }, }, }, }, "ArrayLastElement": { reason: "Deleting the last element from an array should work", data: []byte(`{"items":["a", "b"]}`), args: args{ path: "items[1]", }, want: want{ object: map[string]any{ "items": []any{ "a", }, }, }, }, "ArrayMidElement": { reason: "Deleting an element that is neither first nor last from an array should work", data: []byte(`{"items":["a", "b", "c"]}`), args: args{ path: "items[1]", }, want: want{ object: map[string]any{ "items": []any{ "a", "c", }, }, }, }, "ArrayOnlyElements": { reason: "Deleting the only element from an array should work", data: []byte(`{"items":["a"]}`), args: args{ path: "items[0]", }, want: want{ object: map[string]any{ "items": []any{}, }, }, }, "ArrayMultipleIndex": { reason: "Deleting an element from an array of array should work", data: []byte(`{"items":[["a", "b"]]}`), args: args{ path: "items[0][1]", }, want: want{ object: map[string]any{ "items": []any{ []any{ "a", }, }, }, }, }, "ArrayNoElement": { reason: "Deleting an element from an empty array should work", data: []byte(`{"items":[]}`), args: args{ path: "items[0]", }, want: want{ object: map[string]any{ "items": []any{}, }, }, }, "NonExistentPathInMap": { reason: "It should be no-op if the field does not exist already.", data: []byte(`{"items":[]}`), args: args{ path: "items[0].metadata", }, want: want{ object: map[string]any{ "items": []any{}, }, }, }, "NonExistentPathInArray": { reason: "It should be no-op if the field does not exist already.", data: []byte(`{"items":{"some": "other"}}`), args: args{ path: "items.metadata[0]", }, want: want{ object: map[string]any{ "items": map[string]any{ "some": "other", }, }, }, }, "NonExistentElementInArray": { reason: "It should be no-op if the field does not exist already.", data: []byte(`{"items":["some", "other"]}`), args: args{ path: "items[5]", }, want: want{ object: map[string]any{ "items": []any{ "some", "other", }, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) p := Pave(in) err := p.DeleteField(tc.args.path) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\np.DeleteField(%s): %s: -want error, +got error:\n%s", tc.args.path, tc.reason, diff) } if diff := cmp.Diff(tc.want.object, p.object); diff != "" { t.Fatalf("\np.DeleteField(%s): %s: -want, +got:\n%s", tc.args.path, tc.reason, diff) } }) } } ================================================ FILE: pkg/gate/gate.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package gate contains a gated function callback registration implementation. package gate import ( "slices" "sync" ) // Gate implements a gated function callback registration with comparable conditions. type Gate[T comparable] struct { mux sync.RWMutex satisfied map[T]bool fns []gated[T] } // gated is an internal tracking resource. type gated[T comparable] struct { // fn is the function callback we will invoke when all the dependent conditions are true. fn func() // depends is the list of conditions this gated function is waiting on. This is an AND. depends []T // released means the gated function has been invoked and we can garbage collect this gated function. released bool } // Register a callback function that will be called when all the provided dependent conditions are true. // After all conditions are true, the callback function is removed from the registration and will not be called again. // Thread Safe. func (g *Gate[T]) Register(fn func(), depends ...T) { g.mux.Lock() g.fns = append(g.fns, gated[T]{fn: fn, depends: depends}) g.mux.Unlock() g.process() } // Set marks the associated condition to the given value. If the condition is already set as that value, then this is a // no-op. Returns true if there was an update detected. Thread safe. func (g *Gate[T]) Set(condition T, value bool) bool { g.mux.Lock() if g.satisfied == nil { g.satisfied = make(map[T]bool) } old, found := g.satisfied[condition] updated := false if !found || old != value { updated = true g.satisfied[condition] = value } // process() would also like to lock the mux, so we must unlock here directly and not use defer. g.mux.Unlock() if updated { g.process() } return updated } func (g *Gate[T]) process() { g.mux.Lock() defer g.mux.Unlock() for i := range g.fns { // release controls if we should release the function. release := true for _, dep := range g.fns[i].depends { if !g.satisfied[dep] { release = false } } if release { fn := g.fns[i].fn // mark the function released so we can garbage collect after we are done with the loop. g.fns[i].released = true // Need to capture a copy of fn or else we would be accessing a deleted member when the go routine runs. go fn() } } // garbage collect released functions. g.fns = slices.DeleteFunc(g.fns, func(a gated[T]) bool { return a.released }) } ================================================ FILE: pkg/gate/gate_test.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package gate_test import ( "sync" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/crossplane/crossplane-runtime/v2/pkg/gate" ) func TestGateRegister(t *testing.T) { type args struct { depends []string } type want struct { called bool } cases := map[string]struct { reason string args args want want }{ "NoDependencies": { reason: "Should immediately call function when no dependencies are required", args: args{ depends: []string{}, }, want: want{ called: true, }, }, "SingleDependency": { reason: "Should not call function when dependency is not met", args: args{ depends: []string{"condition1"}, }, want: want{ called: false, }, }, "MultipleDependencies": { reason: "Should not call function when multiple dependencies are not met", args: args{ depends: []string{"condition1", "condition2"}, }, want: want{ called: false, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { g := new(gate.Gate[string]) called := false g.Register(func() { called = true }, tc.args.depends...) // Give some time for goroutine to execute time.Sleep(10 * time.Millisecond) if diff := cmp.Diff(tc.want.called, called); diff != "" { t.Errorf("\n%s\nRegister(...): -want called, +got called:\n%s", tc.reason, diff) } }) } } func TestGateIntegration(t *testing.T) { type want struct { called bool } cases := map[string]struct { reason string setup func(g *gate.Gate[string]) chan bool want want }{ "SingleDependencyMet": { reason: "Should call function when single dependency is met", setup: func(g *gate.Gate[string]) chan bool { called := make(chan bool, 1) g.Register(func() { called <- true }, "condition1") // Set condition to true (will be initialized as false first) g.Set("condition1", true) return called }, want: want{ called: true, }, }, "MultipleDependenciesMet": { reason: "Should call function when all dependencies are met", setup: func(g *gate.Gate[string]) chan bool { called := make(chan bool, 1) g.Register(func() { called <- true }, "condition1", "condition2") // Set both conditions to true g.Set("condition1", true) g.Set("condition2", true) return called }, want: want{ called: true, }, }, "PartialDependenciesMet": { reason: "Should not call function when only some dependencies are met", setup: func(g *gate.Gate[string]) chan bool { called := make(chan bool, 1) g.Register(func() { called <- true }, "condition1", "condition2") // Set only one condition to true g.Set("condition1", true) return called }, want: want{ called: false, }, }, "DependenciesAlreadyMet": { reason: "Should call function when dependencies are already met", setup: func(g *gate.Gate[string]) chan bool { called := make(chan bool, 1) g.Set("condition1", true) g.Set("condition2", true) g.Register(func() { called <- true }, "condition1", "condition2") return called }, want: want{ called: true, }, }, "DependencySetThenUnset": { reason: "Should call function when dependency is met, even if unset later", setup: func(g *gate.Gate[string]) chan bool { called := make(chan bool, 1) g.Register(func() { called <- true }, "condition1") // Set condition to true then false (function already called when true) g.Set("condition1", true) g.Set("condition1", false) return called }, want: want{ called: true, }, }, "FunctionCalledOnlyOnce": { reason: "Should call function only once even if conditions change after", setup: func(g *gate.Gate[string]) chan bool { called := make(chan bool, 2) // Buffer for potential multiple calls g.Register(func() { called <- true }, "condition1") // Set condition multiple times g.Set("condition1", true) g.Set("condition1", false) g.Set("condition1", true) return called }, want: want{ called: true, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { g := new(gate.Gate[string]) callChannel := tc.setup(g) var got bool select { case got = <-callChannel: case <-time.After(100 * time.Millisecond): got = false } if diff := cmp.Diff(tc.want.called, got); diff != "" { t.Errorf("\n%s\nIntegration test: -want called, +got called:\n%s", tc.reason, diff) } // For the "only once" test, ensure no additional calls if name == "FunctionCalledOnlyOnce" && tc.want.called { select { case <-callChannel: t.Errorf("\n%s\nFunction was called more than once", tc.reason) case <-time.After(50 * time.Millisecond): // Good - no additional calls } } }) } } func TestGateConcurrency(t *testing.T) { g := new(gate.Gate[string]) const numGoroutines = 100 var wg sync.WaitGroup callCount := make(chan struct{}, numGoroutines) // Register functions concurrently for range numGoroutines { wg.Go(func() { g.Register(func() { callCount <- struct{}{} }, "shared-condition") }) } // Wait for all registrations wg.Wait() // Set condition to true once g.Set("shared-condition", true) // Give some time for goroutines to execute time.Sleep(100 * time.Millisecond) // Count how many functions were called close(callCount) count := 0 for range callCount { count++ } if count != numGoroutines { t.Errorf("Expected %d function calls, got %d", numGoroutines, count) } } func TestGateTypeSafety(t *testing.T) { intGate := new(gate.Gate[int]) called := false intGate.Register(func() { called = true }, 1, 2, 3) intGate.Set(1, true) intGate.Set(2, true) intGate.Set(3, true) // Give some time for goroutine to execute time.Sleep(10 * time.Millisecond) if !called { t.Error("Function should have been called when all int conditions were met") } } ================================================ FILE: pkg/logging/klog.go ================================================ // Copyright 2024 Upbound Inc. // All rights reserved package logging import ( "flag" "os" "strings" "github.com/go-logr/logr" "k8s.io/klog/v2" ) // SetFilteredKlogLogger sets log as the logger backend of klog, but filtering // aggressively to avoid noise. func SetFilteredKlogLogger(log logr.Logger) { // initialize klog at verbosity level 3, dropping everything higher. fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) klog.InitFlags(fs) fs.Parse([]string{"--v=3"}) //nolint:errcheck // we couldn't do anything here anyway klogr := logr.New(&requestThrottlingFilter{log.GetSink()}) klog.SetLogger(klogr) } // requestThrottlingFilter drops everything that is not a client-go throttling // message, compare: // https://github.com/kubernetes/client-go/blob/8c4efe8d079e405329f314fb789a41ac6af101dc/rest/request.go#L621 type requestThrottlingFilter struct { logr.LogSink } func (l *requestThrottlingFilter) Info(level int, msg string, keysAndValues ...any) { if !strings.Contains(msg, "Waited for ") || !strings.Contains(msg, " request: ") { return } l.LogSink.Info(l.klogToLogrLevel(level), msg, keysAndValues...) } func (l *requestThrottlingFilter) Enabled(level int) bool { return l.LogSink.Enabled(l.klogToLogrLevel(level)) } func (l *requestThrottlingFilter) klogToLogrLevel(klogLvl int) int { // we want a default klog level of 3 for info, 4 for debug, corresponding to // logr levels of 0 and 1. if klogLvl >= 3 { return klogLvl - 3 } return 0 } func (l *requestThrottlingFilter) WithCallDepth(depth int) logr.LogSink { if delegate, ok := l.LogSink.(logr.CallDepthLogSink); ok { return &requestThrottlingFilter{LogSink: delegate.WithCallDepth(depth)} } return l } ================================================ FILE: pkg/logging/logging.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package logging provides Crossplane's recommended logging interface. // // The logging interface defined by this package is inspired by the following: // // * https://peter.bourgon.org/go-best-practices-2016/#logging-and-instrumentation // * https://dave.cheney.net/2015/11/05/lets-talk-about-logging // * https://dave.cheney.net/2017/01/23/the-package-level-logger-anti-pattern // * https://github.com/crossplane/crossplane/blob/c06433/design/one-pager-error-and-event-reporting.md // // It is similar to other logging interfaces inspired by said article, namely: // // * https://github.com/go-logr/logr // * https://github.com/go-log/log // // Crossplane prefers not to use go-logr because it desires a simpler API with // only two levels (per Dave's article); Info and Debug. Crossplane prefers not // to use go-log because it does not support structured logging. This package // *is* however a subset of go-logr's functionality, and is intended to wrap // go-logr (interfaces all the way down!), in order to maintain compatibility // with the https://github.com/kubernetes-sigs/controller-runtime/ log plumbing. package logging import ( "github.com/go-logr/logr" ) // A Logger logs messages. Messages may be supplemented by structured data. type Logger interface { // Info logs a message with optional structured data. Structured data must // be supplied as an array that alternates between string keys and values of // an arbitrary type. Use Info for messages that Crossplane operators are // very likely to be concerned with when running Crossplane. Info(msg string, keysAndValues ...any) // Debug logs a message with optional structured data. Structured data must // be supplied as an array that alternates between string keys and values of // an arbitrary type. Use Debug for messages that Crossplane operators or // developers may be concerned with when debugging Crossplane. Debug(msg string, keysAndValues ...any) // WithValues returns a Logger that will include the supplied structured // data with any subsequent messages it logs. Structured data must // be supplied as an array that alternates between string keys and values of // an arbitrary type. WithValues(keysAndValues ...any) Logger } // NewNopLogger returns a Logger that does nothing. func NewNopLogger() Logger { return nopLogger{} } type nopLogger struct{} func (l nopLogger) Info(_ string, _ ...any) {} func (l nopLogger) Debug(_ string, _ ...any) {} func (l nopLogger) WithValues(_ ...any) Logger { return nopLogger{} } // NewLogrLogger returns a Logger that is satisfied by the supplied logr.Logger, // which may be satisfied in turn by various logging implementations (Zap, klog, // etc). Debug messages are logged at V(1). func NewLogrLogger(l logr.Logger) Logger { return logrLogger{log: l} } type logrLogger struct { log logr.Logger } func (l logrLogger) Info(msg string, keysAndValues ...any) { l.log.Info(msg, keysAndValues...) //nolint:logrlint // False positive - logrlint thinks there's an odd number of args. } func (l logrLogger) Debug(msg string, keysAndValues ...any) { l.log.V(1).Info(msg, keysAndValues...) //nolint:logrlint // False positive - logrlint thinks there's an odd number of args. } func (l logrLogger) WithValues(keysAndValues ...any) Logger { return logrLogger{log: l.log.WithValues(keysAndValues...)} //nolint:logrlint // False positive - logrlint thinks there's an odd number of args. } ================================================ FILE: pkg/meta/meta.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package meta contains functions for dealing with Kubernetes object metadata. package meta import ( "maps" "slices" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) const ( // AnnotationKeyExternalName is the key in the annotations map of a // resource for the name of the resource as it appears on provider's // systems. AnnotationKeyExternalName = "crossplane.io/external-name" // AnnotationKeyExternalCreatePending is the key in the annotations map // of a resource that indicates the last time creation of the external // resource was pending (i.e. about to happen). Its value must be an // RFC3999 timestamp. AnnotationKeyExternalCreatePending = "crossplane.io/external-create-pending" // AnnotationKeyExternalCreateSucceeded is the key in the annotations // map of a resource that represents the last time the external resource // was created successfully. Its value must be an RFC3339 timestamp, // which can be used to determine how long ago a resource was created. // This is useful for eventually consistent APIs that may take some time // before the API called by Observe will report that a recently created // external resource exists. AnnotationKeyExternalCreateSucceeded = "crossplane.io/external-create-succeeded" // AnnotationKeyExternalCreateFailed is the key in the annotations map // of a resource that indicates the last time creation of the external // resource failed. Its value must be an RFC3999 timestamp. AnnotationKeyExternalCreateFailed = "crossplane.io/external-create-failed" // AnnotationKeyReconciliationPaused is the key in the annotations map // of a resource that indicates that further reconciliations on the // resource are paused. All create/update/delete/generic events on // the resource will be filtered and thus no further reconcile requests // will be queued for the resource. AnnotationKeyReconciliationPaused = "crossplane.io/paused" // AnnotationKeyPollInterval overrides the controller-level poll // interval for a specific resource. The value must be a valid Go // duration string (e.g. "1h", "30m", "24h"). AnnotationKeyPollInterval = "crossplane.io/poll-interval" // AnnotationKeyReconcileRequestedAt triggers an immediate // reconciliation when its value changes. The value is an opaque // token, typically a timestamp. After handling, the reconciler // records the token in status.lastHandledReconcileAt. AnnotationKeyReconcileRequestedAt = "crossplane.io/reconcile-requested-at" ) // ReferenceTo returns an object reference to the supplied object, presumed to // be of the supplied group, version, and kind. // // Deprecated: use a more specific reference type, such as TypedReference or // Reference instead of the overly verbose ObjectReference. // See https://github.com/crossplane/crossplane-runtime/issues/49 func ReferenceTo(o metav1.Object, of schema.GroupVersionKind) *corev1.ObjectReference { v, k := of.ToAPIVersionAndKind() return &corev1.ObjectReference{ APIVersion: v, Kind: k, Namespace: o.GetNamespace(), Name: o.GetName(), UID: o.GetUID(), } } // TypedReferenceTo returns a typed object reference to the supplied object, // presumed to be of the supplied group, version, and kind. func TypedReferenceTo(o metav1.Object, of schema.GroupVersionKind) *xpv2.TypedReference { v, k := of.ToAPIVersionAndKind() return &xpv2.TypedReference{ APIVersion: v, Kind: k, Name: o.GetName(), UID: o.GetUID(), } } // AsOwner converts the supplied object reference to an owner reference. func AsOwner(r *xpv2.TypedReference) metav1.OwnerReference { return metav1.OwnerReference{ APIVersion: r.APIVersion, Kind: r.Kind, Name: r.Name, UID: r.UID, } } // AsController converts the supplied object reference to a controller // reference. You may also consider using metav1.NewControllerRef. func AsController(r *xpv2.TypedReference) metav1.OwnerReference { t := true ref := AsOwner(r) ref.Controller = &t ref.BlockOwnerDeletion = &t return ref } // HaveSameController returns true if both supplied objects are controlled by // the same object. func HaveSameController(a, b metav1.Object) bool { ac := metav1.GetControllerOf(a) bc := metav1.GetControllerOf(b) // We do not consider two objects without any controller to have // the same controller. if ac == nil || bc == nil { return false } return ac.UID == bc.UID } // NamespacedNameOf returns the referenced object's namespaced name. func NamespacedNameOf(r *corev1.ObjectReference) types.NamespacedName { return types.NamespacedName{Namespace: r.Namespace, Name: r.Name} } // AddOwnerReference to the supplied object' metadata. Any existing owner with // the same UID as the supplied reference will be replaced. func AddOwnerReference(o metav1.Object, r metav1.OwnerReference) { refs := o.GetOwnerReferences() for i := range refs { if refs[i].UID == r.UID { refs[i] = r o.SetOwnerReferences(refs) return } } o.SetOwnerReferences(append(refs, r)) } // AddControllerReference to the supplied object's metadata. Any existing owner // with the same UID as the supplied reference will be replaced. Returns an // error if the supplied object is already controlled by a different owner. func AddControllerReference(o metav1.Object, r metav1.OwnerReference) error { if c := metav1.GetControllerOf(o); c != nil && c.UID != r.UID { return errors.Errorf("%s is already controlled by %s %s (UID %s)", o.GetName(), c.Kind, c.Name, c.UID) } AddOwnerReference(o, r) return nil } // AddFinalizer to the supplied Kubernetes object's metadata. func AddFinalizer(o metav1.Object, finalizer string) { f := o.GetFinalizers() if slices.Contains(f, finalizer) { return } o.SetFinalizers(append(f, finalizer)) } // RemoveFinalizer from the supplied Kubernetes object's metadata. func RemoveFinalizer(o metav1.Object, finalizer string) { f := o.GetFinalizers() for i, e := range f { if e == finalizer { f = append(f[:i], f[i+1:]...) } } o.SetFinalizers(f) } // FinalizerExists checks whether given finalizer is already set. func FinalizerExists(o metav1.Object, finalizer string) bool { f := o.GetFinalizers() return slices.Contains(f, finalizer) } // AddLabels to the supplied object. func AddLabels(o metav1.Object, labels map[string]string) { l := o.GetLabels() if l == nil { o.SetLabels(labels) return } maps.Copy(l, labels) o.SetLabels(l) } // RemoveLabels with the supplied keys from the supplied object. func RemoveLabels(o metav1.Object, labels ...string) { l := o.GetLabels() if l == nil { return } for _, k := range labels { delete(l, k) } o.SetLabels(l) } // AddAnnotations to the supplied object. func AddAnnotations(o metav1.Object, annotations map[string]string) { a := o.GetAnnotations() if a == nil { o.SetAnnotations(annotations) return } maps.Copy(a, annotations) o.SetAnnotations(a) } // RemoveAnnotations with the supplied keys from the supplied object. func RemoveAnnotations(o metav1.Object, annotations ...string) { a := o.GetAnnotations() if a == nil { return } for _, k := range annotations { delete(a, k) } o.SetAnnotations(a) } // WasDeleted returns true if the supplied object was deleted from the API server. func WasDeleted(o metav1.Object) bool { return !o.GetDeletionTimestamp().IsZero() } // WasCreated returns true if the supplied object was created in the API server. func WasCreated(o metav1.Object) bool { // This looks a little different from WasDeleted because DeletionTimestamp // returns a reference while CreationTimestamp returns a value. t := o.GetCreationTimestamp() return !t.IsZero() } // GetExternalName returns the external name annotation value on the resource. func GetExternalName(o metav1.Object) string { return o.GetAnnotations()[AnnotationKeyExternalName] } // SetExternalName sets the external name annotation of the resource. func SetExternalName(o metav1.Object, name string) { AddAnnotations(o, map[string]string{AnnotationKeyExternalName: name}) } // GetExternalCreatePending returns the time at which the external resource // was most recently pending creation. func GetExternalCreatePending(o metav1.Object) time.Time { a := o.GetAnnotations()[AnnotationKeyExternalCreatePending] t, err := time.Parse(time.RFC3339, a) if err != nil { return time.Time{} } return t } // SetExternalCreatePending sets the time at which the external resource was // most recently pending creation to the supplied time. func SetExternalCreatePending(o metav1.Object, t time.Time) { AddAnnotations(o, map[string]string{AnnotationKeyExternalCreatePending: t.Format(time.RFC3339)}) } // GetExternalCreateSucceeded returns the time at which the external resource // was most recently created. func GetExternalCreateSucceeded(o metav1.Object) time.Time { a := o.GetAnnotations()[AnnotationKeyExternalCreateSucceeded] t, err := time.Parse(time.RFC3339, a) if err != nil { return time.Time{} } return t } // SetExternalCreateSucceeded sets the time at which the external resource was // most recently created to the supplied time. func SetExternalCreateSucceeded(o metav1.Object, t time.Time) { AddAnnotations(o, map[string]string{AnnotationKeyExternalCreateSucceeded: t.Format(time.RFC3339)}) } // GetExternalCreateFailed returns the time at which the external resource // recently failed to create. func GetExternalCreateFailed(o metav1.Object) time.Time { a := o.GetAnnotations()[AnnotationKeyExternalCreateFailed] t, err := time.Parse(time.RFC3339, a) if err != nil { return time.Time{} } return t } // SetExternalCreateFailed sets the time at which the external resource most // recently failed to create. func SetExternalCreateFailed(o metav1.Object, t time.Time) { AddAnnotations(o, map[string]string{AnnotationKeyExternalCreateFailed: t.Format(time.RFC3339)}) } // ExternalCreateIncomplete returns true if creation of the external resource // appears to be incomplete. We deem creation to be incomplete if the 'external // create pending' annotation is the newest of all tracking annotations that are // set (i.e. pending, succeeded, and failed). func ExternalCreateIncomplete(o metav1.Object) bool { pending := GetExternalCreatePending(o) succeeded := GetExternalCreateSucceeded(o) failed := GetExternalCreateFailed(o) // If creation never started it can't be incomplete. if pending.IsZero() { return false } latest := succeeded if failed.After(succeeded) { latest = failed } return pending.After(latest) } // ExternalCreateSucceededDuring returns true if creation of the external // resource that corresponds to the supplied managed resource succeeded within // the supplied duration. func ExternalCreateSucceededDuring(o metav1.Object, d time.Duration) bool { t := GetExternalCreateSucceeded(o) if t.IsZero() { return false } return time.Since(t) < d } // IsPaused returns true if the object has the AnnotationKeyReconciliationPaused // annotation set to `true`. func IsPaused(o metav1.Object) bool { return o.GetAnnotations()[AnnotationKeyReconciliationPaused] == "true" } // GetPollInterval returns the poll interval override for the given resource, // if set via the AnnotationKeyPollInterval annotation. func GetPollInterval(o metav1.Object) (time.Duration, bool) { v, ok := o.GetAnnotations()[AnnotationKeyPollInterval] if !ok || v == "" { return 0, false } d, err := time.ParseDuration(v) if err != nil || d <= 0 { return 0, false } return d, true } // GetReconcileRequest returns the reconcile-requested-at annotation token // and true if present and non-empty, or empty string and false otherwise. func GetReconcileRequest(o metav1.Object) (string, bool) { v, ok := o.GetAnnotations()[AnnotationKeyReconcileRequestedAt] if !ok || v == "" { return "", false } return v, true } // SetReconcileRequest sets the reconcile-requested-at annotation to the // supplied token value. func SetReconcileRequest(o metav1.Object, token string) { AddAnnotations(o, map[string]string{AnnotationKeyReconcileRequestedAt: token}) } ================================================ FILE: pkg/meta/meta_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package meta import ( "testing" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) const ( group = "coolstuff" version = "v1" groupVersion = group + "/" + version kind = "coolresource" namespace = "coolns" name = "cool" uid = types.UID("definitely-a-uuid") ) func TestReferenceTo(t *testing.T) { type args struct { o metav1.Object of schema.GroupVersionKind } tests := map[string]struct { args want *corev1.ObjectReference }{ "WithTypeMeta": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, UID: uid, }, }, of: schema.GroupVersionKind{ Group: group, Version: version, Kind: kind, }, }, want: &corev1.ObjectReference{ APIVersion: groupVersion, Kind: kind, Namespace: namespace, Name: name, UID: uid, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { got := ReferenceTo(tc.o, tc.of) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("ReferenceTo(): -want, +got:\n%s", diff) } }) } } func TestTypedReferenceTo(t *testing.T) { type args struct { o metav1.Object of schema.GroupVersionKind } tests := map[string]struct { args want *xpv2.TypedReference }{ "WithTypeMeta": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, UID: uid, }, }, of: schema.GroupVersionKind{ Group: group, Version: version, Kind: kind, }, }, want: &xpv2.TypedReference{ APIVersion: groupVersion, Kind: kind, Name: name, UID: uid, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { got := TypedReferenceTo(tc.o, tc.of) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("TypedReferenceTo(): -want, +got:\n%s", diff) } }) } } func TestAsOwner(t *testing.T) { tests := map[string]struct { r *xpv2.TypedReference want metav1.OwnerReference }{ "Successful": { r: &xpv2.TypedReference{ APIVersion: groupVersion, Kind: kind, Name: name, UID: uid, }, want: metav1.OwnerReference{ APIVersion: groupVersion, Kind: kind, Name: name, UID: uid, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { got := AsOwner(tc.r) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("AsOwner(): -want, +got:\n%s", diff) } }) } } func TestAsController(t *testing.T) { flag := true tests := map[string]struct { r *xpv2.TypedReference want metav1.OwnerReference }{ "Successful": { r: &xpv2.TypedReference{ APIVersion: groupVersion, Kind: kind, Name: name, UID: uid, }, want: metav1.OwnerReference{ APIVersion: groupVersion, Kind: kind, Name: name, UID: uid, Controller: &flag, BlockOwnerDeletion: &flag, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { got := AsController(tc.r) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("AsController(): -want, +got:\n%s", diff) } }) } } func TestHaveSameController(t *testing.T) { controller := true controllerA := metav1.OwnerReference{ UID: uid, Controller: &controller, } controllerB := metav1.OwnerReference{ UID: types.UID("a-different-uuid"), Controller: &controller, } cases := map[string]struct { a metav1.Object b metav1.Object want bool }{ "SameController": { a: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{controllerA}, }, }, b: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{controllerA}, }, }, want: true, }, "AHasNoController": { a: &corev1.Pod{}, b: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{controllerB}, }, }, want: false, }, "BHasNoController": { a: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{controllerA}, }, }, b: &corev1.Pod{}, want: false, }, "ControllersDiffer": { a: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{controllerA}, }, }, b: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{controllerB}, }, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := HaveSameController(tc.a, tc.b) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("HaveSameController(...): -want, +got:\n%s", diff) } }) } } func TestNamespacedNameOf(t *testing.T) { cases := map[string]struct { r *corev1.ObjectReference want types.NamespacedName }{ "Success": { r: &corev1.ObjectReference{Namespace: namespace, Name: name}, want: types.NamespacedName{Namespace: namespace, Name: name}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := NamespacedNameOf(tc.r) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("NamespacedNameOf(...): -want, +got:\n%s", diff) } }) } } func TestAddOwnerReference(t *testing.T) { owner := metav1.OwnerReference{UID: uid} other := metav1.OwnerReference{UID: "a-different-uuid"} ctrlr := metav1.OwnerReference{UID: uid, Controller: func() *bool { c := true; return &c }()} type args struct { o metav1.Object r metav1.OwnerReference } cases := map[string]struct { args args want []metav1.OwnerReference }{ "NoExistingOwners": { args: args{ o: &corev1.Pod{}, r: owner, }, want: []metav1.OwnerReference{owner}, }, "UpdateExistingOwner": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{ctrlr}, }, }, r: owner, }, want: []metav1.OwnerReference{owner}, }, "OwnedByAnotherObject": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{other}, }, }, r: owner, }, want: []metav1.OwnerReference{other, owner}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { AddOwnerReference(tc.args.o, tc.args.r) got := tc.args.o.GetOwnerReferences() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("tc.args.o.GetOwnerReferences(...): -want, +got:\n%s", diff) } }) } } func TestAddControllerReference(t *testing.T) { owner := metav1.OwnerReference{UID: uid} other := metav1.OwnerReference{UID: "a-different-uuid"} ctrlr := metav1.OwnerReference{UID: uid, Controller: func() *bool { c := true; return &c }()} otrlr := metav1.OwnerReference{ Kind: "lame", Name: "othercontroller", UID: "a-different-uuid", Controller: func() *bool { c := true; return &c }(), } type args struct { o metav1.Object r metav1.OwnerReference } type want struct { owners []metav1.OwnerReference err error } cases := map[string]struct { args args want want }{ "NoExistingOwners": { args: args{ o: &corev1.Pod{}, r: owner, }, want: want{ owners: []metav1.OwnerReference{owner}, }, }, "UpdateExistingOwner": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{ctrlr}, }, }, r: owner, }, want: want{ owners: []metav1.OwnerReference{owner}, }, }, "OwnedByAnotherObject": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{other}, }, }, r: owner, }, want: want{ owners: []metav1.OwnerReference{other, owner}, }, }, "ControlledByAnotherObject": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, OwnerReferences: []metav1.OwnerReference{otrlr}, }, }, r: owner, }, want: want{ owners: []metav1.OwnerReference{otrlr}, err: errors.Errorf("%s is already controlled by %s %s (UID %s)", name, otrlr.Kind, otrlr.Name, otrlr.UID), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := AddControllerReference(tc.args.o, tc.args.r) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("AddControllerReference(...): -want error, +got error:\n%s", diff) } got := tc.args.o.GetOwnerReferences() if diff := cmp.Diff(tc.want.owners, got); diff != "" { t.Errorf("tc.args.o.GetOwnerReferences(...): -want, +got:\n%s", diff) } }) } } func TestAddFinalizer(t *testing.T) { finalizer := "fin" funalizer := "fun" type args struct { o metav1.Object finalizer string } cases := map[string]struct { args args want []string }{ "NoExistingFinalizers": { args: args{ o: &corev1.Pod{}, finalizer: finalizer, }, want: []string{finalizer}, }, "FinalizerAlreadyExists": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Finalizers: []string{finalizer}, }, }, finalizer: finalizer, }, want: []string{finalizer}, }, "AnotherFinalizerExists": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Finalizers: []string{funalizer}, }, }, finalizer: finalizer, }, want: []string{funalizer, finalizer}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { AddFinalizer(tc.args.o, tc.args.finalizer) got := tc.args.o.GetFinalizers() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("tc.args.o.GetFinalizers(...): -want, +got:\n%s", diff) } }) } } func TestRemoveFinalizer(t *testing.T) { finalizer := "fin" funalizer := "fun" type args struct { o metav1.Object finalizer string } cases := map[string]struct { args args want []string }{ "NoExistingFinalizers": { args: args{ o: &corev1.Pod{}, finalizer: finalizer, }, want: nil, }, "FinalizerExists": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Finalizers: []string{finalizer}, }, }, finalizer: finalizer, }, want: []string{}, }, "AnotherFinalizerExists": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Finalizers: []string{finalizer, funalizer}, }, }, finalizer: finalizer, }, want: []string{funalizer}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { RemoveFinalizer(tc.args.o, tc.args.finalizer) got := tc.args.o.GetFinalizers() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("tc.args.o.GetFinalizers(...): -want, +got:\n%s", diff) } }) } } func TestFinalizerExists(t *testing.T) { finalizer := "fin" funalizer := "fun" type args struct { o metav1.Object finalizer string } cases := map[string]struct { args args want bool }{ "NoExistingFinalizers": { args: args{ o: &corev1.Pod{}, finalizer: finalizer, }, want: false, }, "FinalizerExists": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Finalizers: []string{finalizer}, }, }, finalizer: finalizer, }, want: true, }, "AnotherFinalizerExists": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Finalizers: []string{funalizer}, }, }, finalizer: finalizer, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { if diff := cmp.Diff(tc.want, FinalizerExists(tc.args.o, tc.args.finalizer)); diff != "" { t.Errorf("tc.args.o.GetFinalizers(...): -want, +got:\n%s", diff) } }) } } func TestAddLabels(t *testing.T) { key, value := "key", "value" existingKey, existingValue := "ekey", "evalue" type args struct { o metav1.Object labels map[string]string } cases := map[string]struct { args args want map[string]string }{ "ExistingLabels": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ existingKey: existingValue, }, }, }, labels: map[string]string{key: value}, }, want: map[string]string{ existingKey: existingValue, key: value, }, }, "NoExistingLabels": { args: args{ o: &corev1.Pod{}, labels: map[string]string{key: value}, }, want: map[string]string{key: value}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { AddLabels(tc.args.o, tc.args.labels) got := tc.args.o.GetLabels() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("tc.args.o.GetLabels(...): -want, +got:\n%s", diff) } }) } } func TestRemoveLabels(t *testing.T) { keyA, valueA := "keyA", "valueA" keyB, valueB := "keyB", "valueB" type args struct { o metav1.Object labels []string } cases := map[string]struct { args args want map[string]string }{ "ExistingLabels": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ keyA: valueA, keyB: valueB, }, }, }, labels: []string{keyA}, }, want: map[string]string{keyB: valueB}, }, "NoExistingLabels": { args: args{ o: &corev1.Pod{}, labels: []string{keyA}, }, want: nil, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { RemoveLabels(tc.args.o, tc.args.labels...) got := tc.args.o.GetLabels() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("tc.args.o.GetLabels(...): -want, +got:\n%s", diff) } }) } } func TestAddAnnotations(t *testing.T) { key, value := "key", "value" existingKey, existingValue := "ekey", "evalue" type args struct { o metav1.Object annotations map[string]string } cases := map[string]struct { args args want map[string]string }{ "ExistingAnnotations": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ existingKey: existingValue, }, }, }, annotations: map[string]string{key: value}, }, want: map[string]string{ existingKey: existingValue, key: value, }, }, "NoExistingAnnotations": { args: args{ o: &corev1.Pod{}, annotations: map[string]string{key: value}, }, want: map[string]string{key: value}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { AddAnnotations(tc.args.o, tc.args.annotations) got := tc.args.o.GetAnnotations() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("tc.args.o.GetAnnotations(...): -want, +got:\n%s", diff) } }) } } func TestRemoveAnnotations(t *testing.T) { keyA, valueA := "keyA", "valueA" keyB, valueB := "keyB", "valueB" type args struct { o metav1.Object annotations []string } cases := map[string]struct { args args want map[string]string }{ "ExistingAnnotations": { args: args{ o: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ keyA: valueA, keyB: valueB, }, }, }, annotations: []string{keyA}, }, want: map[string]string{keyB: valueB}, }, "NoExistingAnnotations": { args: args{ o: &corev1.Pod{}, annotations: []string{keyA}, }, want: nil, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { RemoveAnnotations(tc.args.o, tc.args.annotations...) got := tc.args.o.GetAnnotations() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("tc.args.o.GetAnnotations(...): -want, +got:\n%s", diff) } }) } } func TestWasDeleted(t *testing.T) { now := metav1.Now() cases := map[string]struct { o metav1.Object want bool }{ "ObjectWasDeleted": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}}, want: true, }, "ObjectWasNotDeleted": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: nil}}, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := WasDeleted(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("WasDeleted(...): -want, +got:\n%s", diff) } }) } } func TestWasCreated(t *testing.T) { now := metav1.Now() zero := metav1.Time{} cases := map[string]struct { o metav1.Object want bool }{ "ObjectWasCreated": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{CreationTimestamp: now}}, want: true, }, "ObjectWasNotCreated": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{CreationTimestamp: zero}}, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := WasCreated(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("WasCreated(...): -want, +got:\n%s", diff) } }) } } func TestGetExternalName(t *testing.T) { cases := map[string]struct { o metav1.Object want string }{ "ExternalNameExists": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalName: name}}}, want: name, }, "NoExternalName": { o: &corev1.Pod{}, want: "", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := GetExternalName(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("GetExternalName(...): -want, +got:\n%s", diff) } }) } } func TestSetExternalName(t *testing.T) { cases := map[string]struct { o metav1.Object name string want metav1.Object }{ "SetsTheCorrectKey": { o: &corev1.Pod{}, name: name, want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalName: name}}}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { SetExternalName(tc.o, tc.name) if diff := cmp.Diff(tc.want, tc.o); diff != "" { t.Errorf("SetExternalName(...): -want, +got:\n%s", diff) } }) } } func TestGetExternalCreatePending(t *testing.T) { now := time.Now().Round(time.Second) cases := map[string]struct { o metav1.Object want time.Time }{ "ExternalCreatePendingExists": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreatePending: now.Format(time.RFC3339)}}}, want: now, }, "NoExternalCreatePending": { o: &corev1.Pod{}, want: time.Time{}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := GetExternalCreatePending(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("GetExternalCreatePending(...): -want, +got:\n%s", diff) } }) } } func TestSetExternalCreatePending(t *testing.T) { now := time.Now() cases := map[string]struct { o metav1.Object t time.Time want metav1.Object }{ "SetsTheCorrectKey": { o: &corev1.Pod{}, t: now, want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreatePending: now.Format(time.RFC3339)}}}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { SetExternalCreatePending(tc.o, tc.t) if diff := cmp.Diff(tc.want, tc.o); diff != "" { t.Errorf("SetExternalCreatePending(...): -want, +got:\n%s", diff) } }) } } func TestGetExternalCreateSucceeded(t *testing.T) { now := time.Now().Round(time.Second) cases := map[string]struct { o metav1.Object want time.Time }{ "ExternalCreateTimeExists": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateSucceeded: now.Format(time.RFC3339)}}}, want: now, }, "NoExternalCreateTime": { o: &corev1.Pod{}, want: time.Time{}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := GetExternalCreateSucceeded(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("GetExternalCreateSucceeded(...): -want, +got:\n%s", diff) } }) } } func TestSetExternalCreateSucceeded(t *testing.T) { now := time.Now() cases := map[string]struct { o metav1.Object t time.Time want metav1.Object }{ "SetsTheCorrectKey": { o: &corev1.Pod{}, t: now, want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateSucceeded: now.Format(time.RFC3339)}}}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { SetExternalCreateSucceeded(tc.o, tc.t) if diff := cmp.Diff(tc.want, tc.o); diff != "" { t.Errorf("SetExternalCreateSucceeded(...): -want, +got:\n%s", diff) } }) } } func TestGetExternalCreateFailed(t *testing.T) { now := time.Now().Round(time.Second) cases := map[string]struct { o metav1.Object want time.Time }{ "ExternalCreateFailedExists": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateFailed: now.Format(time.RFC3339)}}}, want: now, }, "NoExternalCreateFailed": { o: &corev1.Pod{}, want: time.Time{}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := GetExternalCreateFailed(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("GetExternalCreateFailed(...): -want, +got:\n%s", diff) } }) } } func TestSetExternalCreateFailed(t *testing.T) { now := time.Now() cases := map[string]struct { o metav1.Object t time.Time want metav1.Object }{ "SetsTheCorrectKey": { o: &corev1.Pod{}, t: now, want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateFailed: now.Format(time.RFC3339)}}}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { SetExternalCreateFailed(tc.o, tc.t) if diff := cmp.Diff(tc.want, tc.o); diff != "" { t.Errorf("SetExternalCreateFailed(...): -want, +got:\n%s", diff) } }) } } func TestExternalCreateSucceededDuring(t *testing.T) { type args struct { o metav1.Object d time.Duration } cases := map[string]struct { args args want bool }{ "NotYetSuccessfullyCreated": { args: args{ o: &corev1.Pod{}, d: 1 * time.Minute, }, want: false, }, "SuccessfullyCreatedTooLongAgo": { args: args{ o: func() metav1.Object { o := &corev1.Pod{} t := time.Now().Add(-2 * time.Minute) SetExternalCreateSucceeded(o, t) return o }(), d: 1 * time.Minute, }, want: false, }, "SuccessfullyCreatedWithinDuration": { args: args{ o: func() metav1.Object { o := &corev1.Pod{} t := time.Now().Add(-30 * time.Second) SetExternalCreateSucceeded(o, t) return o }(), d: 1 * time.Minute, }, want: true, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := ExternalCreateSucceededDuring(tc.args.o, tc.args.d) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("ExternalCreateSucceededDuring(...): -want, +got:\n%s", diff) } }) } } func TestExternalCreateIncomplete(t *testing.T) { now := time.Now().Format(time.RFC3339) earlier := time.Now().Add(-1 * time.Second).Format(time.RFC3339) evenEarlier := time.Now().Add(-1 * time.Minute).Format(time.RFC3339) cases := map[string]struct { reason string o metav1.Object want bool }{ "CreateNeverPending": { reason: "If we've never called Create it can't be incomplete.", o: &corev1.Pod{}, want: false, }, "CreateSucceeded": { reason: "If Create succeeded since it was pending, it's complete.", o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyExternalCreateFailed: evenEarlier, AnnotationKeyExternalCreatePending: earlier, AnnotationKeyExternalCreateSucceeded: now, }}}, want: false, }, "CreateFailed": { reason: "If Create failed since it was pending, it's complete.", o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyExternalCreateSucceeded: evenEarlier, AnnotationKeyExternalCreatePending: earlier, AnnotationKeyExternalCreateFailed: now, }}}, want: false, }, "CreateNeverCompleted": { reason: "If Create was pending but never succeeded or failed, it's incomplete.", o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyExternalCreatePending: earlier, }}}, want: true, }, "RecreateNeverCompleted": { reason: "If Create is pending and there's an older success we're probably trying to recreate a deleted external resource, and it's incomplete.", o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyExternalCreateSucceeded: earlier, AnnotationKeyExternalCreatePending: now, }}}, want: true, }, "RetryNeverCompleted": { reason: "If Create is pending and there's an older failure we're probably trying to recreate a deleted external resource, and it's incomplete.", o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyExternalCreateFailed: earlier, AnnotationKeyExternalCreatePending: now, }}}, want: true, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := ExternalCreateIncomplete(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("ExternalCreateIncomplete(...): -want, +got:\n%s", diff) } }) } } func TestIsPaused(t *testing.T) { cases := map[string]struct { o metav1.Object want bool }{ "HasPauseAnnotationSetTrue": { o: func() metav1.Object { p := &corev1.Pod{} p.SetAnnotations(map[string]string{ AnnotationKeyReconciliationPaused: "true", }) return p }(), want: true, }, "NoPauseAnnotation": { o: &corev1.Pod{}, want: false, }, "HasEmptyPauseAnnotation": { o: func() metav1.Object { p := &corev1.Pod{} p.SetAnnotations(map[string]string{ AnnotationKeyReconciliationPaused: "", }) return p }(), want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := IsPaused(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("IsPaused(...): -want, +got:\n%s", diff) } }) } } func TestGetPollInterval(t *testing.T) { cases := map[string]struct { o metav1.Object wantDur time.Duration wantBool bool }{ "NoAnnotation": { o: &corev1.Pod{}, }, "EmptyAnnotation": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyPollInterval: "", }}}, }, "InvalidDuration": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyPollInterval: "not-a-duration", }}}, }, "NegativeDuration": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyPollInterval: "-5m", }}}, }, "ZeroDuration": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyPollInterval: "0s", }}}, }, "ValidDuration": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyPollInterval: "24h", }}}, wantDur: 24 * time.Hour, wantBool: true, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { d, ok := GetPollInterval(tc.o) if diff := cmp.Diff(tc.wantBool, ok); diff != "" { t.Errorf("GetPollInterval(...) ok: -want, +got:\n%s", diff) } if diff := cmp.Diff(tc.wantDur, d); diff != "" { t.Errorf("GetPollInterval(...) duration: -want, +got:\n%s", diff) } }) } } func TestGetReconcileRequest(t *testing.T) { cases := map[string]struct { o metav1.Object wantToken string wantBool bool }{ "NoAnnotation": { o: &corev1.Pod{}, }, "EmptyAnnotation": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyReconcileRequestedAt: "", }}}, }, "HasToken": { o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ AnnotationKeyReconcileRequestedAt: "2024-01-15T10:30:00Z", }}}, wantToken: "2024-01-15T10:30:00Z", wantBool: true, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { token, ok := GetReconcileRequest(tc.o) if diff := cmp.Diff(tc.wantBool, ok); diff != "" { t.Errorf("GetReconcileRequest(...) ok: -want, +got:\n%s", diff) } if diff := cmp.Diff(tc.wantToken, token); diff != "" { t.Errorf("GetReconcileRequest(...) token: -want, +got:\n%s", diff) } }) } } func TestSetReconcileRequest(t *testing.T) { o := &corev1.Pod{} SetReconcileRequest(o, "my-token") got := o.GetAnnotations()[AnnotationKeyReconcileRequestedAt] if diff := cmp.Diff("my-token", got); diff != "" { t.Errorf("SetReconcileRequest(...): -want, +got:\n%s", diff) } } ================================================ FILE: pkg/password/password.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package password contains a simple password generator. package password import ( "crypto/rand" "math/big" ) // Settings for password generation. type Settings struct { // CharacterSet of allowed password characters. CharacterSet string // Length of generated passwords. Length int } // Default password generation settings. // //nolint:gochecknoglobals // We treat this as a constant. var Default = Settings{ CharacterSet: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", Length: 27, } // Generate a random, 27 character password that may consist of lowercase // letters, uppercase letters, or numbers. func Generate() (string, error) { return Default.Generate() } // Generate a password. func (s Settings) Generate() (string, error) { pw := make([]byte, s.Length) for i := range s.Length { n, err := rand.Int(rand.Reader, big.NewInt(int64(len(s.CharacterSet)))) if err != nil { return "", err } pw[i] = s.CharacterSet[n.Int64()] } return string(pw), nil } ================================================ FILE: pkg/password/password_test.go ================================================ package password import ( "testing" "github.com/google/go-cmp/cmp" ) func TestGenerate(t *testing.T) { // ¯\_(ツ)_/¯ want := "aaa" got, err := Settings{CharacterSet: "a", Length: 3}.Generate() if diff := cmp.Diff(want, got); diff != "" { t.Errorf("Generate(): -want, +got:\n%s", diff) } if err != nil { t.Errorf("Generate: %s\n", err) } } ================================================ FILE: pkg/ratelimiter/default.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package ratelimiter contains suggested default ratelimiters for Crossplane. package ratelimiter import ( "time" "golang.org/x/time/rate" "k8s.io/client-go/rest" "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // NewGlobal returns a token bucket rate limiter meant for limiting the number // of average total requeues per second for all controllers registered with a // controller manager. The bucket size (i.e. allowed burst) is rps * 10. func NewGlobal(rps int) *BucketRateLimiter { return &workqueue.TypedBucketRateLimiter[string]{Limiter: rate.NewLimiter(rate.Limit(rps), rps*10)} } // ControllerRateLimiter to work with [sigs.k8s.io/controller-runtime/pkg/controller.Options]. type ControllerRateLimiter = workqueue.TypedRateLimiter[reconcile.Request] // NewController returns a rate limiter that takes the maximum delay between the // passed rate limiter and a per-item exponential backoff limiter. The // exponential backoff limiter has a base delay of 1s and a maximum of 60s. func NewController() ControllerRateLimiter { return workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](1*time.Second, 60*time.Second) } // LimitRESTConfig returns a copy of the supplied REST config with rate limits // derived from the supplied rate of reconciles per second. func LimitRESTConfig(cfg *rest.Config, rps int) *rest.Config { // The Kubernetes controller manager and controller-runtime controller // managers use 20qps with 30 burst. We default to 10 reconciles per // second so our defaults are designed to accommodate that. out := rest.CopyConfig(cfg) out.QPS = float32(rps * 5) out.Burst = rps * 10 return out } ================================================ FILE: pkg/ratelimiter/reconciler.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ratelimiter import ( "context" "sync" "time" "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // BucketRateLimiter for a standard crossplane reconciler. type BucketRateLimiter = workqueue.TypedBucketRateLimiter[string] // RateLimiter for a standard crossplane reconciler. type RateLimiter = workqueue.TypedRateLimiter[string] // A Reconciler rate limits an inner, wrapped Reconciler. Requests that are rate // limited immediately return RequeueAfter: d without calling the wrapped // Reconciler, where d is imposed by the rate limiter. type Reconciler struct { name string inner reconcile.Reconciler limit RateLimiter limited map[string]struct{} limitedL sync.RWMutex } // NewReconciler wraps the supplied Reconciler, ensuring requests are passed to // it no more frequently than the supplied RateLimiter allows. Multiple uniquely // named Reconcilers can share the same RateLimiter. func NewReconciler(name string, r reconcile.Reconciler, l RateLimiter) *Reconciler { return &Reconciler{name: name, inner: r, limit: l, limited: make(map[string]struct{})} } // Reconcile the supplied request subject to rate limiting. func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { item := r.name + req.String() if d := r.when(req); d > 0 { return reconcile.Result{RequeueAfter: d}, nil } r.limit.Forget(item) return r.inner.Reconcile(ctx, req) } // when adapts the upstream rate limiter's 'When' method such that rate limited // requests can call it again when they return and will be allowed to proceed // immediately without being subject to further rate limiting. It is optimised // for handling requests that have not been and will not be rate limited without // blocking. func (r *Reconciler) when(req reconcile.Request) time.Duration { item := r.name + req.String() r.limitedL.RLock() _, limited := r.limited[item] r.limitedL.RUnlock() // If we already rate limited this request we trust that it complied and // let it pass immediately. if limited { r.limitedL.Lock() delete(r.limited, item) r.limitedL.Unlock() return 0 } d := r.limit.When(item) // Record that this request was rate limited so that we can let it // through immediately when it requeues after the supplied duration. if d != 0 { r.limitedL.Lock() r.limited[item] = struct{}{} r.limitedL.Unlock() } return d } ================================================ FILE: pkg/ratelimiter/reconciler_test.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ratelimiter import ( "context" "testing" "time" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var _ RateLimiter = &predictableRateLimiter{} type predictableRateLimiter struct{ d time.Duration } func (r *predictableRateLimiter) When(_ string) time.Duration { return r.d } func (r *predictableRateLimiter) Forget(_ string) {} func (r *predictableRateLimiter) NumRequeues(_ string) int { return 0 } func TestReconcile(t *testing.T) { type args struct { ctx context.Context req reconcile.Request } type want struct { res reconcile.Result err error } cases := map[string]struct { reason string r reconcile.Reconciler args args want want }{ "NotRateLimited": { reason: "Requests that are not rate limited should be passed to the inner Reconciler.", r: NewReconciler("test", reconcile.Func(func(_ context.Context, _ reconcile.Request) (reconcile.Result, error) { return reconcile.Result{Requeue: true}, nil }), &predictableRateLimiter{}), want: want{ res: reconcile.Result{Requeue: true}, err: nil, }, }, "RateLimited": { reason: "Requests that are rate limited should be requeued after the duration specified by the RateLimiter.", r: NewReconciler("test", nil, &predictableRateLimiter{d: 8 * time.Second}), want: want{ res: reconcile.Result{RequeueAfter: 8 * time.Second}, err: nil, }, }, "Returning": { reason: "Returning requests that were previously rate limited should be allowed through without further rate limiting.", r: func() reconcile.Reconciler { inner := reconcile.Func(func(_ context.Context, _ reconcile.Request) (reconcile.Result, error) { return reconcile.Result{Requeue: true}, nil }) // Rate limit the request once. r := NewReconciler("test", inner, &predictableRateLimiter{d: 8 * time.Second}) r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "limited"}}) return r }(), args: args{ ctx: context.Background(), req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "limited"}}, }, want: want{ res: reconcile.Result{Requeue: true}, err: nil, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := tc.r.Reconcile(tc.args.ctx, tc.args.req) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("%s\nr.Reconcile(...): -want, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.res, got); diff != "" { t.Errorf("%s\nr.Reconcile(...): -want, +got result:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/reconciler/customresourcesgate/reconciler.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package customresourcesgate implements a CustomResourceReconciler to report GKVs status to a Gate. // This reconciler requires cluster scoped GET,LIST,WATCH on customresourcedefinitions[apiextensions.k8s.io] package customresourcesgate import ( "context" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" "github.com/crossplane/crossplane-runtime/v2/pkg/controller" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" ) // Reconciler reconciles a CustomResourceDefinitions in order to gate and wait // on CRD readiness to start downstream controllers. type Reconciler struct { log logging.Logger gate controller.Gate } // Reconcile reconciles CustomResourceDefinitions and reports ready and unready GVKs to the gate. func (r *Reconciler) Reconcile(_ context.Context, crd *apiextensionsv1.CustomResourceDefinition) (ctrl.Result, error) { established := isEstablished(crd) gkvs := toGVKs(crd) switch { // CRD is not ready or being deleted. case !established || !crd.GetDeletionTimestamp().IsZero(): for gvk := range gkvs { r.log.Debug("gvk is not ready", "gvk", gvk) r.gate.Set(gvk, false) } return ctrl.Result{}, nil // CRD is ready. default: for gvk, served := range gkvs { if served { r.log.Debug("gvk is ready", "gvk", gvk) r.gate.Set(gvk, true) } } } return ctrl.Result{}, nil } func toGVKs(crd *apiextensionsv1.CustomResourceDefinition) map[schema.GroupVersionKind]bool { gvks := make(map[schema.GroupVersionKind]bool, len(crd.Spec.Versions)) for _, version := range crd.Spec.Versions { gvks[schema.GroupVersionKind{Group: crd.Spec.Group, Version: version.Name, Kind: crd.Spec.Names.Kind}] = version.Served } return gvks } func isEstablished(crd *apiextensionsv1.CustomResourceDefinition) bool { if len(crd.Status.Conditions) > 0 { for _, cond := range crd.Status.Conditions { if cond.Type == apiextensionsv1.Established { return cond.Status == apiextensionsv1.ConditionTrue } } } return false } ================================================ FILE: pkg/reconciler/customresourcesgate/reconciler_test.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package customresourcesgate import ( "context" "slices" "strings" "testing" "github.com/google/go-cmp/cmp" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestToGVKs(t *testing.T) { type args struct { crd *apiextensionsv1.CustomResourceDefinition } type want struct { gvks map[schema.GroupVersionKind]bool } cases := map[string]struct { reason string args args want want }{ "SingleVersionServed": { reason: "Should return single GVK for CRD with one served version", args: args{ crd: &apiextensionsv1.CustomResourceDefinition{ Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "example.com", Names: apiextensionsv1.CustomResourceDefinitionNames{ Kind: "TestResource", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ {Name: "v1", Served: true}, }, }, }, }, want: want{ gvks: map[schema.GroupVersionKind]bool{ {Group: "example.com", Version: "v1", Kind: "TestResource"}: true, }, }, }, "MultipleVersionsWithServedStatus": { reason: "Should return GVKs with correct served status for multiple versions", args: args{ crd: &apiextensionsv1.CustomResourceDefinition{ Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "example.com", Names: apiextensionsv1.CustomResourceDefinitionNames{ Kind: "TestResource", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ {Name: "v1alpha1", Served: false}, {Name: "v1beta1", Served: true}, {Name: "v1", Served: true}, }, }, }, }, want: want{ gvks: map[schema.GroupVersionKind]bool{ {Group: "example.com", Version: "v1alpha1", Kind: "TestResource"}: false, {Group: "example.com", Version: "v1beta1", Kind: "TestResource"}: true, {Group: "example.com", Version: "v1", Kind: "TestResource"}: true, }, }, }, "NoVersions": { reason: "Should return empty map for CRD with no versions", args: args{ crd: &apiextensionsv1.CustomResourceDefinition{ Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "example.com", Names: apiextensionsv1.CustomResourceDefinitionNames{ Kind: "TestResource", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{}, }, }, }, want: want{ gvks: map[schema.GroupVersionKind]bool{}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := toGVKs(tc.args.crd) if diff := cmp.Diff(tc.want.gvks, got); diff != "" { t.Errorf("\n%s\ntoGVKs(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestIsEstablished(t *testing.T) { type args struct { crd *apiextensionsv1.CustomResourceDefinition } type want struct { established bool } cases := map[string]struct { reason string args args want want }{ "EstablishedTrue": { reason: "Should return true when CRD has Established condition with True status", args: args{ crd: &apiextensionsv1.CustomResourceDefinition{ Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionTrue, }, }, }, }, }, want: want{ established: true, }, }, "EstablishedFalse": { reason: "Should return false when CRD has Established condition with False status", args: args{ crd: &apiextensionsv1.CustomResourceDefinition{ Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionFalse, }, }, }, }, }, want: want{ established: false, }, }, "EstablishedUnknown": { reason: "Should return false when CRD has Established condition with Unknown status", args: args{ crd: &apiextensionsv1.CustomResourceDefinition{ Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionUnknown, }, }, }, }, }, want: want{ established: false, }, }, "NoEstablishedCondition": { reason: "Should return false when CRD has no Established condition", args: args{ crd: &apiextensionsv1.CustomResourceDefinition{ Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.NamesAccepted, Status: apiextensionsv1.ConditionTrue, }, }, }, }, }, want: want{ established: false, }, }, "NoConditions": { reason: "Should return false when CRD has no conditions", args: args{ crd: &apiextensionsv1.CustomResourceDefinition{ Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{}, }, }, }, want: want{ established: false, }, }, "MultipleConditions": { reason: "Should return true when CRD has multiple conditions including Established=True", args: args{ crd: &apiextensionsv1.CustomResourceDefinition{ Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.NamesAccepted, Status: apiextensionsv1.ConditionTrue, }, { Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionTrue, }, { Type: apiextensionsv1.Terminating, Status: apiextensionsv1.ConditionFalse, }, }, }, }, }, want: want{ established: true, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := isEstablished(tc.args.crd) if diff := cmp.Diff(tc.want.established, got); diff != "" { t.Errorf("\n%s\nisEstablished(...): -want, +got:\n%s", tc.reason, diff) } }) } } // MockGate implements the controller.Gate interface for testing. type MockGate struct { TrueCalls []schema.GroupVersionKind FalseCalls []schema.GroupVersionKind } func NewMockGate() *MockGate { return &MockGate{ TrueCalls: make([]schema.GroupVersionKind, 0), FalseCalls: make([]schema.GroupVersionKind, 0), } } func (m *MockGate) Set(gvk schema.GroupVersionKind, value bool) bool { if value { if m.TrueCalls == nil { m.TrueCalls = make([]schema.GroupVersionKind, 0) } m.TrueCalls = append(m.TrueCalls, gvk) } else { if m.FalseCalls == nil { m.FalseCalls = make([]schema.GroupVersionKind, 0) } m.FalseCalls = append(m.FalseCalls, gvk) } return true } func (m *MockGate) Register(func(), ...schema.GroupVersionKind) {} func TestReconcile(t *testing.T) { now := metav1.Now() type fields struct { gate *MockGate } type args struct { ctx context.Context crd *apiextensionsv1.CustomResourceDefinition } type want struct { result ctrl.Result err error trueCalls []schema.GroupVersionKind falseCalls []schema.GroupVersionKind } cases := map[string]struct { reason string fields fields args args want want }{ "EstablishedCRDCallsGateTrue": { reason: "Should call gate.True for all GVKs when CRD is established", fields: fields{ gate: NewMockGate(), }, args: args{ ctx: context.Background(), crd: &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "testresources.example.com", }, Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "example.com", Names: apiextensionsv1.CustomResourceDefinitionNames{ Kind: "TestResource", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ {Name: "v1alpha1", Served: true}, {Name: "v1", Served: true}, }, }, Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionTrue, }, }, }, }, }, want: want{ result: ctrl.Result{}, err: nil, trueCalls: []schema.GroupVersionKind{ {Group: "example.com", Version: "v1alpha1", Kind: "TestResource"}, {Group: "example.com", Version: "v1", Kind: "TestResource"}, }, falseCalls: []schema.GroupVersionKind{}, }, }, "NotEstablishedCRDCallsGateFalse": { reason: "Should call gate.False for all GVKs when CRD is not established", fields: fields{ gate: NewMockGate(), }, args: args{ ctx: context.Background(), crd: &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "testresources.example.com", }, Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "example.com", Names: apiextensionsv1.CustomResourceDefinitionNames{ Kind: "TestResource", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ {Name: "v1", Served: true}, }, }, Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionFalse, }, }, }, }, }, want: want{ result: ctrl.Result{}, err: nil, trueCalls: []schema.GroupVersionKind{}, falseCalls: []schema.GroupVersionKind{ {Group: "example.com", Version: "v1", Kind: "TestResource"}, }, }, }, "DeletingCRDCallsGateFalse": { reason: "Should call gate.False for all GVKs when CRD is being deleted", fields: fields{ gate: NewMockGate(), }, args: args{ ctx: context.Background(), crd: &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "testresources.example.com", DeletionTimestamp: &now, }, Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "example.com", Names: apiextensionsv1.CustomResourceDefinitionNames{ Kind: "TestResource", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ {Name: "v1", Served: true}, }, }, Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionTrue, }, }, }, }, }, want: want{ result: ctrl.Result{}, err: nil, trueCalls: []schema.GroupVersionKind{}, falseCalls: []schema.GroupVersionKind{ {Group: "example.com", Version: "v1", Kind: "TestResource"}, }, }, }, "MixedServedVersions": { reason: "Should only call gate.True for served versions", fields: fields{ gate: NewMockGate(), }, args: args{ ctx: context.Background(), crd: &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "testresources.example.com", }, Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "example.com", Names: apiextensionsv1.CustomResourceDefinitionNames{ Kind: "TestResource", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ {Name: "v1alpha1", Served: false}, {Name: "v1", Served: true}, }, }, Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionTrue, }, }, }, }, }, want: want{ result: ctrl.Result{}, err: nil, trueCalls: []schema.GroupVersionKind{ {Group: "example.com", Version: "v1", Kind: "TestResource"}, }, falseCalls: []schema.GroupVersionKind{}, }, }, "NoVersionsCRD": { reason: "Should handle CRD with no versions gracefully", fields: fields{ gate: NewMockGate(), }, args: args{ ctx: context.Background(), crd: &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "testresources.example.com", }, Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "example.com", Names: apiextensionsv1.CustomResourceDefinitionNames{ Kind: "TestResource", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{}, }, Status: apiextensionsv1.CustomResourceDefinitionStatus{ Conditions: []apiextensionsv1.CustomResourceDefinitionCondition{ { Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionTrue, }, }, }, }, }, want: want{ result: ctrl.Result{}, err: nil, trueCalls: []schema.GroupVersionKind{}, falseCalls: []schema.GroupVersionKind{}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := &Reconciler{ log: logging.NewNopLogger(), gate: tc.fields.gate, } got, err := r.Reconcile(tc.args.ctx, tc.args.crd) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nr.Reconcile(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.result, got); diff != "" { t.Errorf("\n%s\nr.Reconcile(...): -want result, +got result:\n%s", tc.reason, diff) } // Only check gate calls if gate is not nil if tc.fields.gate != nil { sortGVK := func(a, b schema.GroupVersionKind) int { if c := strings.Compare(a.Group, b.Group); c != 0 { return c } if c := strings.Compare(a.Version, b.Version); c != 0 { return c } return strings.Compare(a.Kind, b.Kind) } slices.SortFunc(tc.want.trueCalls, sortGVK) slices.SortFunc(tc.fields.gate.TrueCalls, sortGVK) if diff := cmp.Diff(tc.want.trueCalls, tc.fields.gate.TrueCalls); diff != "" { t.Errorf("\n%s\ngate.True calls: -want, +got:\n%s", tc.reason, diff) } slices.SortFunc(tc.want.falseCalls, sortGVK) slices.SortFunc(tc.fields.gate.FalseCalls, sortGVK) if diff := cmp.Diff(tc.want.falseCalls, tc.fields.gate.FalseCalls); diff != "" { t.Errorf("\n%s\ngate.False calls: -want, +got:\n%s", tc.reason, diff) } } }) } } ================================================ FILE: pkg/reconciler/customresourcesgate/setup.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package customresourcesgate import ( "errors" "reflect" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/crossplane/crossplane-runtime/v2/pkg/controller" ) // Setup adds a controller that reconciles CustomResourceDefinitions to support delayed start of controllers. // o.Gate is expected to be something like *gate.Gate[schema.GroupVersionKind]. func Setup(mgr ctrl.Manager, o controller.Options) error { if o.Gate == nil || reflect.ValueOf(o.Gate).IsNil() { return errors.New("gate is required") } r := &Reconciler{ log: o.Logger, gate: o.Gate, } return ctrl.NewControllerManagedBy(mgr). For(&apiextensionsv1.CustomResourceDefinition{}). Named("crd-gate"). Complete(reconcile.AsReconciler[*apiextensionsv1.CustomResourceDefinition](mgr.GetClient(), r)) } ================================================ FILE: pkg/reconciler/doc.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package reconciler provides reconcilers for Crossplane resources. package reconciler ================================================ FILE: pkg/reconciler/managed/api.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "context" "encoding/json" jsonpatch "github.com/evanphx/json-patch" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) const ( // fieldOwnerAPISimpleRefResolver owns the reference fields // the managed reconciler resolves. fieldOwnerAPISimpleRefResolver = "managed.crossplane.io/api-simple-reference-resolver" ) // Error strings. const ( errCreateOrUpdateSecret = "cannot create or update connection secret" errUpdateManaged = "cannot update managed resource" errPatchManaged = "cannot patch the managed resource via server-side apply" errMarshalExisting = "cannot marshal the existing object into JSON" errMarshalResolved = "cannot marshal the object with the resolved references into JSON" errPreparePatch = "cannot prepare the JSON merge patch for the resolved object" errUpdateManagedStatus = "cannot update managed resource status" errResolveReferences = "cannot resolve references" errUpdateCriticalAnnotations = "cannot update critical annotations" ) // NameAsExternalName writes the name of the managed resource to // the external name annotation field in order to be used as name of // the external resource in provider. type NameAsExternalName struct{ client client.Client } // NewNameAsExternalName returns a new NameAsExternalName. func NewNameAsExternalName(c client.Client) *NameAsExternalName { return &NameAsExternalName{client: c} } // Initialize the given managed resource. func (a *NameAsExternalName) Initialize(ctx context.Context, mg resource.Managed) error { if meta.GetExternalName(mg) != "" { return nil } meta.SetExternalName(mg, mg.GetName()) return errors.Wrap(a.client.Update(ctx, mg), errUpdateManaged) } // An APISecretPublisher publishes ConnectionDetails by submitting a Secret to a // Kubernetes API server. type APISecretPublisher struct { secret resource.Applicator typer runtime.ObjectTyper } // NewAPISecretPublisher returns a new APISecretPublisher. func NewAPISecretPublisher(c client.Client, ot runtime.ObjectTyper) *APISecretPublisher { // NOTE(negz): We transparently inject an APIPatchingApplicator in order to maintain // backward compatibility with the original API of this function. return &APISecretPublisher{ secret: resource.NewApplicatorWithRetry(resource.NewAPIPatchingApplicator(c), resource.IsAPIErrorWrapped, nil), typer: ot, } } // PublishConnection publishes the supplied ConnectionDetails to a Secret in the // same namespace as the supplied Managed resource. It is a no-op if the secret // already exists with the supplied ConnectionDetails. func (a *APISecretPublisher) PublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) { // This resource does not want to expose a connection secret. if o.GetWriteConnectionSecretToReference() == nil { return false, nil } s := resource.ConnectionSecretFor(o, resource.MustGetKind(o, a.typer)) s.Data = c err := a.secret.Apply(ctx, s, resource.ConnectionSecretMustBeControllableBy(o.GetUID()), resource.AllowUpdateIf(func(current, desired runtime.Object) bool { // We consider the update to be a no-op and don't allow it if the // current and existing secret data are identical. //nolint:forcetypeassert // Will always be a secret. return !cmp.Equal(current.(*corev1.Secret).Data, desired.(*corev1.Secret).Data, cmpopts.EquateEmpty()) }), ) if resource.IsNotAllowed(err) { // The update was not allowed because it was a no-op. return false, nil } if err != nil { return false, errors.Wrap(err, errCreateOrUpdateSecret) } return true, nil } // UnpublishConnection is no-op since PublishConnection only creates resources // that will be garbage collected by Kubernetes when the managed resource is // deleted. func (a *APISecretPublisher) UnpublishConnection(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) error { return nil } // An APILocalSecretPublisher publishes ConnectionDetails by submitting a Secret to a // Kubernetes API server. type APILocalSecretPublisher struct { secret resource.Applicator typer runtime.ObjectTyper } // NewAPILocalSecretPublisher returns a new APILocalSecretPublisher. func NewAPILocalSecretPublisher(c client.Client, ot runtime.ObjectTyper) *APILocalSecretPublisher { // NOTE(negz): We transparently inject an APIPatchingApplicator in order to maintain // backward compatibility with the original API of this function. return &APILocalSecretPublisher{ secret: resource.NewApplicatorWithRetry(resource.NewAPIPatchingApplicator(c), resource.IsAPIErrorWrapped, nil), typer: ot, } } // PublishConnection publishes the supplied ConnectionDetails to a Secret in the // same namespace as the supplied Managed resource. It is a no-op if the secret // already exists with the supplied ConnectionDetails. func (a *APILocalSecretPublisher) PublishConnection(ctx context.Context, o resource.LocalConnectionSecretOwner, c ConnectionDetails) (bool, error) { // This resource does not want to expose a connection secret. if o.GetWriteConnectionSecretToReference() == nil { return false, nil } s := resource.LocalConnectionSecretFor(o, resource.MustGetKind(o, a.typer)) s.Data = c err := a.secret.Apply(ctx, s, resource.ConnectionSecretMustBeControllableBy(o.GetUID()), resource.AllowUpdateIf(func(current, desired runtime.Object) bool { // We consider the update to be a no-op and don't allow it if the // current and existing secret data are identical. //nolint:forcetypeassert // Will always be a secret. // NOTE(erhancagirici): cmp package is not recommended for production use return !cmp.Equal(current.(*corev1.Secret).Data, desired.(*corev1.Secret).Data, cmpopts.EquateEmpty()) }), ) if resource.IsNotAllowed(err) { // The update was not allowed because it was a no-op. return false, nil } if err != nil { return false, errors.Wrap(err, errCreateOrUpdateSecret) } return true, nil } // UnpublishConnection is no-op since PublishConnection only creates resources // that will be garbage collected by Kubernetes when the managed resource is // deleted. func (a *APILocalSecretPublisher) UnpublishConnection(_ context.Context, _ resource.LocalConnectionSecretOwner, _ ConnectionDetails) error { return nil } // An APISimpleReferenceResolver resolves references from one managed resource // to others by calling the referencing resource's ResolveReferences method, if // any. type APISimpleReferenceResolver struct { client client.Client } // NewAPISimpleReferenceResolver returns a ReferenceResolver that resolves // references from one managed resource to others by calling the referencing // resource's ResolveReferences method, if any. func NewAPISimpleReferenceResolver(c client.Client) *APISimpleReferenceResolver { return &APISimpleReferenceResolver{client: c} } func prepareJSONMerge(existing, resolved runtime.Object) ([]byte, error) { // restore the to be replaced GVK so that the existing object is // not modified by this function. defer existing.GetObjectKind().SetGroupVersionKind(existing.GetObjectKind().GroupVersionKind()) // we need the apiVersion and kind in the patch document so we set them // to their zero values and make them available in the calculated patch // in the first place, instead of an unmarshal/marshal from the prepared // patch []byte later. existing.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{}) eBuff, err := json.Marshal(existing) if err != nil { return nil, errors.Wrap(err, errMarshalExisting) } rBuff, err := json.Marshal(resolved) if err != nil { return nil, errors.Wrap(err, errMarshalResolved) } patch, err := jsonpatch.CreateMergePatch(eBuff, rBuff) return patch, errors.Wrap(err, errPreparePatch) } // ResolveReferences of the supplied managed resource by calling its // ResolveReferences method, if any. func (a *APISimpleReferenceResolver) ResolveReferences(ctx context.Context, mg resource.Managed) error { rr, ok := mg.(interface { ResolveReferences(ctx context.Context, r client.Reader) error }) if !ok { // This managed resource doesn't have any references to resolve. return nil } existing := mg.DeepCopyObject() if err := rr.ResolveReferences(ctx, a.client); err != nil { return errors.Wrap(err, errResolveReferences) } if cmp.Equal(existing, mg, cmpopts.EquateEmpty()) { // The resource didn't change during reference resolution. return nil } patch, err := prepareJSONMerge(existing, mg) if err != nil { return err } return errors.Wrap(a.client.Patch(ctx, mg, client.RawPatch(types.ApplyPatchType, patch), client.FieldOwner(fieldOwnerAPISimpleRefResolver), client.ForceOwnership), errPatchManaged) } // A RetryingCriticalAnnotationUpdater is a CriticalAnnotationUpdater that // retries annotation updates in the face of API server errors. type RetryingCriticalAnnotationUpdater struct { client client.Client } // NewRetryingCriticalAnnotationUpdater returns a CriticalAnnotationUpdater that // retries annotation updates in the face of API server errors. func NewRetryingCriticalAnnotationUpdater(c client.Client) *RetryingCriticalAnnotationUpdater { return &RetryingCriticalAnnotationUpdater{client: c} } // UpdateCriticalAnnotations updates (i.e. persists) the annotations of the // supplied Object. It retries in the face of any API server error several times // in order to ensure annotations that contain critical state are persisted. // Pending changes to the supplied Object's spec, status, or other metadata // might get reset to their current state according to the API server, e.g. in // case of a conflict error. func (u *RetryingCriticalAnnotationUpdater) UpdateCriticalAnnotations(ctx context.Context, o client.Object) error { a := o.GetAnnotations() err := retry.OnError(retry.DefaultRetry, func(err error) bool { return !errors.Is(err, context.Canceled) }, func() error { err := u.client.Update(ctx, o) if kerrors.IsConflict(err) { if getErr := u.client.Get(ctx, client.ObjectKeyFromObject(o), o); getErr != nil { return getErr } meta.AddAnnotations(o, a) } return err }) return errors.Wrap(err, errUpdateCriticalAnnotations) } ================================================ FILE: pkg/reconciler/managed/api_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "context" "testing" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var ( _ Initializer = &NameAsExternalName{} _ ConnectionPublisher = &APISecretPublisher{} _ LocalConnectionPublisher = &APILocalSecretPublisher{} ) func TestNameAsExternalName(t *testing.T) { type args struct { ctx context.Context mg resource.Managed } type want struct { err error mg resource.Managed } errBoom := errors.New("boom") testExternalName := "my-" + "external-name" cases := map[string]struct { client client.Client args args want want }{ "UpdateManagedError": { client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(errBoom)}, args: args{ ctx: context.Background(), mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{Name: testExternalName}}, }, want: want{ err: errors.Wrap(errBoom, errUpdateManaged), mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{ Name: testExternalName, Annotations: map[string]string{meta.AnnotationKeyExternalName: testExternalName}, }}, }, }, "UpdateSuccessful": { client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)}, args: args{ ctx: context.Background(), mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{Name: testExternalName}}, }, want: want{ err: nil, mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{ Name: testExternalName, Annotations: map[string]string{meta.AnnotationKeyExternalName: testExternalName}, }}, }, }, "UpdateNotNeeded": { args: args{ ctx: context.Background(), mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{ Name: testExternalName, Annotations: map[string]string{meta.AnnotationKeyExternalName: "some-name"}, }}, }, want: want{ err: nil, mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{ Name: testExternalName, Annotations: map[string]string{meta.AnnotationKeyExternalName: "some-name"}, }}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { api := NewNameAsExternalName(tc.client) err := api.Initialize(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("api.Initialize(...): -want error, +got error:\n%s", diff) } if diff := cmp.Diff(tc.want.mg, tc.args.mg, test.EquateConditions()); diff != "" { t.Errorf("api.Initialize(...) Managed: -want, +got:\n%s", diff) } }) } } func TestAPISecretPublisher(t *testing.T) { errBoom := errors.New("boom") mg := &fake.LegacyManaged{ ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: &xpv2.SecretReference{ Namespace: "coolnamespace", Name: "coolsecret", }}, } cd := ConnectionDetails{"cool": {42}} type fields struct { secret resource.Applicator typer runtime.ObjectTyper } type args struct { ctx context.Context mg resource.LegacyManaged c ConnectionDetails } type want struct { err error published bool } cases := map[string]struct { reason string fields fields args args want want }{ "ResourceDoesNotPublishSecret": { reason: "A managed resource with a nil GetWriteConnectionSecretToReference should not publish a secret", args: args{ ctx: context.Background(), mg: &fake.LegacyManaged{}, }, }, "ApplyError": { reason: "An error applying the connection secret should be returned", fields: fields{ secret: resource.ApplyFn(func(_ context.Context, _ client.Object, _ ...resource.ApplyOption) error { return errBoom }), typer: fake.SchemeWith(&fake.LegacyManaged{}), }, args: args{ ctx: context.Background(), mg: mg, }, want: want{ err: errors.Wrap(errBoom, errCreateOrUpdateSecret), }, }, "AlreadyPublished": { reason: "An up to date connection secret should result in no error and not being published", fields: fields{ secret: resource.ApplyFn(func(ctx context.Context, o client.Object, ao ...resource.ApplyOption) error { want := resource.ConnectionSecretFor(mg, fake.GVK(mg)) want.Data = cd for _, fn := range ao { if err := fn(ctx, o, want); err != nil { return err } } return nil }), typer: fake.SchemeWith(&fake.LegacyManaged{}), }, args: args{ ctx: context.Background(), mg: mg, c: cd, }, want: want{ published: false, err: nil, }, }, "Success": { reason: "A successful application of the connection secret should result in no error", fields: fields{ secret: resource.ApplyFn(func(_ context.Context, o client.Object, _ ...resource.ApplyOption) error { want := resource.ConnectionSecretFor(mg, fake.GVK(mg)) want.Data = cd if diff := cmp.Diff(want, o); diff != "" { t.Errorf("-want, +got:\n%s", diff) } return nil }), typer: fake.SchemeWith(&fake.LegacyManaged{}), }, args: args{ ctx: context.Background(), mg: mg, c: cd, }, want: want{ published: true, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { a := &APISecretPublisher{tc.fields.secret, tc.fields.typer} got, gotErr := a.PublishConnection(tc.args.ctx, tc.args.mg, tc.args.c) if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nPublish(...): -wantErr, +gotErr:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.published, got); diff != "" { t.Errorf("\n%s\nPublish(...): -wantPublished, +gotPublished:\n%s", tc.reason, diff) } }) } } func TestAPILocalSecretPublisher(t *testing.T) { errBoom := errors.New("boom") mg := &fake.ModernManaged{ LocalConnectionSecretWriterTo: fake.LocalConnectionSecretWriterTo{Ref: &xpv2.LocalSecretReference{ Name: "coolsecret", }}, } cd := ConnectionDetails{"cool": {42}} type fields struct { secret resource.Applicator typer runtime.ObjectTyper } type args struct { ctx context.Context mg resource.ModernManaged c ConnectionDetails } type want struct { err error published bool } cases := map[string]struct { reason string fields fields args args want want }{ "ResourceDoesNotPublishSecret": { reason: "A managed resource with a nil GetWriteConnectionSecretToReference should not publish a secret", args: args{ ctx: context.Background(), mg: &fake.ModernManaged{}, }, }, "ApplyError": { reason: "An error applying the connection secret should be returned", fields: fields{ secret: resource.ApplyFn(func(_ context.Context, _ client.Object, _ ...resource.ApplyOption) error { return errBoom }), typer: fake.SchemeWith(&fake.ModernManaged{}), }, args: args{ ctx: context.Background(), mg: mg, }, want: want{ err: errors.Wrap(errBoom, errCreateOrUpdateSecret), }, }, "AlreadyPublished": { reason: "An up to date connection secret should result in no error and not being published", fields: fields{ secret: resource.ApplyFn(func(ctx context.Context, o client.Object, ao ...resource.ApplyOption) error { want := resource.LocalConnectionSecretFor(mg, fake.GVK(mg)) want.Data = cd for _, fn := range ao { if err := fn(ctx, o, want); err != nil { return err } } return nil }), typer: fake.SchemeWith(&fake.ModernManaged{}), }, args: args{ ctx: context.Background(), mg: mg, c: cd, }, want: want{ published: false, err: nil, }, }, "Success": { reason: "A successful application of the connection secret should result in no error", fields: fields{ secret: resource.ApplyFn(func(_ context.Context, o client.Object, _ ...resource.ApplyOption) error { want := resource.LocalConnectionSecretFor(mg, fake.GVK(mg)) want.Data = cd if diff := cmp.Diff(want, o); diff != "" { t.Errorf("-want, +got:\n%s", diff) } return nil }), typer: fake.SchemeWith(&fake.ModernManaged{}), }, args: args{ ctx: context.Background(), mg: mg, c: cd, }, want: want{ published: true, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { a := &APILocalSecretPublisher{tc.fields.secret, tc.fields.typer} got, gotErr := a.PublishConnection(tc.args.ctx, tc.args.mg, tc.args.c) if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nPublish(...): -wantErr, +gotErr:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.published, got); diff != "" { t.Errorf("\n%s\nPublish(...): -wantPublished, +gotPublished:\n%s", tc.reason, diff) } }) } } type mockSimpleReferencer struct { resource.Managed MockResolveReferences func(context.Context, client.Reader) error `json:"-"` } func (r *mockSimpleReferencer) ResolveReferences(ctx context.Context, c client.Reader) error { return r.MockResolveReferences(ctx, c) } func (r *mockSimpleReferencer) DeepCopyObject() runtime.Object { return &mockSimpleReferencer{Managed: r.Managed.DeepCopyObject().(resource.Managed)} } func (r *mockSimpleReferencer) Equal(s *mockSimpleReferencer) bool { return cmp.Equal(r.Managed, s.Managed) } func TestResolveReferences(t *testing.T) { errBoom := errors.New("boom") different := &fake.LegacyManaged{} type args struct { ctx context.Context mg resource.Managed } cases := map[string]struct { reason string c client.Client args args want error }{ "NoReferencersFound": { reason: "Should return early without error when the managed resource has no references.", args: args{ ctx: context.Background(), mg: &fake.LegacyManaged{}, }, want: nil, }, "ResolveReferencesError": { reason: "Should return errors encountered while resolving references.", c: &test.MockClient{ MockUpdate: test.NewMockUpdateFn(nil), }, args: args{ ctx: context.Background(), mg: &mockSimpleReferencer{ Managed: &fake.LegacyManaged{}, MockResolveReferences: func(context.Context, client.Reader) error { return errBoom }, }, }, want: errors.Wrap(errBoom, errResolveReferences), }, "SuccessfulNoop": { reason: "Should return without error when resolution does not change the managed resource.", c: &test.MockClient{ MockUpdate: test.NewMockUpdateFn(nil), }, args: args{ ctx: context.Background(), mg: &mockSimpleReferencer{ Managed: &fake.LegacyManaged{}, MockResolveReferences: func(context.Context, client.Reader) error { return nil }, }, }, want: nil, }, "SuccessfulUpdate": { reason: "Should return without error when a value is successfully resolved.", c: &test.MockClient{ MockPatch: test.NewMockPatchFn(nil), }, args: args{ ctx: context.Background(), mg: &mockSimpleReferencer{ Managed: different, MockResolveReferences: func(context.Context, client.Reader) error { different.SetName("I'm different!") return nil }, }, }, want: nil, }, "PatchError": { reason: "Should return an error when the managed resource cannot be updated.", c: &test.MockClient{ MockPatch: test.NewMockPatchFn(errBoom), }, args: args{ ctx: context.Background(), mg: &mockSimpleReferencer{ Managed: different, MockResolveReferences: func(context.Context, client.Reader) error { different.SetName("I'm different-er!") return nil }, }, }, want: errors.Wrap(errBoom, errPatchManaged), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewAPISimpleReferenceResolver(tc.c) got := r.ResolveReferences(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nr.ResolveReferences(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestPrepareJSONMerge(t *testing.T) { type args struct { existing runtime.Object resolved runtime.Object } type want struct { patch string err error } cases := map[string]struct { reason string args args want want }{ "SuccessfulPatch": { reason: "Should successfully compute the JSON merge patch document.", args: args{ existing: &fake.LegacyManaged{}, resolved: &fake.LegacyManaged{ ObjectMeta: metav1.ObjectMeta{ Name: "resolved", }, }, }, want: want{ patch: `{"name":"resolved"}`, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { patch, err := prepareJSONMerge(tc.args.existing, tc.args.resolved) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nprepareJSONMerge(...): -wantErr, +gotErr:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.patch, string(patch)); diff != "" { t.Errorf("\n%s\nprepareJSONMerge(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestRetryingCriticalAnnotationUpdater(t *testing.T) { errBoom := errors.New("boom") type args struct { ctx context.Context o client.Object } type want struct { err error o client.Object } setLabels := func(obj client.Object) error { obj.SetLabels(map[string]string{"getcalled": "true"}) return nil } objectReturnedByGet := &fake.LegacyManaged{} setLabels(objectReturnedByGet) cases := map[string]struct { reason string c *test.MockClient args args want want }{ "UpdateConflictGetError": { reason: "We should return any error we encounter getting the supplied object", c: &test.MockClient{ MockGet: test.NewMockGetFn(errBoom, setLabels), MockUpdate: test.NewMockUpdateFn(kerrors.NewConflict(schema.GroupResource{ Group: "foo.com", Resource: "bars", }, "abc", errBoom)), }, args: args{ o: &fake.LegacyManaged{}, }, want: want{ err: errors.Wrap(errBoom, errUpdateCriticalAnnotations), o: objectReturnedByGet, }, }, "UpdateError": { reason: "We should return any error we encounter updating the supplied object", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, setLabels), MockUpdate: test.NewMockUpdateFn(errBoom), }, args: args{ o: &fake.LegacyManaged{}, }, want: want{ err: errors.Wrap(errBoom, errUpdateCriticalAnnotations), o: &fake.LegacyManaged{}, }, }, "SuccessfulGetAfterAConflict": { reason: "A successful get after a conflict should not hide the conflict error and prevent retries", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, setLabels), MockUpdate: test.NewMockUpdateFn(kerrors.NewConflict(schema.GroupResource{ Group: "foo.com", Resource: "bars", }, "abc", errBoom)), }, args: args{ o: &fake.LegacyManaged{}, }, want: want{ err: errors.Wrap(kerrors.NewConflict(schema.GroupResource{ Group: "foo.com", Resource: "bars", }, "abc", errBoom), errUpdateCriticalAnnotations), o: objectReturnedByGet, }, }, "Success": { reason: "We should return without error if we successfully update our annotations", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, setLabels), MockUpdate: test.NewMockUpdateFn(errBoom), }, args: args{ o: &fake.LegacyManaged{}, }, want: want{ err: errors.Wrap(errBoom, errUpdateCriticalAnnotations), o: &fake.LegacyManaged{}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { u := NewRetryingCriticalAnnotationUpdater(tc.c) got := u.UpdateCriticalAnnotations(tc.args.ctx, tc.args.o) if diff := cmp.Diff(tc.want.err, got, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nu.UpdateCriticalAnnotations(...): -want, +got:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.o, tc.args.o); diff != "" { t.Errorf("\n%s\nu.UpdateCriticalAnnotations(...): -want, +got:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/reconciler/managed/changelogger.go ================================================ /* Copyright 2024 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "context" "time" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/timestamppb" "k8s.io/utils/ptr" "github.com/crossplane/crossplane-runtime/v2/apis/changelogs/proto/v1alpha1" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) const ( defaultSendTimeout = 10 * time.Second ) // ChangeLogger is an interface for recording changes made to resources to the // change logs. type ChangeLogger interface { Log(ctx context.Context, managed resource.Managed, opType v1alpha1.OperationType, changeErr error, ad AdditionalDetails) error } // GRPCChangeLogger processes changes to resources and helps to send them to the // change log gRPC service. type GRPCChangeLogger struct { client v1alpha1.ChangeLogServiceClient providerVersion string sendTimeout time.Duration } // NewGRPCChangeLogger creates a new gRPC based ChangeLogger initialized with // the given client. func NewGRPCChangeLogger(client v1alpha1.ChangeLogServiceClient, o ...GRPCChangeLoggerOption) *GRPCChangeLogger { g := &GRPCChangeLogger{ client: client, sendTimeout: defaultSendTimeout, } for _, clo := range o { clo(g) } return g } // A GRPCChangeLoggerOption configures a GRPCChangeLoggerOption. type GRPCChangeLoggerOption func(*GRPCChangeLogger) // WithProviderVersion sets the provider version to be included in the change // log entry. func WithProviderVersion(version string) GRPCChangeLoggerOption { return func(g *GRPCChangeLogger) { g.providerVersion = version } } // WithSendTimeout sets the timeout for sending and/or waiting for change log // entries to the change log service. func WithSendTimeout(timeout time.Duration) GRPCChangeLoggerOption { return func(g *GRPCChangeLogger) { g.sendTimeout = timeout } } // Log sends the given change log entry to the change log service. func (g *GRPCChangeLogger) Log(ctx context.Context, managed resource.Managed, opType v1alpha1.OperationType, changeErr error, ad AdditionalDetails) error { // get an error message from the error if it exists var changeErrMessage *string if changeErr != nil { changeErrMessage = ptr.To(changeErr.Error()) } // capture the full state of the managed resource from before we performed the change snapshot, err := resource.AsProtobufStruct(managed) if err != nil { return errors.Wrap(err, "cannot snapshot managed resource") } gvk := managed.GetObjectKind().GroupVersionKind() entry := &v1alpha1.ChangeLogEntry{ Timestamp: timestamppb.Now(), Provider: g.providerVersion, ApiVersion: gvk.GroupVersion().String(), Kind: gvk.Kind, Name: managed.GetName(), ExternalName: meta.GetExternalName(managed), Operation: opType, Snapshot: snapshot, ErrorMessage: changeErrMessage, AdditionalDetails: ad, } // create a specific context and timeout for sending the change log entry // that is different than the parent context that is for the entire // reconciliation sendCtx, sendCancel := context.WithTimeout(ctx, g.sendTimeout) defer sendCancel() // send everything we've got to the change log service _, err = g.client.SendChangeLog(sendCtx, &v1alpha1.SendChangeLogRequest{Entry: entry}, grpc.WaitForReady(true)) return errors.Wrap(err, "cannot send change log entry") } // nopChangeLogger does nothing for recording change logs, this is the default // implementation if a provider has not enabled the change logs feature. type nopChangeLogger struct{} func newNopChangeLogger() *nopChangeLogger { return &nopChangeLogger{} } func (n *nopChangeLogger) Log(_ context.Context, _ resource.Managed, _ v1alpha1.OperationType, _ error, _ AdditionalDetails) error { return nil } ================================================ FILE: pkg/reconciler/managed/changelogger_test.go ================================================ /* Copyright 2024 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "context" "reflect" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/grpc" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" "github.com/crossplane/crossplane-runtime/v2/apis/changelogs/proto/v1alpha1" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) // A mock implementation of the ChangeLogServiceClient interface to help with // testing and verifying change log entries. type changeLogServiceClient struct { requests []*v1alpha1.SendChangeLogRequest sendFn func(ctx context.Context, in *v1alpha1.SendChangeLogRequest, opts ...grpc.CallOption) (*v1alpha1.SendChangeLogResponse, error) } func (c *changeLogServiceClient) SendChangeLog(ctx context.Context, in *v1alpha1.SendChangeLogRequest, opts ...grpc.CallOption) (*v1alpha1.SendChangeLogResponse, error) { c.requests = append(c.requests, in) if c.sendFn != nil { return c.sendFn(ctx, in, opts...) } return nil, nil } func TestChangeLogger(t *testing.T) { type args struct { mr resource.Managed ad AdditionalDetails err error c *changeLogServiceClient } type want struct { requests []*v1alpha1.SendChangeLogRequest err error } errBoom := errors.New("boom") cases := map[string]struct { reason string args args want want }{ "ChangeLogsSuccess": { reason: "Change log entry should be recorded successfully.", args: args{ mr: &fake.Managed{ObjectMeta: metav1.ObjectMeta{ Name: "cool-managed", Annotations: map[string]string{meta.AnnotationKeyExternalName: "cool-managed"}, }}, err: errBoom, ad: AdditionalDetails{"key": "value", "key2": "value2"}, c: &changeLogServiceClient{requests: []*v1alpha1.SendChangeLogRequest{}}, }, want: want{ // a well fleshed out change log entry should be sent requests: []*v1alpha1.SendChangeLogRequest{ { Entry: &v1alpha1.ChangeLogEntry{ Timestamp: timestamppb.Now(), Provider: "provider-cool:v9.99.999", ApiVersion: (&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupVersion().String(), Kind: (&fake.Managed{}).GetObjectKind().GroupVersionKind().Kind, Name: "cool-managed", ExternalName: "cool-managed", Operation: v1alpha1.OperationType_OPERATION_TYPE_CREATE, Snapshot: mustObjectAsProtobufStruct(&fake.Managed{ObjectMeta: metav1.ObjectMeta{ Name: "cool-managed", Annotations: map[string]string{meta.AnnotationKeyExternalName: "cool-managed"}, }}), ErrorMessage: ptr.To("boom"), AdditionalDetails: AdditionalDetails{"key": "value", "key2": "value2"}, }, }, }, }, }, "SendChangeLogsFailure": { reason: "Error from sending change log entry should be handled and recorded.", args: args{ mr: &fake.Managed{}, c: &changeLogServiceClient{ requests: []*v1alpha1.SendChangeLogRequest{}, // make the send change log function return an error sendFn: func(_ context.Context, _ *v1alpha1.SendChangeLogRequest, _ ...grpc.CallOption) (*v1alpha1.SendChangeLogResponse, error) { return &v1alpha1.SendChangeLogResponse{}, errBoom }, }, }, want: want{ // we'll still see a change log entry, but it won't make it all // the way to its destination and we should see an event for // that failure requests: []*v1alpha1.SendChangeLogRequest{ { Entry: &v1alpha1.ChangeLogEntry{ // we expect less fields to be set on the change log // entry because we're not initializing the managed // resource with much data in this simulated failure // test case Timestamp: timestamppb.Now(), Provider: "provider-cool:v9.99.999", ApiVersion: (&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupVersion().String(), Kind: (&fake.Managed{}).GetObjectKind().GroupVersionKind().Kind, Operation: v1alpha1.OperationType_OPERATION_TYPE_CREATE, Snapshot: mustObjectAsProtobufStruct(&fake.Managed{}), }, }, }, err: errors.Wrap(errBoom, "cannot send change log entry"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { change := NewGRPCChangeLogger(tc.args.c, WithProviderVersion("provider-cool:v9.99.999")) err := change.Log(context.Background(), tc.args.mr, v1alpha1.OperationType_OPERATION_TYPE_CREATE, tc.args.err, tc.args.ad) if diff := cmp.Diff(tc.want.requests, tc.args.c.requests, equateApproxTimepb(time.Second)...); diff != "" { t.Errorf("\nReason: %s\nr.RecordChangeLog(...): -want requests, +got requests:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\nReason: %s\nr.RecordChangeLog(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func mustObjectAsProtobufStruct(o runtime.Object) *structpb.Struct { s, err := resource.AsProtobufStruct(o) if err != nil { panic(err) } return s } // A set of cmp.Option that enables usage of cmpopts.EquateApproxTime for // timestamppb.Timestamp types. // Source: https://github.com/golang/protobuf/issues/1347 func equateApproxTimepb(margin time.Duration) []cmp.Option { return cmp.Options{ cmpopts.EquateApproxTime(margin), protocmp.Transform(), cmp.FilterPath( func(p cmp.Path) bool { if p.Last().Type() == reflect.TypeFor[protocmp.Message]() { a, b := p.Last().Values() return msgIsTimestamp(a) && msgIsTimestamp(b) } return false }, cmp.Transformer("timestamppb", func(t protocmp.Message) time.Time { return time.Unix(t["seconds"].(int64), int64(t["nanos"].(int32))).UTC() }), ), } } func msgIsTimestamp(x reflect.Value) bool { return x.Interface().(protocmp.Message).Descriptor().FullName() == "google.protobuf.Timestamp" } ================================================ FILE: pkg/reconciler/managed/doc.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package managed provides a reconciler that manages the lifecycle of a // resource in an external system. package managed ================================================ FILE: pkg/reconciler/managed/metrics.go ================================================ /* Copyright 2024 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "sync" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/prometheus/client_golang/prometheus" corev1 "k8s.io/api/core/v1" kmetrics "k8s.io/component-base/metrics" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) const subSystem = "crossplane" // MetricRecorder records the managed resource metrics. type MetricRecorder interface { //nolint:interfacebloat // The first two methods are coming from Prometheus Describe(ch chan<- *prometheus.Desc) Collect(ch chan<- prometheus.Metric) recordUnchanged(name string) recordFirstTimeReconciled(managed resource.Managed) recordFirstTimeReady(managed resource.Managed) recordDrift(managed resource.Managed) recordDeleted(managed resource.Managed) } // MRMetricRecorder records the lifecycle metrics of managed resources. type MRMetricRecorder struct { firstObservation sync.Map lastObservation sync.Map mrDetected *prometheus.HistogramVec mrFirstTimeReady *prometheus.HistogramVec mrDeletion *prometheus.HistogramVec mrDrift *prometheus.HistogramVec } // NewMRMetricRecorder returns a new MRMetricRecorder which records metrics for managed resources. func NewMRMetricRecorder() *MRMetricRecorder { return &MRMetricRecorder{ mrDetected: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Subsystem: subSystem, Name: "managed_resource_first_time_to_reconcile_seconds", Help: "The time it took for a managed resource to be detected by the controller", Buckets: kmetrics.ExponentialBuckets(10e-9, 10, 10), }, []string{"gvk"}), mrFirstTimeReady: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Subsystem: subSystem, Name: "managed_resource_first_time_to_readiness_seconds", Help: "The time it took for a managed resource to become ready first time after creation", Buckets: []float64{1, 5, 10, 15, 30, 60, 120, 300, 600, 1800, 3600}, }, []string{"gvk"}), mrDeletion: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Subsystem: subSystem, Name: "managed_resource_deletion_seconds", Help: "The time it took for a managed resource to be deleted", Buckets: []float64{1, 5, 10, 15, 30, 60, 120, 300, 600, 1800, 3600}, }, []string{"gvk"}), mrDrift: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Subsystem: subSystem, Name: "managed_resource_drift_seconds", Help: "ALPHA: How long since the previous successful reconcile when a resource was found to be out of sync; excludes restart of the provider", Buckets: kmetrics.ExponentialBuckets(10e-9, 10, 10), }, []string{"gvk"}), } } // Describe sends the super-set of all possible descriptors of metrics // collected by this Collector to the provided channel and returns once // the last descriptor has been sent. func (r *MRMetricRecorder) Describe(ch chan<- *prometheus.Desc) { r.mrDetected.Describe(ch) r.mrFirstTimeReady.Describe(ch) r.mrDeletion.Describe(ch) r.mrDrift.Describe(ch) } // Collect is called by the Prometheus registry when collecting // metrics. The implementation sends each collected metric via the // provided channel and returns once the last metric has been sent. func (r *MRMetricRecorder) Collect(ch chan<- prometheus.Metric) { r.mrDetected.Collect(ch) r.mrFirstTimeReady.Collect(ch) r.mrDeletion.Collect(ch) r.mrDrift.Collect(ch) } func (r *MRMetricRecorder) recordUnchanged(name string) { r.lastObservation.Store(name, time.Now()) } func (r *MRMetricRecorder) recordFirstTimeReconciled(managed resource.Managed) { if managed.GetCondition(xpv2.TypeSynced).Status == corev1.ConditionUnknown { r.mrDetected.With(getLabels(managed)).Observe(time.Since(managed.GetCreationTimestamp().Time).Seconds()) r.firstObservation.Store(managed.GetName(), time.Now()) // this is the first time we reconciled on this resource } } func (r *MRMetricRecorder) recordDrift(managed resource.Managed) { name := managed.GetName() last, ok := r.lastObservation.Load(name) if !ok { return } lt, ok := last.(time.Time) if !ok { return } r.mrDrift.With(getLabels(managed)).Observe(time.Since(lt).Seconds()) r.lastObservation.Store(name, time.Now()) } func (r *MRMetricRecorder) recordDeleted(managed resource.Managed) { r.mrDeletion.With(getLabels(managed)).Observe(time.Since(managed.GetDeletionTimestamp().Time).Seconds()) } func (r *MRMetricRecorder) recordFirstTimeReady(managed resource.Managed) { // Note that providers may set the ready condition to "True", so we need // to check the value here to send the ready metric if managed.GetCondition(xpv2.TypeReady).Status == corev1.ConditionTrue { _, ok := r.firstObservation.Load(managed.GetName()) // This map is used to identify the first time to readiness if !ok { return } r.mrFirstTimeReady.With(getLabels(managed)).Observe(time.Since(managed.GetCreationTimestamp().Time).Seconds()) r.firstObservation.Delete(managed.GetName()) } } // A NopMetricRecorder does nothing. type NopMetricRecorder struct{} // NewNopMetricRecorder returns a MRMetricRecorder that does nothing. func NewNopMetricRecorder() *NopMetricRecorder { return &NopMetricRecorder{} } // Describe does nothing. func (r *NopMetricRecorder) Describe(_ chan<- *prometheus.Desc) {} // Collect does nothing. func (r *NopMetricRecorder) Collect(_ chan<- prometheus.Metric) {} func (r *NopMetricRecorder) recordUnchanged(_ string) {} func (r *NopMetricRecorder) recordFirstTimeReconciled(_ resource.Managed) {} func (r *NopMetricRecorder) recordDrift(_ resource.Managed) {} func (r *NopMetricRecorder) recordDeleted(_ resource.Managed) {} func (r *NopMetricRecorder) recordFirstTimeReady(_ resource.Managed) {} func getLabels(r resource.Managed) prometheus.Labels { return prometheus.Labels{ "gvk": r.GetObjectKind().GroupVersionKind().String(), } } ================================================ FILE: pkg/reconciler/managed/policies.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "fmt" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "k8s.io/apimachinery/pkg/util/sets" ) // ManagementPoliciesResolver is used to perform management policy checks // based on the management policy and if the management policy feature is enabled. type ManagementPoliciesResolver struct { enabled bool supportedPolicies []sets.Set[xpv2.ManagementAction] managementPolicies sets.Set[xpv2.ManagementAction] } // LegacyManagementPoliciesResolver is used to perform management policy checks // based on the management policy and if the management policy feature is enabled. // // Deprecated: this is for LegacyManaged types, that had deletion policy. // ModernManaged resources should use ManagementPoliciesResolver. type LegacyManagementPoliciesResolver struct { *ManagementPoliciesResolver deletionPolicy xpv2.DeletionPolicy } // A ManagementPoliciesResolverOption configures a ManagementPoliciesResolver. type ManagementPoliciesResolverOption func(*ManagementPoliciesResolver) // WithSupportedManagementPolicies sets the supported management policies. func WithSupportedManagementPolicies(supportedManagementPolicies []sets.Set[xpv2.ManagementAction]) ManagementPoliciesResolverOption { return func(r *ManagementPoliciesResolver) { r.supportedPolicies = supportedManagementPolicies } } func defaultSupportedManagementPolicies() []sets.Set[xpv2.ManagementAction] { return []sets.Set[xpv2.ManagementAction]{ // Default (all), the standard behaviour of crossplane in which all // reconciler actions are done. sets.New[xpv2.ManagementAction](xpv2.ManagementActionAll), // All actions explicitly set, the same as default. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionUpdate, xpv2.ManagementActionLateInitialize, xpv2.ManagementActionDelete), // ObserveOnly, just observe action is done, the external resource is // considered as read-only. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve), // Pause, no action is being done. Alternative to setting the pause // annotation. sets.New[xpv2.ManagementAction](), // No LateInitialize filling in the spec.forProvider, allowing some // external resource fields to be managed externally. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionUpdate, xpv2.ManagementActionDelete), // No Delete, the external resource is not deleted when the managed // resource is deleted. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionUpdate, xpv2.ManagementActionLateInitialize), // No Delete and no LateInitialize, the external resource is not deleted // when the managed resource is deleted and the spec.forProvider is not // late initialized. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionUpdate), // No Update, the external resource is not updated when the managed // resource is updated. Useful for immutable external resources. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete, xpv2.ManagementActionLateInitialize), // No Update and no Delete, the external resource is not updated // when the managed resource is updated and the external resource // is not deleted when the managed resource is deleted. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionLateInitialize), // No Update and no LateInitialize, the external resource is not updated // when the managed resource is updated and the spec.forProvider is not // late initialized. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete), // No Update, no Delete and no LateInitialize, the external resource is // not updated when the managed resource is updated, the external resource // is not deleted when the managed resource is deleted and the // spec.forProvider is not late initialized. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionCreate), // Like ObserveOnly, but the external resource is deleted when the // managed resource is deleted. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionDelete), // No Crate and no Delete. Just update/patch the external resource. // Useful when the same external resource is managed by multiple // managed resources. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionUpdate), // Import mode: Allows observation of existing resources and populates spec.forProvider // through late initialization, without making any changes to the external resource. // Useful for safely importing existing resources to discover their current state. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionLateInitialize), // No Create, no Delete. Just Observe, Update and LateInitialize. // Useful when external resource lifecycle is managed elsewhere but you want // to allow Crossplane to make updates and discover state changes. sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve, xpv2.ManagementActionUpdate, xpv2.ManagementActionLateInitialize), } } // NewManagementPoliciesResolver returns an ManagementPolicyChecker based // on the management policies and if the management policies feature // is enabled. func NewManagementPoliciesResolver(managementPolicyEnabled bool, managementPolicy xpv2.ManagementPolicies, o ...ManagementPoliciesResolverOption) ManagementPoliciesChecker { r := &ManagementPoliciesResolver{ enabled: managementPolicyEnabled, supportedPolicies: defaultSupportedManagementPolicies(), managementPolicies: sets.New[xpv2.ManagementAction](managementPolicy...), } for _, ro := range o { ro(r) } return r } // NewLegacyManagementPoliciesResolver returns an ManagementPolicyChecker based // on the management policies and if the management policies feature // is enabled. // // Deprecated: this is intended for LegacyManaged resources that had deletionPolicy // ModernManaged resources should use NewManagementPoliciesResolver. func NewLegacyManagementPoliciesResolver(managementPolicyEnabled bool, managementPolicy xpv2.ManagementPolicies, deletionPolicy xpv2.DeletionPolicy, o ...ManagementPoliciesResolverOption) ManagementPoliciesChecker { r := &ManagementPoliciesResolver{ enabled: managementPolicyEnabled, supportedPolicies: defaultSupportedManagementPolicies(), managementPolicies: sets.New[xpv2.ManagementAction](managementPolicy...), } for _, ro := range o { ro(r) } return &LegacyManagementPoliciesResolver{r, deletionPolicy} } // Validate checks if the management policy is valid. // If the management policy feature is disabled, but uses a non-default value, // it returns an error. // If the management policy feature is enabled, but uses a non-supported value, // it returns an error. func (m *ManagementPoliciesResolver) Validate() error { // check if its disabled, but uses a non-default value. if !m.enabled { if !m.managementPolicies.Equal(sets.New[xpv2.ManagementAction](xpv2.ManagementActionAll)) && m.managementPolicies.Len() != 0 { return fmt.Errorf(errFmtManagementPolicyNonDefault, m.managementPolicies.UnsortedList()) } // if its just disabled we don't care about supported policies return nil } // check if the policy is a non-supported combination for _, p := range m.supportedPolicies { if p.Equal(m.managementPolicies) { return nil } } return fmt.Errorf(errFmtManagementPolicyNotSupported, m.managementPolicies.UnsortedList()) } // IsPaused returns true if the management policy is empty and the // management policies feature is enabled. func (m *ManagementPoliciesResolver) IsPaused() bool { if !m.enabled { return false } return m.managementPolicies.Len() == 0 } // ShouldCreate returns true if the Create action is allowed. // If the management policy feature is disabled, it returns true. func (m *ManagementPoliciesResolver) ShouldCreate() bool { if !m.enabled { return true } return m.managementPolicies.HasAny(xpv2.ManagementActionCreate, xpv2.ManagementActionAll) } // ShouldUpdate returns true if the Update action is allowed. // If the management policy feature is disabled, it returns true. func (m *ManagementPoliciesResolver) ShouldUpdate() bool { if !m.enabled { return true } return m.managementPolicies.HasAny(xpv2.ManagementActionUpdate, xpv2.ManagementActionAll) } // ShouldLateInitialize returns true if the LateInitialize action is allowed. // If the management policy feature is disabled, it returns true. func (m *ManagementPoliciesResolver) ShouldLateInitialize() bool { if !m.enabled { return true } return m.managementPolicies.HasAny(xpv2.ManagementActionLateInitialize, xpv2.ManagementActionAll) } // ShouldOnlyObserve returns true if the Observe action is allowed and all // other actions are not allowed. If the management policy feature is disabled, // it returns false. func (m *ManagementPoliciesResolver) ShouldOnlyObserve() bool { if !m.enabled { return false } return m.managementPolicies.Equal(sets.New[xpv2.ManagementAction](xpv2.ManagementActionObserve)) } // ShouldDelete returns true based only on the managementPolicies. // If the management policy feature is disabled, returns true. // Otherwise, it checks whether the managementPolicies explicitly // include Delete or * (all). func (m *ManagementPoliciesResolver) ShouldDelete() bool { if !m.enabled { return true } return m.managementPolicies.HasAny(xpv2.ManagementActionDelete, xpv2.ManagementActionAll) } // ShouldDelete returns true based on the combination of the deletionPolicy and // the managementPolicies. If the management policy feature is disabled, it // returns true if the deletionPolicy is set to "Delete". Otherwise, it checks // which field is set to a non-default value and makes a decision based on that. // We need to be careful until we completely remove the deletionPolicy in favor // of managementPolicies which conflict with the deletionPolicy regarding // deleting of the external resource. This function implements the proposal in // the Ignore Changes design doc under the "Deprecation of `deletionPolicy`". func (m *LegacyManagementPoliciesResolver) ShouldDelete() bool { if !m.enabled { return m.deletionPolicy != xpv2.DeletionOrphan } // delete external resource if both the deletionPolicy and the // managementPolicies are set to delete if m.deletionPolicy == xpv2.DeletionDelete && m.managementPolicies.HasAny(xpv2.ManagementActionDelete, xpv2.ManagementActionAll) { return true } // if the managementPolicies is not default, and it contains the deletion // action, we should delete the external resource if !m.managementPolicies.Equal(sets.New[xpv2.ManagementAction](xpv2.ManagementActionAll)) && m.managementPolicies.Has(xpv2.ManagementActionDelete) { return true } // For all other cases, we should orphan the external resource. // Obvious cases: // DeletionOrphan && ManagementPolicies without Delete Action // Conflicting cases: // DeletionOrphan && Management Policy ["*"] (obeys non-default configuration) // DeletionDelete && ManagementPolicies that does not include the Delete // Action (obeys non-default configuration) return false } ================================================ FILE: pkg/reconciler/managed/reconciler.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "context" "math/rand" "strings" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/crossplane/crossplane-runtime/v2/apis/changelogs/proto/v1alpha1" "github.com/crossplane/crossplane-runtime/v2/pkg/conditions" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/event" "github.com/crossplane/crossplane-runtime/v2/pkg/feature" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) const ( // FinalizerName is the string that is used as finalizer on managed resource // objects. FinalizerName = "finalizer.managedresource.crossplane.io" reconcileGracePeriod = 30 * time.Second reconcileTimeout = 1 * time.Minute defaultPollInterval = 1 * time.Minute defaultGracePeriod = 30 * time.Second ) // Error strings. const ( errFmtManagementPolicyNonDefault = "`spec.managementPolicies` is set to a non-default value but the feature is not enabled: %s" errFmtManagementPolicyNotSupported = "`spec.managementPolicies` is set to a value(%s) which is not supported. Check docs for supported policies" errGetManaged = "cannot get managed resource" errUpdateManagedAnnotations = "cannot update managed resource annotations" errCreateIncomplete = "cannot determine creation result - remove the " + meta.AnnotationKeyExternalCreatePending + " annotation if it is safe to proceed" errReconcileConnect = "connect failed" errReconcileObserve = "observe failed" errReconcileCreate = "create failed" errReconcileUpdate = "update failed" errReconcileDelete = "delete failed" errRecordChangeLog = "cannot record change log entry" errExternalResourceNotExist = "external resource does not exist" errManagedNotImplemented = "managed resource does not implement connection details" ) // Event reasons. const ( reasonCannotConnect event.Reason = "CannotConnectToProvider" reasonCannotDisconnect event.Reason = "CannotDisconnectFromProvider" reasonCannotInitialize event.Reason = "CannotInitializeManagedResource" reasonCannotResolveRefs event.Reason = "CannotResolveResourceReferences" reasonCannotObserve event.Reason = "CannotObserveExternalResource" reasonCannotCreate event.Reason = "CannotCreateExternalResource" reasonCannotDelete event.Reason = "CannotDeleteExternalResource" reasonCannotPublish event.Reason = "CannotPublishConnectionDetails" reasonCannotUnpublish event.Reason = "CannotUnpublishConnectionDetails" reasonCannotUpdate event.Reason = "CannotUpdateExternalResource" reasonCannotUpdateManaged event.Reason = "CannotUpdateManagedResource" reasonManagementPolicyInvalid event.Reason = "CannotUseInvalidManagementPolicy" reasonDeleted event.Reason = "DeletedExternalResource" reasonCreated event.Reason = "CreatedExternalResource" reasonUpdated event.Reason = "UpdatedExternalResource" reasonPending event.Reason = "PendingExternalResource" reasonReconciliationPaused event.Reason = "ReconciliationPaused" reasonReconcileRequestHandled event.Reason = "ReconcileRequestHandled" ) // ControllerName returns the recommended name for controllers that use this // package to reconcile a particular kind of managed resource. func ControllerName(kind string) string { return "managed/" + strings.ToLower(kind) } // ManagementPoliciesChecker is used to perform checks on management policies // to determine specific actions are allowed, or if they are the only allowed // action. type ManagementPoliciesChecker interface { //nolint:interfacebloat // This has to be big. // Validate validates the management policies. Validate() error // IsPaused returns true if the resource is paused based // on the management policy. IsPaused() bool // ShouldOnlyObserve returns true if only the Observe action is allowed. ShouldOnlyObserve() bool // ShouldCreate returns true if the Create action is allowed. ShouldCreate() bool // ShouldLateInitialize returns true if the LateInitialize action is // allowed. ShouldLateInitialize() bool // ShouldUpdate returns true if the Update action is allowed. ShouldUpdate() bool // ShouldDelete returns true if the Delete action is allowed. ShouldDelete() bool } // A reconcileRequestTracker can record which reconcile-request token was last // handled. Managed resources that embed ObservedStatus implement this // interface automatically. type reconcileRequestTracker interface { GetLastHandledReconcileAt() string SetLastHandledReconcileAt(token string) } // A CriticalAnnotationUpdater is used when it is critical that annotations must // be updated before returning from the Reconcile loop. type CriticalAnnotationUpdater interface { UpdateCriticalAnnotations(ctx context.Context, o client.Object) error } // A CriticalAnnotationUpdateFn may be used when it is critical that annotations // must be updated before returning from the Reconcile loop. type CriticalAnnotationUpdateFn func(ctx context.Context, o client.Object) error // UpdateCriticalAnnotations of the supplied object. func (fn CriticalAnnotationUpdateFn) UpdateCriticalAnnotations(ctx context.Context, o client.Object) error { return fn(ctx, o) } // ConnectionDetails created or updated during an operation on an external // resource, for example usernames, passwords, endpoints, ports, etc. type ConnectionDetails map[string][]byte // AdditionalDetails represent any additional details the external client wants // to return about an operation that has been performed. These details will be // included in the change logs. type AdditionalDetails map[string]string // A ConnectionPublisher manages the supplied ConnectionDetails for the // supplied Managed resource. ManagedPublishers must handle the case in which // the supplied ConnectionDetails are empty. type ConnectionPublisher interface { // PublishConnection details for the supplied Managed resource. Publishing // must be additive; i.e. if details (a, b, c) are published, subsequently // publicing details (b, c, d) should update (b, c) but not remove a. PublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, c ConnectionDetails) (published bool, err error) // UnpublishConnection details for the supplied Managed resource. UnpublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, c ConnectionDetails) error } // ConnectionPublisherFns is the pluggable struct to produce objects with ConnectionPublisher interface. type ConnectionPublisherFns struct { PublishConnectionFn func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) UnpublishConnectionFn func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error } // PublishConnection details for the supplied Managed resource. func (fn ConnectionPublisherFns) PublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) { return fn.PublishConnectionFn(ctx, o, c) } // UnpublishConnection details for the supplied Managed resource. func (fn ConnectionPublisherFns) UnpublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error { return fn.UnpublishConnectionFn(ctx, o, c) } // A LocalConnectionPublisher manages the supplied ConnectionDetails for the // supplied Managed resource. ManagedPublishers must handle the case in which // the supplied ConnectionDetails are empty. type LocalConnectionPublisher interface { // PublishConnection details for the supplied Managed resource. Publishing // must be additive; i.e. if details (a, b, c) are published, subsequently // publicing details (b, c, d) should update (b, c) but not remove a. PublishConnection(ctx context.Context, lso resource.LocalConnectionSecretOwner, c ConnectionDetails) (published bool, err error) // UnpublishConnection details for the supplied Managed resource. UnpublishConnection(ctx context.Context, lso resource.LocalConnectionSecretOwner, c ConnectionDetails) error } // LocalConnectionPublisherFns is the pluggable struct to produce objects with LocalConnectionPublisher interface. type LocalConnectionPublisherFns struct { PublishConnectionFn func(ctx context.Context, o resource.LocalConnectionSecretOwner, c ConnectionDetails) (bool, error) UnpublishConnectionFn func(ctx context.Context, o resource.LocalConnectionSecretOwner, c ConnectionDetails) error } // PublishConnection details for the supplied Managed resource. func (fn LocalConnectionPublisherFns) PublishConnection(ctx context.Context, o resource.LocalConnectionSecretOwner, c ConnectionDetails) (bool, error) { return fn.PublishConnectionFn(ctx, o, c) } // UnpublishConnection details for the supplied Managed resource. func (fn LocalConnectionPublisherFns) UnpublishConnection(ctx context.Context, o resource.LocalConnectionSecretOwner, c ConnectionDetails) error { return fn.UnpublishConnectionFn(ctx, o, c) } // A ConnectionDetailsFetcher fetches connection details for the supplied // Connection Secret owner. type ConnectionDetailsFetcher interface { FetchConnection(ctx context.Context, so resource.ConnectionSecretOwner) (ConnectionDetails, error) } // Initializer establishes ownership of the supplied Managed resource. // This typically involves the operations that are run before calling any // ExternalClient methods. type Initializer interface { Initialize(ctx context.Context, mg resource.Managed) error } // A InitializerChain chains multiple managed initializers. type InitializerChain []Initializer // Initialize calls each Initializer serially. It returns the first // error it encounters, if any. func (cc InitializerChain) Initialize(ctx context.Context, mg resource.Managed) error { for _, c := range cc { if err := c.Initialize(ctx, mg); err != nil { return err } } return nil } // A InitializerFn is a function that satisfies the Initializer // interface. type InitializerFn func(ctx context.Context, mg resource.Managed) error // Initialize calls InitializerFn function. func (m InitializerFn) Initialize(ctx context.Context, mg resource.Managed) error { return m(ctx, mg) } // A ReferenceResolver resolves references to other managed resources. type ReferenceResolver interface { // ResolveReferences resolves all fields in the supplied managed resource // that are references to other managed resources by updating corresponding // fields, for example setting spec.network to the Network resource // specified by spec.networkRef.name. ResolveReferences(ctx context.Context, mg resource.Managed) error } // A ReferenceResolverFn is a function that satisfies the // ReferenceResolver interface. type ReferenceResolverFn func(context.Context, resource.Managed) error // ResolveReferences calls ReferenceResolverFn function. func (m ReferenceResolverFn) ResolveReferences(ctx context.Context, mg resource.Managed) error { return m(ctx, mg) } // An ExternalConnector produces a new ExternalClient given the supplied // Managed resource. type ExternalConnector = TypedExternalConnector[resource.Managed] // A TypedExternalConnector produces a new ExternalClient given the supplied // Managed resource. type TypedExternalConnector[managed resource.Managed] interface { // Connect to the provider specified by the supplied managed resource and // produce an ExternalClient. Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) } // A NopDisconnector converts an ExternalConnector into an // ExternalConnectDisconnector with a no-op Disconnect method. type NopDisconnector = TypedNopDisconnector[resource.Managed] // A TypedNopDisconnector converts an ExternalConnector into an // ExternalConnectDisconnector with a no-op Disconnect method. type TypedNopDisconnector[managed resource.Managed] struct { c TypedExternalConnector[managed] } // Connect calls the underlying ExternalConnector's Connect method. func (c *TypedNopDisconnector[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) { return c.c.Connect(ctx, mg) } // Disconnect does nothing. It never returns an error. func (c *TypedNopDisconnector[managed]) Disconnect(_ context.Context) error { return nil } // NewNopDisconnector converts an ExternalConnector into an // ExternalConnectDisconnector with a no-op Disconnect method. func NewNopDisconnector(c ExternalConnector) ExternalConnectDisconnector { return NewTypedNopDisconnector(c) } // NewTypedNopDisconnector converts an TypedExternalConnector into an // ExternalConnectDisconnector with a no-op Disconnect method. func NewTypedNopDisconnector[managed resource.Managed](c TypedExternalConnector[managed]) TypedExternalConnectDisconnector[managed] { return &TypedNopDisconnector[managed]{c} } // An ExternalConnectDisconnector produces a new ExternalClient given the supplied // Managed resource. type ExternalConnectDisconnector = TypedExternalConnectDisconnector[resource.Managed] // A TypedExternalConnectDisconnector produces a new ExternalClient given the supplied // Managed resource. type TypedExternalConnectDisconnector[managed resource.Managed] interface { TypedExternalConnector[managed] ExternalDisconnector } // An ExternalConnectorFn is a function that satisfies the ExternalConnector // interface. type ExternalConnectorFn = TypedExternalConnectorFn[resource.Managed] // An TypedExternalConnectorFn is a function that satisfies the // TypedExternalConnector interface. type TypedExternalConnectorFn[managed resource.Managed] func(ctx context.Context, mg managed) (TypedExternalClient[managed], error) // Connect to the provider specified by the supplied managed resource and // produce an ExternalClient. func (ec TypedExternalConnectorFn[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) { return ec(ctx, mg) } // An ExternalDisconnectorFn is a function that satisfies the ExternalConnector // interface. type ExternalDisconnectorFn func(ctx context.Context) error // Disconnect from provider and close the ExternalClient. func (ed ExternalDisconnectorFn) Disconnect(ctx context.Context) error { return ed(ctx) } // ExternalConnectDisconnectorFns are functions that satisfy the // ExternalConnectDisconnector interface. type ExternalConnectDisconnectorFns = TypedExternalConnectDisconnectorFns[resource.Managed] // TypedExternalConnectDisconnectorFns are functions that satisfy the // TypedExternalConnectDisconnector interface. type TypedExternalConnectDisconnectorFns[managed resource.Managed] struct { ConnectFn func(ctx context.Context, mg managed) (TypedExternalClient[managed], error) DisconnectFn func(ctx context.Context) error } // Connect to the provider specified by the supplied managed resource and // produce an ExternalClient. func (fns TypedExternalConnectDisconnectorFns[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) { return fns.ConnectFn(ctx, mg) } // Disconnect from the provider and close the ExternalClient. func (fns TypedExternalConnectDisconnectorFns[managed]) Disconnect(ctx context.Context) error { return fns.DisconnectFn(ctx) } // An ExternalClient manages the lifecycle of an external resource. // None of the calls here should be blocking. All of the calls should be // idempotent. For example, Create call should not return AlreadyExists error // if it's called again with the same parameters or Delete call should not // return error if there is an ongoing deletion or resource does not exist. type ExternalClient = TypedExternalClient[resource.Managed] // A TypedExternalClient manages the lifecycle of an external resource. // None of the calls here should be blocking. All of the calls should be // idempotent. For example, Create call should not return AlreadyExists error // if it's called again with the same parameters or Delete call should not // return error if there is an ongoing deletion or resource does not exist. type TypedExternalClient[managedType resource.Managed] interface { // Observe the external resource the supplied Managed resource // represents, if any. Observe implementations must not modify the // external resource, but may update the supplied Managed resource to // reflect the state of the external resource. Status modifications are // automatically persisted unless ResourceLateInitialized is true - see // ResourceLateInitialized for more detail. Observe(ctx context.Context, mg managedType) (ExternalObservation, error) // Create an external resource per the specifications of the supplied // Managed resource. Called when Observe reports that the associated // external resource does not exist. Create implementations may update // managed resource annotations, and those updates will be persisted. // All other updates will be discarded. Create(ctx context.Context, mg managedType) (ExternalCreation, error) // Update the external resource represented by the supplied Managed // resource, if necessary. Called unless Observe reports that the // associated external resource is up to date. Update(ctx context.Context, mg managedType) (ExternalUpdate, error) // Delete the external resource upon deletion of its associated Managed // resource. Called when the managed resource has been deleted. Delete(ctx context.Context, mg managedType) (ExternalDelete, error) // Disconnect from the provider and close the ExternalClient. // Called at the end of reconcile loop. An ExternalClient not requiring // to explicitly disconnect to cleanup it resources, can provide a no-op // implementation which just return nil. Disconnect(ctx context.Context) error } // ExternalClientFns are a series of functions that satisfy the ExternalClient // interface. type ExternalClientFns = TypedExternalClientFns[resource.Managed] // TypedExternalClientFns are a series of functions that satisfy the // ExternalClient interface. type TypedExternalClientFns[managed resource.Managed] struct { ObserveFn func(ctx context.Context, mg managed) (ExternalObservation, error) CreateFn func(ctx context.Context, mg managed) (ExternalCreation, error) UpdateFn func(ctx context.Context, mg managed) (ExternalUpdate, error) DeleteFn func(ctx context.Context, mg managed) (ExternalDelete, error) DisconnectFn func(ctx context.Context) error } // Observe the external resource the supplied Managed resource represents, if // any. func (e TypedExternalClientFns[managed]) Observe(ctx context.Context, mg managed) (ExternalObservation, error) { return e.ObserveFn(ctx, mg) } // Create an external resource per the specifications of the supplied Managed // resource. func (e TypedExternalClientFns[managed]) Create(ctx context.Context, mg managed) (ExternalCreation, error) { return e.CreateFn(ctx, mg) } // Update the external resource represented by the supplied Managed resource, if // necessary. func (e TypedExternalClientFns[managed]) Update(ctx context.Context, mg managed) (ExternalUpdate, error) { return e.UpdateFn(ctx, mg) } // Delete the external resource upon deletion of its associated Managed // resource. func (e TypedExternalClientFns[managed]) Delete(ctx context.Context, mg managed) (ExternalDelete, error) { return e.DeleteFn(ctx, mg) } // Disconnect the external client. func (e TypedExternalClientFns[managed]) Disconnect(ctx context.Context) error { return e.DisconnectFn(ctx) } // A NopConnector does nothing. type NopConnector struct{} // Connect returns a NopClient. It never returns an error. func (c *NopConnector) Connect(_ context.Context, _ resource.Managed) (ExternalClient, error) { return &NopClient{}, nil } // A NopClient does nothing. type NopClient struct{} // Observe does nothing. It returns an empty ExternalObservation and no error. func (c *NopClient) Observe(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{}, nil } // Create does nothing. It returns an empty ExternalCreation and no error. func (c *NopClient) Create(_ context.Context, _ resource.Managed) (ExternalCreation, error) { return ExternalCreation{}, nil } // Update does nothing. It returns an empty ExternalUpdate and no error. func (c *NopClient) Update(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, nil } // Delete does nothing. It never returns an error. func (c *NopClient) Delete(_ context.Context, _ resource.Managed) (ExternalDelete, error) { return ExternalDelete{}, nil } // Disconnect does nothing. It never returns an error. func (c *NopClient) Disconnect(_ context.Context) error { return nil } // An ExternalObservation is the result of an observation of an external // resource. type ExternalObservation struct { // ResourceExists must be true if a corresponding external resource exists // for the managed resource. Typically this is proven by the presence of an // external resource of the expected kind whose unique identifier matches // the managed resource's external name. Crossplane uses this information to // determine whether it needs to create or delete the external resource. ResourceExists bool // ResourceUpToDate should be true if the corresponding external resource // appears to be up-to-date - i.e. updating the external resource to match // the desired state of the managed resource would be a no-op. Keep in mind // that often only a subset of external resource fields can be updated. // Crossplane uses this information to determine whether it needs to update // the external resource. ResourceUpToDate bool // ResourceLateInitialized should be true if the managed resource's spec was // updated during its observation. A Crossplane provider may update a // managed resource's spec fields after it is created or updated, as long as // the updates are limited to setting previously unset fields, and adding // keys to maps. Crossplane uses this information to determine whether // changes to the spec were made during observation that must be persisted. // Note that changes to the spec will be persisted before changes to the // status, and that pending changes to the status may be lost when the spec // is persisted. Status changes will be persisted by the first subsequent // observation that _does not_ late initialize the managed resource, so it // is important that Observe implementations do not late initialize the // resource every time they are called. ResourceLateInitialized bool // ConnectionDetails required to connect to this resource. These details // are a set that is collated throughout the managed resource's lifecycle - // i.e. returning new connection details will have no affect on old details // unless an existing key is overwritten. Crossplane may publish these // credentials to a store (e.g. a Secret). ConnectionDetails ConnectionDetails // Diff is a Debug level message that is sent to the reconciler when // there is a change in the observed Managed Resource. It is useful for // finding where the observed diverges from the desired state. // The string should be a cmp.Diff that details the difference. Diff string } // An ExternalCreation is the result of the creation of an external resource. type ExternalCreation struct { // ConnectionDetails required to connect to this resource. These details // are a set that is collated throughout the managed resource's lifecycle - // i.e. returning new connection details will have no affect on old details // unless an existing key is overwritten. Crossplane may publish these // credentials to a store (e.g. a Secret). ConnectionDetails ConnectionDetails // AdditionalDetails represent any additional details the external client // wants to return about the creation operation that was performed. AdditionalDetails AdditionalDetails } // An ExternalUpdate is the result of an update to an external resource. type ExternalUpdate struct { // ConnectionDetails required to connect to this resource. These details // are a set that is collated throughout the managed resource's lifecycle - // i.e. returning new connection details will have no affect on old details // unless an existing key is overwritten. Crossplane may publish these // credentials to a store (e.g. a Secret). ConnectionDetails ConnectionDetails // AdditionalDetails represent any additional details the external client // wants to return about the update operation that was performed. AdditionalDetails AdditionalDetails } // An ExternalDelete is the result of a deletion of an external resource. type ExternalDelete struct { // AdditionalDetails represent any additional details the external client // wants to return about the delete operation that was performed. AdditionalDetails AdditionalDetails } // A Reconciler reconciles managed resources by creating and managing the // lifecycle of an external resource, i.e. a resource in an external system such // as a cloud provider API. Each controller must watch the managed resource kind // for which it is responsible. type Reconciler struct { client client.Client newManaged func() resource.Managed pollInterval time.Duration minPollInterval time.Duration pollIntervalHook PollIntervalHook timeout time.Duration creationGracePeriod time.Duration features feature.Flags // The below structs embed the set of interfaces used to implement the // managed resource reconciler. We do this primarily for readability, so // that the reconciler logic reads r.external.Connect(), // r.managed.Delete(), etc. external mrExternal managed mrManaged conditions conditions.Manager supportedManagementPolicies []sets.Set[xpv2.ManagementAction] log logging.Logger record event.Recorder metricRecorder MetricRecorder change ChangeLogger deterministicExternalName bool } type mrManaged struct { CriticalAnnotationUpdater ConnectionPublisher resource.Finalizer Initializer ReferenceResolver LocalConnectionPublisher } func defaultMRManaged(m manager.Manager) mrManaged { return mrManaged{ CriticalAnnotationUpdater: NewRetryingCriticalAnnotationUpdater(m.GetClient()), Finalizer: resource.NewAPIFinalizer(m.GetClient(), FinalizerName), Initializer: NewNameAsExternalName(m.GetClient()), ReferenceResolver: NewAPISimpleReferenceResolver(m.GetClient()), ConnectionPublisher: NewAPISecretPublisher(m.GetClient(), m.GetScheme()), LocalConnectionPublisher: NewAPILocalSecretPublisher(m.GetClient(), m.GetScheme()), } } func (m mrManaged) PublishConnection(ctx context.Context, managed resource.Managed, c ConnectionDetails) (bool, error) { switch so := managed.(type) { case resource.LocalConnectionSecretOwner: return m.LocalConnectionPublisher.PublishConnection(ctx, so, c) case resource.ConnectionSecretOwner: return m.ConnectionPublisher.PublishConnection(ctx, so, c) default: return false, errors.New(errManagedNotImplemented) } } func (m mrManaged) UnpublishConnection(ctx context.Context, managed resource.Managed, c ConnectionDetails) error { switch so := managed.(type) { case resource.LocalConnectionSecretOwner: return m.LocalConnectionPublisher.UnpublishConnection(ctx, so, c) case resource.ConnectionSecretOwner: return m.ConnectionPublisher.UnpublishConnection(ctx, so, c) default: return errors.New(errManagedNotImplemented) } } type mrExternal struct { ExternalConnectDisconnector } func defaultMRExternal() mrExternal { return mrExternal{ ExternalConnectDisconnector: NewNopDisconnector(&NopConnector{}), } } // A ReconcilerOption configures a Reconciler. type ReconcilerOption func(*Reconciler) // WithTimeout specifies the timeout duration cumulatively for all the calls happen // in the reconciliation function. In case the deadline exceeds, reconciler will // still have some time to make the necessary calls to report the error such as // status update. func WithTimeout(duration time.Duration) ReconcilerOption { return func(r *Reconciler) { r.timeout = duration } } // WithPollInterval specifies how long the Reconciler should wait before queueing // a new reconciliation after a successful reconcile. The Reconciler requeues // after a specified duration when it is not actively waiting for an external // operation, but wishes to check whether an existing external resource needs to // be synced to its Crossplane Managed resource. func WithPollInterval(after time.Duration) ReconcilerOption { return func(r *Reconciler) { r.pollInterval = after } } // WithMinPollInterval specifies the shortest poll interval a resource may // request via annotation. Annotation values below this floor are clamped to // the minimum. func WithMinPollInterval(d time.Duration) ReconcilerOption { return func(r *Reconciler) { r.minPollInterval = d } } // WithMetricRecorder configures the Reconciler to use the supplied MetricRecorder. func WithMetricRecorder(recorder MetricRecorder) ReconcilerOption { return func(r *Reconciler) { r.metricRecorder = recorder } } // PollIntervalHook represents the function type passed to the // WithPollIntervalHook option to support dynamic computation of the poll // interval. type PollIntervalHook func(managed resource.Managed, pollInterval time.Duration) time.Duration func defaultPollIntervalHook(_ resource.Managed, pollInterval time.Duration) time.Duration { return pollInterval } // WithPollIntervalHook adds a hook that can be used to configure the // delay before an up-to-date resource is reconciled again after a successful // reconcile. If this option is passed multiple times, only the latest hook // will be used. func WithPollIntervalHook(hook PollIntervalHook) ReconcilerOption { return func(r *Reconciler) { r.pollIntervalHook = hook } } // WithPollJitterHook adds a simple PollIntervalHook to add jitter to the poll // interval used when queuing a new reconciliation after a successful // reconcile. The added jitter will be a random duration between -jitter and // +jitter. This option wraps WithPollIntervalHook, and is subject to the same // constraint that only the latest hook will be used. func WithPollJitterHook(jitter time.Duration) ReconcilerOption { return WithPollIntervalHook(func(_ resource.Managed, pollInterval time.Duration) time.Duration { return pollInterval + time.Duration((rand.Float64()-0.5)*2*float64(jitter)) //nolint:gosec // No need for secure randomness. }) } // WithCreationGracePeriod configures an optional period during which we will // wait for the external API to report that a newly created external resource // exists. This allows us to tolerate eventually consistent APIs that do not // immediately report that newly created resources exist when queried. All // resources have a 30 second grace period by default. func WithCreationGracePeriod(d time.Duration) ReconcilerOption { return func(r *Reconciler) { r.creationGracePeriod = d } } // WithExternalConnector specifies how the Reconciler should connect to the API // used to sync and delete external resources. func WithExternalConnector(c ExternalConnector) ReconcilerOption { return func(r *Reconciler) { r.external.ExternalConnectDisconnector = NewNopDisconnector(c) } } // WithTypedExternalConnector specifies how the Reconciler should connect to the API // used to sync and delete external resources. func WithTypedExternalConnector[managed resource.Managed](c TypedExternalConnector[managed]) ReconcilerOption { return func(r *Reconciler) { r.external.ExternalConnectDisconnector = &typedExternalConnectDisconnectorWrapper[managed]{ c: NewTypedNopDisconnector(c), } } } // WithCriticalAnnotationUpdater specifies how the Reconciler should update a // managed resource's critical annotations. Implementations typically contain // some kind of retry logic to increase the likelihood that critical annotations // (like non-deterministic external names) will be persisted. func WithCriticalAnnotationUpdater(u CriticalAnnotationUpdater) ReconcilerOption { return func(r *Reconciler) { r.managed.CriticalAnnotationUpdater = u } } // withConnectionPublishers specifies how the Reconciler should publish // its connection details such as credentials and endpoints. // for unit testing only. func withConnectionPublishers(p ConnectionPublisher) ReconcilerOption { return func(r *Reconciler) { r.managed.ConnectionPublisher = p } } // withLocalConnectionPublishers specifies how the Reconciler should publish // its connection details such as credentials and endpoints. // for unit testing only. func withLocalConnectionPublishers(p LocalConnectionPublisher) ReconcilerOption { return func(r *Reconciler) { r.managed.LocalConnectionPublisher = p } } // WithInitializers specifies how the Reconciler should initialize a // managed resource before calling any of the ExternalClient functions. func WithInitializers(i ...Initializer) ReconcilerOption { return func(r *Reconciler) { r.managed.Initializer = InitializerChain(i) } } // WithFinalizer specifies how the Reconciler should add and remove // finalizers to and from the managed resource. func WithFinalizer(f resource.Finalizer) ReconcilerOption { return func(r *Reconciler) { r.managed.Finalizer = f } } // WithReferenceResolver specifies how the Reconciler should resolve any // inter-resource references it encounters while reconciling managed resources. func WithReferenceResolver(rr ReferenceResolver) ReconcilerOption { return func(r *Reconciler) { r.managed.ReferenceResolver = rr } } // WithLogger specifies how the Reconciler should log messages. func WithLogger(l logging.Logger) ReconcilerOption { return func(r *Reconciler) { r.log = l } } // WithRecorder specifies how the Reconciler should record events. func WithRecorder(er event.Recorder) ReconcilerOption { return func(r *Reconciler) { r.record = er } } // WithManagementPolicies enables support for management policies. func WithManagementPolicies() ReconcilerOption { return func(r *Reconciler) { r.features.Enable(feature.EnableBetaManagementPolicies) } } // WithReconcilerSupportedManagementPolicies configures which management policies are // supported by the reconciler. func WithReconcilerSupportedManagementPolicies(supported []sets.Set[xpv2.ManagementAction]) ReconcilerOption { return func(r *Reconciler) { r.supportedManagementPolicies = supported } } // WithChangeLogger enables support for capturing change logs during // reconciliation. func WithChangeLogger(c ChangeLogger) ReconcilerOption { return func(r *Reconciler) { r.change = c } } // WithDeterministicExternalName specifies that the external name of the MR is // deterministic. If this value is not "true", the provider will not re-queue the // managed resource in scenarios where creation is deemed incomplete. This behaviour // is a safeguard to avoid a leaked resource due to a non-deterministic name generated // by the external system. Conversely, if this value is "true", signifying that the // managed resources is deterministically named by the external system, then this // safeguard is ignored as it is safe to re-queue a deterministically named resource. func WithDeterministicExternalName(b bool) ReconcilerOption { return func(r *Reconciler) { r.deterministicExternalName = b } } // effectivePollInterval returns the poll interval for the given resource, // taking into account any per-resource override via annotation. Overrides // below the configured minimum are clamped to the minimum. func (r *Reconciler) effectivePollInterval(o metav1.Object) time.Duration { if d, ok := meta.GetPollInterval(o); ok { if d >= r.minPollInterval { return d } return r.minPollInterval } return r.pollInterval } // NewReconciler returns a Reconciler that reconciles managed resources of the // supplied ManagedKind with resources in an external system such as a cloud // provider API. It panics if asked to reconcile a managed resource kind that is // not registered with the supplied manager's runtime.Scheme. The returned // Reconciler reconciles with a dummy, no-op 'external system' by default; // callers should supply an ExternalConnector that returns an ExternalClient // capable of managing resources in a real system. func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOption) *Reconciler { nm := func() resource.Managed { //nolint:forcetypeassert // If this isn't an MR it's a programming error and we want to panic. return resource.MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(resource.Managed) } // Panic early if we've been asked to reconcile a resource kind that has not // been registered with our controller manager's scheme. _ = nm() r := &Reconciler{ client: m.GetClient(), newManaged: nm, pollInterval: defaultPollInterval, pollIntervalHook: defaultPollIntervalHook, creationGracePeriod: defaultGracePeriod, timeout: reconcileTimeout, managed: defaultMRManaged(m), external: defaultMRExternal(), supportedManagementPolicies: defaultSupportedManagementPolicies(), log: logging.NewNopLogger(), record: event.NewNopRecorder(), metricRecorder: NewNopMetricRecorder(), change: newNopChangeLogger(), conditions: new(conditions.ObservedGenerationPropagationManager), } for _, ro := range o { ro(r) } return r } // Reconcile a managed resource with an external resource. func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (result reconcile.Result, err error) { //nolint:gocognit // See note below. // NOTE(negz): This method is a well over our cyclomatic complexity goal. // Be wary of adding additional complexity. defer func() { result, err = errors.SilentlyRequeueOnConflict(result, err) }() log := r.log.WithValues("request", req) log.Debug("Reconciling") ctx, cancel := context.WithTimeout(ctx, r.timeout+reconcileGracePeriod) defer cancel() externalCtx, externalCancel := context.WithTimeout(ctx, r.timeout) defer externalCancel() managed := r.newManaged() if err := r.client.Get(ctx, req.NamespacedName, managed); err != nil { // There's no need to requeue if we no longer exist. Otherwise we'll be // requeued implicitly because we return an error. log.Debug("Cannot get managed resource", "error", err) return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetManaged) } r.metricRecorder.recordFirstTimeReconciled(managed) status := r.conditions.For(managed) record := r.record.WithAnnotations("external-name", meta.GetExternalName(managed)) log = log.WithValues( "uid", managed.GetUID(), "version", managed.GetResourceVersion(), "external-name", meta.GetExternalName(managed), ) managementPoliciesEnabled := r.features.Enabled(feature.EnableBetaManagementPolicies) if managementPoliciesEnabled { log.WithValues("managementPolicies", managed.GetManagementPolicies()) } // Create the management policy resolver which will assist us in determining // what actions to take on the managed resource based on the management // and deletion policies. var policy ManagementPoliciesChecker switch mg := managed.(type) { case resource.LegacyManaged: policy = NewLegacyManagementPoliciesResolver(managementPoliciesEnabled, mg.GetManagementPolicies(), mg.GetDeletionPolicy(), WithSupportedManagementPolicies(r.supportedManagementPolicies)) default: policy = NewManagementPoliciesResolver(managementPoliciesEnabled, managed.GetManagementPolicies(), WithSupportedManagementPolicies(r.supportedManagementPolicies)) } // Check if the resource has paused reconciliation based on the // annotation or the management policies. // Log, publish an event and update the SYNC status condition. if meta.IsPaused(managed) || policy.IsPaused() { log.Debug("Reconciliation is paused either through the `spec.managementPolicies` or the pause annotation", "annotation", meta.AnnotationKeyReconciliationPaused) record.Event(managed, event.Normal(reasonReconciliationPaused, "Reconciliation is paused either through the `spec.managementPolicies` or the pause annotation", "annotation", meta.AnnotationKeyReconciliationPaused)) status.MarkConditions(xpv2.ReconcilePaused()) // if the pause annotation is removed or the management policies changed, we will have a chance to reconcile // again and resume and if status update fails, we will reconcile again to retry to update the status return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } // Detect a new reconcile-request token early and emit the event once. // The actual status mutation is deferred to updateStatus so it survives // full-object Updates (late-init, create annotations) that reset // in-memory status. var reconcileRequestToken string if token, ok := meta.GetReconcileRequest(managed); ok { if tracker, ok := managed.(reconcileRequestTracker); ok { if tracker.GetLastHandledReconcileAt() != token { log.Debug("Processing reconcile request", "token", token) record.Event(managed, event.Normal(reasonReconcileRequestHandled, "Handling reconcile request", "token", token)) reconcileRequestToken = token } } } // updateStatus applies the reconcile-request token (if any) immediately // before persisting status. This ensures the token is not lost by // intervening full-object Updates. updateStatus := func() error { if reconcileRequestToken != "" { if tracker, ok := managed.(reconcileRequestTracker); ok { tracker.SetLastHandledReconcileAt(reconcileRequestToken) } } return r.client.Status().Update(ctx, managed) } // Check if the ManagementPolicies is set to a non-default value while the // feature is not enabled. This is a safety check to let users know that // they need to enable the feature flag before using the feature. For // example, we wouldn't want someone to set the policy to ObserveOnly but // not realize that the controller is still trying to reconcile // (and modify or delete) the resource since they forgot to enable the // feature flag. Also checks if the management policy is set to a value // that is not supported by the controller. if err := policy.Validate(); err != nil { log.Debug(err.Error()) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonManagementPolicyInvalid, err)) status.MarkConditions(xpv2.ReconcileError(err)) return reconcile.Result{}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // If managed resource has a deletion timestamp and a deletion policy of // Orphan, we do not need to observe the external resource before attempting // to unpublish connection details and remove finalizer. if meta.WasDeleted(managed) && !policy.ShouldDelete() { log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp()) // Empty ConnectionDetails are passed to UnpublishConnection because we // have not retrieved them from the external resource. In practice we // currently only write connection details to a Secret, and we rely on // garbage collection to delete the entire secret, regardless of the // supplied connection details. if err := r.managed.UnpublishConnection(ctx, managed, ConnectionDetails{}); err != nil { // If this is the first time we encounter this issue we'll be // requeued implicitly when we update our status with the new error // condition. If not, we requeue explicitly, which will trigger // backoff. log.Debug("Cannot unpublish connection details", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotUnpublish, err)) status.MarkConditions(xpv2.Deleting(), xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } if err := r.managed.RemoveFinalizer(ctx, managed); err != nil { // If this is the first time we encounter this issue we'll be // requeued implicitly when we update our status with the new error // condition. If not, we requeue explicitly, which will trigger // backoff. log.Debug("Cannot remove managed resource finalizer", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } status.MarkConditions(xpv2.Deleting(), xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // We've successfully unpublished our managed resource's connection // details and removed our finalizer. If we assume we were the only // controller that added a finalizer to this resource then it should no // longer exist and thus there is no point trying to update its status. r.metricRecorder.recordDeleted(managed) log.Debug("Successfully deleted managed resource") return reconcile.Result{Requeue: false}, nil } if err := r.managed.Initialize(ctx, managed); err != nil { // If this is the first time we encounter this issue we'll be requeued // implicitly when we update our status with the new error condition. If // not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot initialize managed resource", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotInitialize, err)) status.MarkConditions(xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // If we started but never completed creation of an external resource we // may have lost critical information. For example if we didn't persist // an updated external name which is non-deterministic, we have leaked a // resource. The safest thing to do is to refuse to proceed. However, if // the resource has a deterministic external name, it is safe to proceed. if meta.ExternalCreateIncomplete(managed) { if !r.deterministicExternalName { log.Debug(errCreateIncomplete) record.Event(managed, event.Warning(reasonCannotInitialize, errors.New(errCreateIncomplete))) status.MarkConditions(xpv2.Creating(), xpv2.ReconcileError(errors.New(errCreateIncomplete))) return reconcile.Result{Requeue: false}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } log.Debug("Cannot determine creation result, but proceeding due to deterministic external name") } // We resolve any references before observing our external resource because // in some rare examples we need a spec field to make the observe call, and // that spec field could be set by a reference. // // We do not resolve references when being deleted because it is likely that // the resources we reference are also being deleted, and would thus block // resolution due to being unready or non-existent. It is unlikely (but not // impossible) that we need to resolve a reference in order to process a // delete, and that reference is stale at delete time. if !meta.WasDeleted(managed) { if err := r.managed.ResolveReferences(ctx, managed); err != nil { // If any of our referenced resources are not yet ready (or if we // encountered an error resolving them) we want to try again. If // this is the first time we encounter this situation we'll be // requeued implicitly due to the status update. If not, we want // requeue explicitly, which will trigger backoff. log.Debug("Cannot resolve managed resource references", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotResolveRefs, err)) status.MarkConditions(xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } } external, err := r.external.Connect(externalCtx, managed) if err != nil { // We'll usually hit this case if our Provider or its secret are missing // or invalid. If this is first time we encounter this issue we'll be // requeued implicitly when we update our status with the new error // condition. If not, we requeue explicitly, which will trigger // backoff. log.Debug("Cannot connect to provider", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotConnect, err)) status.MarkConditions(xpv2.ReconcileError(errors.Wrap(err, errReconcileConnect))) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } defer func() { if err := r.external.Disconnect(ctx); err != nil { log.Debug("Cannot disconnect from provider", "error", err) record.Event(managed, event.Warning(reasonCannotDisconnect, err)) } if external != nil { if err := external.Disconnect(ctx); err != nil { log.Debug("Cannot disconnect from provider", "error", err) record.Event(managed, event.Warning(reasonCannotDisconnect, err)) } } }() observation, err := external.Observe(externalCtx, managed) if err != nil { // We'll usually hit this case if our Provider credentials are invalid // or insufficient for observing the external resource type we're // concerned with. If this is the first time we encounter this issue // we'll be requeued implicitly when we update our status with the new // error condition. If not, we requeue explicitly, which will // trigger backoff. log.Debug("Cannot observe external resource", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotObserve, err)) status.MarkConditions(xpv2.ReconcileError(errors.Wrap(err, errReconcileObserve))) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // In the observe-only mode, !observation.ResourceExists will be an error // case, and we will explicitly return this information to the user. if !observation.ResourceExists && policy.ShouldOnlyObserve() { record.Event(managed, event.Warning(reasonCannotObserve, errors.New(errExternalResourceNotExist))) status.MarkConditions(xpv2.ReconcileError(errors.Wrap(errors.New(errExternalResourceNotExist), errReconcileObserve))) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // If this resource has a non-zero creation grace period we want to wait // for that period to expire before we trust that the resource really // doesn't exist. This is because some external APIs are eventually // consistent and may report that a recently created resource does not // exist. if !observation.ResourceExists && meta.ExternalCreateSucceededDuring(managed, r.creationGracePeriod) { log.Debug("Waiting for external resource existence to be confirmed") record.Event(managed, event.Normal(reasonPending, "Waiting for external resource existence to be confirmed")) return reconcile.Result{Requeue: true}, nil } // deep copy the managed resource now that we've called Observe() and have // not performed any external operations - we can use this as the // pre-operation managed resource state in the change logs later //nolint:forcetypeassert // managed.DeepCopyObject() will always be a resource.Managed. managedPreOp := managed.DeepCopyObject().(resource.Managed) if meta.WasDeleted(managed) { log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp()) if len(managed.GetFinalizers()) > 1 { // There are other controllers monitoring this resource so preserve the external instance // until all other finalizers have been removed log.Debug("Delay external deletion until all finalizers have been removed") return reconcile.Result{Requeue: true}, nil } if observation.ResourceExists && policy.ShouldDelete() { deletion, err := external.Delete(externalCtx, managed) if err != nil { // We'll hit this condition if we can't delete our external // resource, for example if our provider credentials don't have // access to delete it. If this is the first time we encounter // this issue we'll be requeued implicitly when we update our // status with the new error condition. If not, we want requeue // explicitly, which will trigger backoff. log.Debug("Cannot delete external resource", "error", err) if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_DELETE, err, deletion.AdditionalDetails); err != nil { log.Info(errRecordChangeLog, "error", err) } record.Event(managed, event.Warning(reasonCannotDelete, err)) status.MarkConditions(xpv2.Deleting(), xpv2.ReconcileError(errors.Wrap(err, errReconcileDelete))) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // We've successfully requested deletion of our external resource. // We queue another reconcile after a short wait rather than // immediately finalizing our delete in order to verify that the // external resource was actually deleted. If it no longer exists // we'll skip this block on the next reconcile and proceed to // unpublish and finalize. If it still exists we'll re-enter this // block and try again. log.Debug("Successfully requested deletion of external resource") if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_DELETE, nil, deletion.AdditionalDetails); err != nil { log.Info(errRecordChangeLog, "error", err) } record.Event(managed, event.Normal(reasonDeleted, "Successfully requested deletion of external resource")) status.MarkConditions(xpv2.Deleting(), xpv2.ReconcileSuccess()) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } if err := r.managed.UnpublishConnection(ctx, managed, observation.ConnectionDetails); err != nil { // If this is the first time we encounter this issue we'll be // requeued implicitly when we update our status with the new error // condition. If not, we requeue explicitly, which will trigger // backoff. log.Debug("Cannot unpublish connection details", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotUnpublish, err)) status.MarkConditions(xpv2.Deleting(), xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } if err := r.managed.RemoveFinalizer(ctx, managed); err != nil { // If this is the first time we encounter this issue we'll be // requeued implicitly when we update our status with the new error // condition. If not, we requeue explicitly, which will trigger // backoff. log.Debug("Cannot remove managed resource finalizer", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } status.MarkConditions(xpv2.Deleting(), xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // We've successfully deleted our external resource (if necessary) and // removed our finalizer. If we assume we were the only controller that // added a finalizer to this resource then it should no longer exist and // thus there is no point trying to update its status. r.metricRecorder.recordDeleted(managed) log.Debug("Successfully deleted managed resource") return reconcile.Result{Requeue: false}, nil } if _, err := r.managed.PublishConnection(ctx, managed, observation.ConnectionDetails); err != nil { // If this is the first time we encounter this issue we'll be requeued // implicitly when we update our status with the new error condition. If // not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot publish connection details", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotPublish, err)) status.MarkConditions(xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } if err := r.managed.AddFinalizer(ctx, managed); err != nil { // If this is the first time we encounter this issue we'll be requeued // implicitly when we update our status with the new error condition. If // not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot add finalizer", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } status.MarkConditions(xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } if !observation.ResourceExists && policy.ShouldCreate() { // We write this annotation for two reasons. Firstly, it helps // us to detect the case in which we fail to persist critical // information (like the external name) that may be set by the // subsequent external.Create call. Secondly, it guarantees that // we're operating on the latest version of our resource. We // don't use the CriticalAnnotationUpdater because we _want_ the // update to fail if we get a 409 due to a stale version. meta.SetExternalCreatePending(managed, time.Now()) if err := r.client.Update(ctx, managed); err != nil { log.Debug(errUpdateManaged, "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManaged))) status.MarkConditions(xpv2.Creating(), xpv2.ReconcileError(errors.Wrap(err, errUpdateManaged))) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } creation, err := external.Create(externalCtx, managed) if err != nil { // We'll hit this condition if we can't create our external // resource, for example if our provider credentials don't have // access to create it. If this is the first time we encounter this // issue we'll be requeued implicitly when we update our status with // the new error condition. If not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot create external resource", "error", err) if !kerrors.IsConflict(err) { record.Event(managed, event.Warning(reasonCannotCreate, err)) } // We handle annotations specially here because it's // critical that they are persisted to the API server. // If we don't add the external-create-failed annotation // the reconciler will refuse to proceed, because it // won't know whether or not it created an external // resource. meta.SetExternalCreateFailed(managed, time.Now()) if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil { log.Debug(errUpdateManagedAnnotations, "error", err) record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAnnotations))) // We only log and emit an event here rather // than setting a status condition and returning // early because presumably it's more useful to // set our status condition to the reason the // create failed. } if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_CREATE, err, creation.AdditionalDetails); err != nil { log.Info(errRecordChangeLog, "error", err) } status.MarkConditions(xpv2.Creating(), xpv2.ReconcileError(errors.Wrap(err, errReconcileCreate))) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // In some cases our external-name may be set by Create above. log = log.WithValues("external-name", meta.GetExternalName(managed)) record = r.record.WithAnnotations("external-name", meta.GetExternalName(managed)) if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_CREATE, nil, creation.AdditionalDetails); err != nil { log.Info(errRecordChangeLog, "error", err) } // We handle annotations specially here because it's critical // that they are persisted to the API server. If we don't remove // add the external-create-succeeded annotation the reconciler // will refuse to proceed, because it won't know whether or not // it created an external resource. This is also important in // cases where we must record an external-name annotation set by // the Create call. Any other changes made during Create will be // reverted when annotations are updated; at the time of writing // Create implementations are advised not to alter status, but // we may revisit this in future. meta.SetExternalCreateSucceeded(managed, time.Now()) if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil { log.Debug(errUpdateManagedAnnotations, "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAnnotations))) status.MarkConditions(xpv2.Creating(), xpv2.ReconcileError(errors.Wrap(err, errUpdateManagedAnnotations))) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } if _, err := r.managed.PublishConnection(ctx, managed, creation.ConnectionDetails); err != nil { // If this is the first time we encounter this issue we'll be // requeued implicitly when we update our status with the new error // condition. If not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot publish connection details", "error", err) if kerrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil } record.Event(managed, event.Warning(reasonCannotPublish, err)) status.MarkConditions(xpv2.Creating(), xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // We've successfully created our external resource. In many cases the // creation process takes a little time to finish. We requeue explicitly // order to observe the external resource to determine whether it's // ready for use. log.Debug("Successfully requested creation of external resource") record.Event(managed, event.Normal(reasonCreated, "Successfully requested creation of external resource")) status.MarkConditions(xpv2.Creating(), xpv2.ReconcileSuccess()) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } if observation.ResourceLateInitialized && policy.ShouldLateInitialize() { // Note that this update may reset any pending updates to the status of // the managed resource from when it was observed above. This is because // the API server replies to the update with its unchanged view of the // resource's status, which is subsequently deserialized into managed. // This is usually tolerable because the update will implicitly requeue // an immediate reconcile which should re-observe the external resource // and persist its status. if err := r.client.Update(ctx, managed); err != nil { log.Debug(errUpdateManaged, "error", err) record.Event(managed, event.Warning(reasonCannotUpdateManaged, err)) status.MarkConditions(xpv2.ReconcileError(errors.Wrap(err, errUpdateManaged))) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } } if observation.ResourceUpToDate { // We did not need to create, update, or delete our external resource. // Per the below issue nothing will notify us if and when the external // resource we manage changes, so we requeue a speculative reconcile // after the specified poll interval in order to observe it and react // accordingly. // https://github.com/crossplane/crossplane/issues/289 reconcileAfter := r.pollIntervalHook(managed, r.effectivePollInterval(managed)) log.Debug("External resource is up to date", "requeue-after", time.Now().Add(reconcileAfter)) status.MarkConditions(xpv2.ReconcileSuccess()) r.metricRecorder.recordFirstTimeReady(managed) // record that we intentionally did not update the managed resource // because no drift was detected. We call this so late in the reconcile // because all the cases above could contribute (for different reasons) // that the external object would not have been updated. r.metricRecorder.recordUnchanged(managed.GetName()) return reconcile.Result{RequeueAfter: reconcileAfter}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } if observation.Diff != "" { log.Debug("External resource differs from desired state", "diff", observation.Diff) } // skip the update if the management policy is set to ignore updates if !policy.ShouldUpdate() { reconcileAfter := r.pollIntervalHook(managed, r.effectivePollInterval(managed)) log.Debug("Skipping update due to managementPolicies. Reconciliation succeeded", "requeue-after", time.Now().Add(reconcileAfter)) status.MarkConditions(xpv2.ReconcileSuccess()) return reconcile.Result{RequeueAfter: reconcileAfter}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } update, err := external.Update(externalCtx, managed) if err != nil { // We'll hit this condition if we can't update our external resource, // for example if our provider credentials don't have access to update // it. If this is the first time we encounter this issue we'll be // requeued implicitly when we update our status with the new error // condition. If not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot update external resource") if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_UPDATE, err, update.AdditionalDetails); err != nil { log.Info(errRecordChangeLog, "error", err) } record.Event(managed, event.Warning(reasonCannotUpdate, err)) status.MarkConditions(xpv2.ReconcileError(errors.Wrap(err, errReconcileUpdate))) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // record the drift after the successful update. r.metricRecorder.recordDrift(managed) if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_UPDATE, nil, update.AdditionalDetails); err != nil { log.Info(errRecordChangeLog, "error", err) } if _, err := r.managed.PublishConnection(ctx, managed, update.ConnectionDetails); err != nil { // If this is the first time we encounter this issue we'll be requeued // implicitly when we update our status with the new error condition. If // not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot publish connection details", "error", err) record.Event(managed, event.Warning(reasonCannotPublish, err)) status.MarkConditions(xpv2.ReconcileError(err)) return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } // We've successfully updated our external resource. Per the below issue // nothing will notify us if and when the external resource we manage // changes, so we requeue a speculative reconcile after the specified poll // interval in order to observe it and react accordingly. // https://github.com/crossplane/crossplane/issues/289 reconcileAfter := r.pollIntervalHook(managed, r.effectivePollInterval(managed)) log.Debug("Successfully requested update of external resource", "requeue-after", time.Now().Add(reconcileAfter)) record.Event(managed, event.Normal(reasonUpdated, "Successfully requested update of external resource")) status.MarkConditions(xpv2.ReconcileSuccess()) return reconcile.Result{RequeueAfter: reconcileAfter}, errors.Wrap(updateStatus(), errUpdateManagedStatus) } ================================================ FILE: pkg/reconciler/managed/reconciler_deprecated.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "context" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) // ExternalConnecter an alias to ExternalConnector. // // Deprecated: use ExternalConnector. type ExternalConnecter = ExternalConnector // TypedExternalConnecter an alias to TypedExternalConnector. // // Deprecated: use TypedExternalConnector. type TypedExternalConnecter[managed resource.Managed] interface { TypedExternalConnector[managed] } // An ExternalDisconnector disconnects from a provider. // // Deprecated: Please use Disconnect() on the ExternalClient for disconnecting // from the provider. // //nolint:iface // We know it is a redundant interface. type ExternalDisconnector interface { // Disconnect from the provider and close the ExternalClient. Disconnect(ctx context.Context) error } // ExternalDisconnecter an alias to ExternalDisconnector. // // Deprecated: Please use Disconnect() on the ExternalClient for disconnecting // from the provider. // //nolint:iface // We know it is a redundant interface type ExternalDisconnecter interface { ExternalDisconnector } // NopDisconnecter aliases NopDisconnector. // // Deprecated: Use NopDisconnector. type NopDisconnecter = NopDisconnector // TODO: these types of aliases are only allowed in Go 1.23 and above. // type TypedNopDisconnecter[managed resource.Managed] = TypedNopDisconnector[managed] // type TypedNopDisconnecter[managed resource.Managed] = TypedNopDisconnector[managed] // type TypedExternalConnectDisconnecterFns[managed resource.Managed] = TypedExternalConnectDisconnectorFns[managed] // NewNopDisconnecter an alias to NewNopDisconnector. // // Deprecated: use NewNopDisconnector. func NewNopDisconnecter(c ExternalConnector) ExternalConnectDisconnector { return NewNopDisconnector(c) } // ExternalDisconnecterFn aliases ExternalDisconnectorFn. // // Deprecated: use ExternalDisconnectorFn. type ExternalDisconnecterFn = ExternalDisconnectorFn // ExternalConnectDisconnecterFns aliases ExternalConnectDisconnectorFns. // // Deprecated: use ExternalConnectDisconnectorFns. type ExternalConnectDisconnecterFns = ExternalConnectDisconnectorFns // NopConnecter aliases NopConnector. // // Deprecated: use NopConnector. type NopConnecter = NopConnector // WithExternalConnecter aliases WithExternalConnector. // // Deprecated: use WithExternalConnector. func WithExternalConnecter(c ExternalConnector) ReconcilerOption { return WithExternalConnector(c) } // WithExternalConnectDisconnector specifies how the Reconciler should connect and disconnect to the API // used to sync and delete external resources. // // Deprecated: Please use Disconnect() on the ExternalClient for disconnecting from the provider. func WithExternalConnectDisconnector(c ExternalConnectDisconnector) ReconcilerOption { return func(r *Reconciler) { r.external.ExternalConnectDisconnector = c } } // WithExternalConnectDisconnecter aliases WithExternalConnectDisconnector. // // Deprecated: Please use Disconnect() on the ExternalClient for disconnecting from the provider. func WithExternalConnectDisconnecter(c ExternalConnectDisconnector) ReconcilerOption { return func(r *Reconciler) { r.external.ExternalConnectDisconnector = c } } // WithTypedExternalConnectDisconnector specifies how the Reconciler should connect and disconnect to the API // used to sync and delete external resources. // // Deprecated: Please use Disconnect() on the ExternalClient for disconnecting from the provider. func WithTypedExternalConnectDisconnector[managed resource.Managed](c TypedExternalConnectDisconnector[managed]) ReconcilerOption { return func(r *Reconciler) { r.external.ExternalConnectDisconnector = &typedExternalConnectDisconnectorWrapper[managed]{c} } } // WithTypedExternalConnectDisconnecter aliases WithTypedExternalConnectDisconnector. // // Deprecated: Please use Disconnect() on the ExternalClient for disconnecting from the provider. func WithTypedExternalConnectDisconnecter[managed resource.Managed](c TypedExternalConnectDisconnector[managed]) ReconcilerOption { return func(r *Reconciler) { r.external.ExternalConnectDisconnector = &typedExternalConnectDisconnectorWrapper[managed]{c} } } ================================================ FILE: pkg/reconciler/managed/reconciler_legacy_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "context" "fmt" "testing" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/crossplane/crossplane-runtime/v2/apis/changelogs/proto/v1alpha1" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var _ reconcile.Reconciler = &Reconciler{} func TestReconciler(t *testing.T) { type args struct { m manager.Manager mg resource.ManagedKind o []ReconcilerOption } type want struct { result reconcile.Result resultCmpOpts []cmp.Option err error } errBoom := errors.New("boom") now := metav1.Now() cases := map[string]struct { reason string args args want want }{ "GetManagedError": { reason: "Any error (except not found) encountered while getting the resource under reconciliation should be returned.", args: args{ m: &fake.Manager{ Client: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)}, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), }, want: want{err: errors.Wrap(errBoom, errGetManaged)}, }, "ManagedNotFound": { reason: "Not found errors encountered while getting the resource under reconciliation should be ignored.", args: args{ m: &fake.Manager{ Client: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))}, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), }, want: want{result: reconcile.Result{}}, }, "UnpublishConnectionDetailsDeletionPolicyDeleteOrpahn": { reason: "Errors unpublishing connection details should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) mg.SetDeletionPolicy(xpv2.DeletionOrphan) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetDeletionTimestamp(&now) want.SetDeletionPolicy(xpv2.DeletionOrphan) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors unpublishing connection details should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ withConnectionPublishers(ConnectionPublisherFns{ UnpublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) error { return errBoom }, }), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "RemoveFinalizerErrorDeletionPolicyOrphan": { reason: "Errors removing the managed resource finalizer should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) mg.SetDeletionPolicy(xpv2.DeletionOrphan) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetDeletionTimestamp(&now) want.SetDeletionPolicy(xpv2.DeletionOrphan) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors removing the managed resource finalizer should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return errBoom }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "DeleteSuccessfulDeletionPolicyOrphan": { reason: "Successful managed resource deletion with deletion policy Orphan should not trigger a requeue or status update.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) mg.SetDeletionPolicy(xpv2.DeletionOrphan) return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: false}}, }, "InitializeError": { reason: "Errors initializing the managed resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors initializing the managed resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(InitializerFn(func(_ context.Context, _ resource.Managed) error { return errBoom })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExtraFinalizersDelayDelete": { reason: "The existence of multiple finalizers should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) mg.SetDeletionPolicy(xpv2.DeletionDelete) mg.SetFinalizers([]string{FinalizerName, "finalizer2"}) return nil }), MockUpdate: test.NewMockUpdateFn(nil), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalCreatePending": { reason: "We should return early if the managed resource appears to be pending creation. We might have leaked a resource and don't want to create another.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { asLegacyManaged(obj, 42) meta.SetExternalCreatePending(obj, now.Time) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) meta.SetExternalCreatePending(want, now.Time) want.SetConditions( xpv2.Creating().WithObservedGeneration(42), xpv2.ReconcileError(errors.New(errCreateIncomplete)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "We should update our status when we're asked to reconcile a managed resource that is pending creation." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(InitializerFn(func(_ context.Context, _ resource.Managed) error { return nil })), }, }, want: want{result: reconcile.Result{Requeue: false}}, }, "ResolveReferencesError": { reason: "Errors during reference resolution references should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors during reference resolution should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return errBoom })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalConnectError": { reason: "Errors connecting to the provider should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, got client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileConnect)).WithObservedGeneration(42)) if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" { reason := "Errors connecting to the provider should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { return nil, errBoom })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalDisconnectError": { reason: "Error disconnecting from the provider should not trigger requeue.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful no-op reconcile should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return errBoom }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ExternalObserveError": { reason: "Errors observing the external resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileObserve)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors observing the managed resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "CreationGracePeriod": { reason: "If our resource appears not to exist during the creation grace period we should return early.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalCreateSucceeded(obj, time.Now()) return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithCreationGracePeriod(1 * time.Minute), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalDeleteError": { reason: "Errors deleting the external resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetDeletionTimestamp(&now) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileDelete)).WithObservedGeneration(42)) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "An error deleting an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { return ExternalDelete{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalDeleteSuccessful": { reason: "A deleted managed resource with the 'delete' reclaim policy should delete its external resource then requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetDeletionTimestamp(&now) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A deleted external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { return ExternalDelete{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "UnpublishConnectionDetailsDeletionPolicyDeleteError": { reason: "Errors unpublishing connection details should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetDeletionTimestamp(&now) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors unpublishing connection details should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), withConnectionPublishers(ConnectionPublisherFns{ UnpublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) error { return errBoom }, }), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "RemoveFinalizerErrorDeletionPolicyDelete": { reason: "Errors removing the managed resource finalizer should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetDeletionTimestamp(&now) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors removing the managed resource finalizer should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return errBoom }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "DeleteSuccessfulDeletionPolicyDelete": { reason: "Successful managed resource deletion with deletion policy Delete should not trigger a requeue or status update.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: false}}, }, "PublishObservationConnectionDetailsError": { reason: "Errors publishing connection details after observation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors publishing connection details after observation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), withConnectionPublishers(ConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) (bool, error) { return false, errBoom }, }), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "AddFinalizerError": { reason: "Errors adding a finalizer should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors adding a finalizer should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return errBoom }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "UpdateCreatePendingError": { reason: "Errors while updating our external-create-pending annotation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(errBoom), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) meta.SetExternalCreatePending(want, time.Now()) want.SetConditions( xpv2.Creating().WithObservedGeneration(42), xpv2.ReconcileError(errors.Wrap(errBoom, errUpdateManaged)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Errors while creating an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { return ExternalCreation{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "CreateExternalError": { reason: "Errors while creating an external resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateFailed(want, time.Now()) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileCreate)).WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Errors while creating an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { return ExternalCreation{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), // We simulate our critical annotation update failing too here. // This is mostly just to exercise the code, which just creates a log and an event. WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return errBoom })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "UpdateCriticalAnnotationsError": { reason: "Errors updating critical annotations after creation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errUpdateManagedAnnotations)).WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Errors updating critical annotations after creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { return ExternalCreation{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return errBoom })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "PublishCreationConnectionDetailsError": { reason: "Errors publishing connection details after creation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Errors publishing connection details after creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { cd := ConnectionDetails{"create": []byte{}} return ExternalCreation{ConnectionDetails: cd}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), withConnectionPublishers(ConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, cd ConnectionDetails) (bool, error) { // We're called after observe, create, and update // but we only want to fail when publishing details // after a creation. if _, ok := cd["create"]; ok { return false, errBoom } return true, nil }, }), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "CreateSuccessful": { reason: "Successful managed resource creation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Successful managed resource creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "CreateSuccessfulAfterExternalCreatePendingAndDeterministicName": { reason: "Successful managed resource creation which was previously pending and has a deterministic external name should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { asLegacyManaged(obj, 42) meta.SetExternalCreatePending(obj, now.Time) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Successful managed resource creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithDeterministicExternalName(true), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "LateInitializeUpdateError": { reason: "Errors updating a managed resource to persist late initialized fields should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(errBoom), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errUpdateManaged)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors updating a managed resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true, ResourceLateInitialized: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalResourceUpToDate": { reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful no-op reconcile should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ExternalResourceUpToDateWithJitter": { reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait with jitter added.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithPollJitterHook(time.Second), }, }, want: want{ result: reconcile.Result{RequeueAfter: defaultPollInterval}, resultCmpOpts: []cmp.Option{cmp.Comparer(func(l, r time.Duration) bool { diff := l - r if diff < 0 { diff = -diff } return diff < time.Second })}, }, }, "ExternalResourceUpToDateWithPollIntervalHook": { reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait processed by the interval hook.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithPollIntervalHook(func(_ resource.Managed, pollInterval time.Duration) time.Duration { return 2 * pollInterval }), }, }, want: want{ result: reconcile.Result{RequeueAfter: 2 * defaultPollInterval}, }, }, "ExternalResourceUpToDateWithMultiplePollIntervalHooks": { reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait processed by the latest interval hook.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithPollJitterHook(time.Second), WithPollIntervalHook(func(_ resource.Managed, pollInterval time.Duration) time.Duration { return 2 * pollInterval }), WithPollIntervalHook(func(_ resource.Managed, pollInterval time.Duration) time.Duration { return 3 * pollInterval }), }, }, want: want{ result: reconcile.Result{RequeueAfter: 3 * defaultPollInterval}, }, }, "UpdateExternalError": { reason: "Errors while updating an external resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileUpdate)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors while updating an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "PublishUpdateConnectionDetailsError": { reason: "Errors publishing connection details after an update should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors publishing connection details after an update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { cd := ConnectionDetails{"update": []byte{}} return ExternalUpdate{ConnectionDetails: cd}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), withConnectionPublishers(ConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, cd ConnectionDetails) (bool, error) { // We're called after observe, create, and update // but we only want to fail when publishing details // after an update. if _, ok := cd["update"]; ok { return false, errBoom } return false, nil }, }), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "UpdateSuccessful": { reason: "A successful managed resource update should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful managed resource update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "TypedReconcilerUpdateSuccessful": { reason: "A successful managed resource update should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful managed resource update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithTypedExternalConnector(TypedExternalConnectorFn[*fake.LegacyManaged](func(_ context.Context, _ *fake.LegacyManaged) (TypedExternalClient[*fake.LegacyManaged], error) { c := &TypedExternalClientFns[*fake.LegacyManaged]{ ObserveFn: func(_ context.Context, _ *fake.LegacyManaged) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ *fake.LegacyManaged) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{ result: reconcile.Result{RequeueAfter: defaultPollInterval}, }, }, "ReconciliationPausedSuccessful": { reason: `If a managed resource has the pause annotation with value "true", there should be no further requeue requests.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "true"}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "true"}) want.SetConditions(xpv2.ReconcilePaused().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has the pause annotation with value "true", it should acquire "Synced" status condition with the status "False" and the reason "ReconcilePaused".` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), }, want: want{result: reconcile.Result{}}, }, "ManagementPolicyReconciliationPausedSuccessful": { reason: `If a managed resource has the pause annotation with value "true", there should be no further requeue requests.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{}) want.SetConditions(xpv2.ReconcilePaused().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has the pause annotation with value "true", it should acquire "Synced" status condition with the status "False" and the reason "ReconcilePaused".` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithInitializers(), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{}}, }, "ReconciliationResumes": { reason: `If a managed resource has the pause annotation with some value other than "true" and the Synced=False/ReconcilePaused status condition, reconciliation should resume with requeueing.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "false"}) mg.SetConditions(xpv2.ReconcilePaused()) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "false"}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `Managed resource should acquire Synced=False/ReconcileSuccess status condition after a resume.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ReconciliationPausedError": { reason: `If a managed resource has the pause annotation with value "true" and the status update due to reconciliation being paused fails, error should be reported causing an exponentially backed-off requeue.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "true"}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return errBoom }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), }, want: want{err: errors.Wrap(errBoom, errUpdateManagedStatus)}, }, "ManagementPoliciesUsedButNotEnabled": { reason: `If management policies tried to be used without enabling the feature, we should throw an error.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionCreate}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionCreate}) want.SetConditions(xpv2.ReconcileError(fmt.Errorf(errFmtManagementPolicyNonDefault, xpv2.ManagementPolicies{xpv2.ManagementActionCreate})).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has a non default management policy but feature not enabled, it should return a proper error.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), }, want: want{result: reconcile.Result{}}, }, "ManagementPolicyNotSupported": { reason: `If an unsupported management policy is used, we should throw an error.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionCreate}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionCreate}) want.SetConditions(xpv2.ReconcileError(fmt.Errorf(errFmtManagementPolicyNotSupported, xpv2.ManagementPolicies{xpv2.ManagementActionCreate})).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has non supported management policy, it should return a proper error.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), }, }, want: want{result: reconcile.Result{}}, }, "CustomManagementPolicyNotSupported": { reason: `If a custom unsupported management policy is used, we should throw an error.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) want.SetConditions(xpv2.ReconcileError(fmt.Errorf(errFmtManagementPolicyNotSupported, xpv2.ManagementPolicies{xpv2.ManagementActionAll})).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has non supported management policy, it should return a proper error.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithReconcilerSupportedManagementPolicies([]sets.Set[xpv2.ManagementAction]{sets.New(xpv2.ManagementActionObserve)}), }, }, want: want{result: reconcile.Result{}}, }, "ObserveOnlyResourceDoesNotExist": { reason: "With only Observe management action, observing a resource that does not exist should be reported as a conditioned status error.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errors.New(errExternalResourceNotExist), errReconcileObserve)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Resource does not exist should be reported as a conditioned status when ObserveOnly." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ObserveOnlyPublishConnectionDetailsError": { reason: "With Observe, errors publishing connection details after observation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors publishing connection details after observation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), withConnectionPublishers(ConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) (bool, error) { return false, errBoom }, }), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ObserveOnlySuccessfulObserve": { reason: "With Observe, a successful managed resource observe should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "With ObserveOnly, a successful managed resource observation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), withConnectionPublishers(ConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) (bool, error) { return false, nil }, }), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ManagementPolicyAllCreateSuccessful": { reason: "Successful managed resource creation using management policy all should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Successful managed resource creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ManagementPolicyCreateCreateSuccessful": { reason: "Successful managed resource creation using management policy Create should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Successful managed resource creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ManagementPolicyImmutable": { reason: "Successful reconciliation skipping update should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionLateInitialize, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete}) return nil }), MockUpdate: test.NewMockUpdateFn(errBoom), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionLateInitialize, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `Managed resource should acquire Synced=False/ReconcileSuccess status condition.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ManagementPolicyAllUpdateSuccessful": { reason: "A successful managed resource update using management policies should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful managed resource update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ManagementPolicyUpdateUpdateSuccessful": { reason: "A successful managed resource update using management policies should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful managed resource update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ManagementPolicySkipLateInitialize": { reason: "Should skip updating a managed resource to persist late initialized fields and should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionUpdate, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete}) return nil }), MockUpdate: test.NewMockUpdateFn(errBoom), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newLegacyManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionUpdate, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors updating a managed resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true, ResourceLateInitialized: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ObserveAndLateInitializePolicy": { reason: "If management policy is set to Observe and LateInitialize, reconciliation should proceed", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionLateInitialize}) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithReconcilerSupportedManagementPolicies(defaultSupportedManagementPolicies()), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ObserveUpdateAndLateInitializePolicy": { reason: "If management policy is set to Observe, Update and LateInitialize, reconciliation should proceed", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asLegacyManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{ xpv2.ManagementActionObserve, xpv2.ManagementActionUpdate, xpv2.ManagementActionLateInitialize, }) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithReconcilerSupportedManagementPolicies(defaultSupportedManagementPolicies()), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewReconciler(tc.args.m, tc.args.mg, tc.args.o...) got, err := r.Reconcile(context.Background(), reconcile.Request{}) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.result, got, tc.want.resultCmpOpts...); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestTestLegacyManagementPoliciesResolverIsPaused(t *testing.T) { type args struct { enabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "Disabled": { reason: "Should return false if management policies are disabled", args: args{ enabled: false, policy: xpv2.ManagementPolicies{}, }, want: false, }, "EnabledEmptyPolicies": { reason: "Should return true if the management policies are enabled and empty", args: args{ enabled: true, policy: xpv2.ManagementPolicies{}, }, want: true, }, "EnabledNonEmptyPolicies": { reason: "Should return true if the management policies are enabled and non empty", args: args{ enabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewLegacyManagementPoliciesResolver(tc.args.enabled, tc.args.policy, xpv2.DeletionDelete) if diff := cmp.Diff(tc.want, r.IsPaused()); diff != "" { t.Errorf("\nReason: %s\nIsPaused(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestLegacyManagementPoliciesResolverValidate(t *testing.T) { type args struct { enabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want error }{ "Enabled": { reason: "Should return nil if the management policy is enabled.", args: args{ enabled: true, policy: xpv2.ManagementPolicies{}, }, want: nil, }, "DisabledNonDefault": { reason: "Should return error if the management policy is non-default and disabled.", args: args{ enabled: false, policy: xpv2.ManagementPolicies{xpv2.ManagementActionCreate}, }, want: fmt.Errorf(errFmtManagementPolicyNonDefault, []xpv2.ManagementAction{xpv2.ManagementActionCreate}), }, "DisabledDefault": { reason: "Should return nil if the management policy is default and disabled.", args: args{ enabled: false, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: nil, }, "EnabledSupported": { reason: "Should return nil if the management policy is supported.", args: args{ enabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: nil, }, "EnabledNotSupported": { reason: "Should return err if the management policy is not supported.", args: args{ enabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionDelete}, }, want: fmt.Errorf(errFmtManagementPolicyNotSupported, []xpv2.ManagementAction{xpv2.ManagementActionDelete}), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewLegacyManagementPoliciesResolver(tc.args.enabled, tc.args.policy, xpv2.DeletionDelete) if diff := cmp.Diff(tc.want, r.Validate(), test.EquateErrors()); diff != "" { t.Errorf("\nReason: %s\nIsNonDefault(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestLegacyManagementPoliciesResolverShouldCreate(t *testing.T) { type args struct { managementPoliciesEnabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "ManagementPoliciesDisabled": { reason: "Should return true if management policies are disabled", args: args{ managementPoliciesEnabled: false, }, want: true, }, "ManagementPoliciesEnabledHasCreate": { reason: "Should return true if management policies are enabled and managementPolicies has action Create", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionCreate}, }, want: true, }, "ManagementPoliciesEnabledHasCreateAll": { reason: "Should return true if management policies are enabled and managementPolicies has action All", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: true, }, "ManagementPoliciesEnabledActionNotAllowed": { reason: "Should return false if management policies are enabled and managementPolicies does not have Create", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewLegacyManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy, xpv2.DeletionOrphan) if diff := cmp.Diff(tc.want, r.ShouldCreate()); diff != "" { t.Errorf("\nReason: %s\nShouldCreate(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestLegacyManagementPoliciesResolverShouldUpdate(t *testing.T) { type args struct { managementPoliciesEnabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "ManagementPoliciesDisabled": { reason: "Should return true if management policies are disabled", args: args{ managementPoliciesEnabled: false, }, want: true, }, "ManagementPoliciesEnabledHasUpdate": { reason: "Should return true if management policies are enabled and managementPolicies has action Update", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionUpdate}, }, want: true, }, "ManagementPoliciesEnabledHasUpdateAll": { reason: "Should return true if management policies are enabled and managementPolicies has action All", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: true, }, "ManagementPoliciesEnabledActionNotAllowed": { reason: "Should return false if management policies are enabled and managementPolicies does not have Update", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewLegacyManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy, xpv2.DeletionOrphan) if diff := cmp.Diff(tc.want, r.ShouldUpdate()); diff != "" { t.Errorf("\nReason: %s\nShouldUpdate(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestLegacyManagementPoliciesResolverShouldLateInitialize(t *testing.T) { type args struct { managementPoliciesEnabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "ManagementPoliciesDisabled": { reason: "Should return true if management policies are disabled", args: args{ managementPoliciesEnabled: false, }, want: true, }, "ManagementPoliciesEnabledHasLateInitialize": { reason: "Should return true if management policies are enabled and managementPolicies has action LateInitialize", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionLateInitialize}, }, want: true, }, "ManagementPoliciesEnabledHasLateInitializeAll": { reason: "Should return true if management policies are enabled and managementPolicies has action All", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: true, }, "ManagementPoliciesEnabledActionNotAllowed": { reason: "Should return false if management policies are enabled and managementPolicies does not have LateInitialize", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewLegacyManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy, xpv2.DeletionOrphan) if diff := cmp.Diff(tc.want, r.ShouldLateInitialize()); diff != "" { t.Errorf("\nReason: %s\nShouldLateInitialize(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestLegacyManagementPoliciesResolverOnlyObserve(t *testing.T) { type args struct { managementPoliciesEnabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "ManagementPoliciesDisabled": { reason: "Should return false if management policies are disabled", args: args{ managementPoliciesEnabled: false, }, want: false, }, "ManagementPoliciesEnabledHasOnlyObserve": { reason: "Should return true if management policies are enabled and managementPolicies has action LateInitialize", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, want: true, }, "ManagementPoliciesEnabledHasMultipleActions": { reason: "Should return false if management policies are enabled and managementPolicies has multiple actions", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionLateInitialize, xpv2.ManagementActionObserve}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewLegacyManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy, xpv2.DeletionOrphan) if diff := cmp.Diff(tc.want, r.ShouldOnlyObserve()); diff != "" { t.Errorf("\nReason: %s\nShouldOnlyObserve(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestLegacyShouldDelete(t *testing.T) { type args struct { managementPoliciesEnabled bool managed resource.LegacyManaged } type want struct { delete bool } cases := map[string]struct { reason string args args want want }{ "DeletionOrphan": { reason: "Should orphan if management policies are disabled and deletion policy is set to Orphan.", args: args{ managementPoliciesEnabled: false, managed: &fake.LegacyManaged{ Orphanable: fake.Orphanable{ Policy: xpv2.DeletionOrphan, }, }, }, want: want{delete: false}, }, "DeletionDelete": { reason: "Should delete if management policies are disabled and deletion policy is set to Delete.", args: args{ managementPoliciesEnabled: false, managed: &fake.LegacyManaged{ Orphanable: fake.Orphanable{ Policy: xpv2.DeletionDelete, }, }, }, want: want{delete: true}, }, "DeletionDeleteManagementActionAll": { reason: "Should delete if management policies are enabled and deletion policy is set to Delete and management policy is set to All.", args: args{ managementPoliciesEnabled: true, managed: &fake.LegacyManaged{ Orphanable: fake.Orphanable{ Policy: xpv2.DeletionDelete, }, Manageable: fake.Manageable{ Policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, }, }, want: want{delete: true}, }, "DeletionOrphanManagementActionAll": { reason: "Should orphan if management policies are enabled and deletion policy is set to Orphan and management policy is set to All.", args: args{ managementPoliciesEnabled: true, managed: &fake.LegacyManaged{ Orphanable: fake.Orphanable{ Policy: xpv2.DeletionOrphan, }, Manageable: fake.Manageable{ Policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, }, }, want: want{delete: false}, }, "DeletionDeleteManagementActionDelete": { reason: "Should delete if management policies are enabled and deletion policy is set to Delete and management policy has action Delete.", args: args{ managementPoliciesEnabled: true, managed: &fake.LegacyManaged{ Orphanable: fake.Orphanable{ Policy: xpv2.DeletionDelete, }, Manageable: fake.Manageable{ Policy: xpv2.ManagementPolicies{xpv2.ManagementActionDelete}, }, }, }, want: want{delete: true}, }, "DeletionOrphanManagementActionDelete": { reason: "Should delete if management policies are enabled and deletion policy is set to Orphan and management policy has action Delete.", args: args{ managementPoliciesEnabled: true, managed: &fake.LegacyManaged{ Orphanable: fake.Orphanable{ Policy: xpv2.DeletionOrphan, }, Manageable: fake.Manageable{ Policy: xpv2.ManagementPolicies{xpv2.ManagementActionDelete}, }, }, }, want: want{delete: true}, }, "DeletionDeleteManagementActionNoDelete": { reason: "Should orphan if management policies are enabled and deletion policy is set to Delete and management policy does not have action Delete.", args: args{ managementPoliciesEnabled: true, managed: &fake.LegacyManaged{ Orphanable: fake.Orphanable{ Policy: xpv2.DeletionDelete, }, Manageable: fake.Manageable{ Policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, }, }, want: want{delete: false}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewLegacyManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.managed.GetManagementPolicies(), tc.args.managed.GetDeletionPolicy()) if diff := cmp.Diff(tc.want.delete, r.ShouldDelete()); diff != "" { t.Errorf("\nReason: %s\nShouldDelete(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestLegacyReconcilerChangeLogs(t *testing.T) { type args struct { m manager.Manager mg resource.ManagedKind o []ReconcilerOption c *changeLogServiceClient } type want struct { callCount int opType v1alpha1.OperationType errMessage string } now := metav1.Now() errBoom := errors.New("boom") cases := map[string]struct { reason string args args want want }{ "CreateSuccessfulWithChangeLogs": { reason: "Successful managed resource creation should send a create change log entry when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource doesn't exist, which should trigger a create operation return ExternalObservation{ResourceExists: false, ResourceUpToDate: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { return ExternalCreation{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_CREATE, errMessage: "", }, }, "CreateFailureWithChangeLogs": { reason: "Failed managed resource creation should send a create change log entry with the error when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource doesn't exist, which should trigger a create operation return ExternalObservation{ResourceExists: false, ResourceUpToDate: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { // return an error from Create to simulate a failed creation return ExternalCreation{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_CREATE, errMessage: errBoom.Error(), }, }, "UpdateSuccessfulWithChangeLogs": { reason: "Successful managed resource update should send an update change log entry when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource exists but isn't up to date, which should trigger an update operation return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_UPDATE, errMessage: "", }, }, "UpdateFailureWithChangeLogs": { reason: "Failed managed resource update should send an update change log entry with the error when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: legacyManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource exists but isn't up to date, which should trigger an update operation return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { // return an error from Update to simulate a failed update return ExternalUpdate{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_UPDATE, errMessage: errBoom.Error(), }, }, "DeleteSuccessfulWithChangeLogs": { reason: "Successful managed resource delete should send a delete change log entry when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { // set a deletion timestamp, which should trigger a delete operation mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource exists but we set a deletion timestamp above, which should trigger a delete operation return ExternalObservation{ResourceExists: true}, nil }, DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { return ExternalDelete{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_DELETE, errMessage: "", }, }, "DeleteFailureWithChangeLogs": { reason: "Failed managed resource delete should send a delete change log entry with the error when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { // set a deletion timestamp, which should trigger a delete operation mg := asLegacyManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.LegacyManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource exists but we set a deletion timestamp above, which should trigger a delete operation return ExternalObservation{ResourceExists: true}, nil }, DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { // return an error from Delete to simulate a failed delete return ExternalDelete{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_DELETE, errMessage: errBoom.Error(), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.args.o = append(tc.args.o, WithChangeLogger(NewGRPCChangeLogger(tc.args.c, WithProviderVersion("provider-cool:v9.99.999")))) r := NewReconciler(tc.args.m, tc.args.mg, tc.args.o...) r.Reconcile(context.Background(), reconcile.Request{}) if diff := cmp.Diff(tc.want.callCount, len(tc.args.c.requests)); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want callCount, +got callCount:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.opType, tc.args.c.requests[0].GetEntry().GetOperation()); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want opType, +got opType:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.errMessage, tc.args.c.requests[0].GetEntry().GetErrorMessage()); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want errMessage, +got errMessage:\n%s", tc.reason, diff) } }) } } func asLegacyManaged(obj client.Object, generation int64) *fake.LegacyManaged { mg := obj.(*fake.LegacyManaged) mg.Generation = generation return mg } func newLegacyManaged(generation int64) *fake.LegacyManaged { mg := &fake.LegacyManaged{} mg.Generation = generation return mg } func legacyManagedMockGetFn(err error, generation int64) test.MockGetFn { return test.NewMockGetFn(err, func(obj client.Object) error { asLegacyManaged(obj, generation) return nil }) } ================================================ FILE: pkg/reconciler/managed/reconciler_modern_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package managed import ( "context" "fmt" "testing" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/crossplane/crossplane-runtime/v2/apis/changelogs/proto/v1alpha1" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var _ reconcile.Reconciler = &Reconciler{} func TestModernReconciler(t *testing.T) { type args struct { m manager.Manager mg resource.ManagedKind o []ReconcilerOption } type want struct { result reconcile.Result resultCmpOpts []cmp.Option err error } errBoom := errors.New("boom") now := metav1.Now() cases := map[string]struct { reason string args args want want }{ "GetManagedError": { reason: "Any error (except not found) encountered while getting the resource under reconciliation should be returned.", args: args{ m: &fake.Manager{ Client: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)}, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), }, want: want{err: errors.Wrap(errBoom, errGetManaged)}, }, "ManagedNotFound": { reason: "Not found errors encountered while getting the resource under reconciliation should be ignored.", args: args{ m: &fake.Manager{ Client: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))}, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), }, want: want{result: reconcile.Result{}}, }, "UnpublishConnectionDetailsDeletionPolicyOrphan": { reason: "Errors unpublishing connection details should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionUpdate, xpv2.ManagementActionLateInitialize}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetDeletionTimestamp(&now) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionUpdate, xpv2.ManagementActionLateInitialize}) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors unpublishing connection details should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), withLocalConnectionPublishers(LocalConnectionPublisherFns{ UnpublishConnectionFn: func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ ConnectionDetails) error { return errBoom }, }), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "RemoveFinalizerErrorDeletionPolicyOrphan": { reason: "Errors removing the managed resource finalizer should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionUpdate, xpv2.ManagementActionLateInitialize}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetDeletionTimestamp(&now) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionCreate, xpv2.ManagementActionUpdate, xpv2.ManagementActionLateInitialize}) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors removing the managed resource finalizer should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return errBoom }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "DeleteSuccessfulDeletionPolicyOrphan": { reason: "Successful managed resource deletion with no-delete management policy should not trigger a requeue or status update.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: false}}, }, "InitializeError": { reason: "Errors initializing the managed resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors initializing the managed resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(InitializerFn(func(_ context.Context, _ resource.Managed) error { return errBoom })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExtraFinalizersDelayDelete": { reason: "The existence of multiple finalizers should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) mg.SetFinalizers([]string{FinalizerName, "finalizer2"}) return nil }), MockUpdate: test.NewMockUpdateFn(nil), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalCreatePending": { reason: "We should return early if the managed resource appears to be pending creation. We might have leaked a resource and don't want to create another.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { asModernManaged(obj, 42) meta.SetExternalCreatePending(obj, now.Time) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) meta.SetExternalCreatePending(want, now.Time) want.SetConditions( xpv2.Creating().WithObservedGeneration(42), xpv2.ReconcileError(errors.New(errCreateIncomplete)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "We should update our status when we're asked to reconcile a managed resource that is pending creation." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(InitializerFn(func(_ context.Context, _ resource.Managed) error { return nil })), }, }, want: want{result: reconcile.Result{Requeue: false}}, }, "ResolveReferencesError": { reason: "Errors during reference resolution references should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors during reference resolution should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return errBoom })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalConnectError": { reason: "Errors connecting to the provider should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, got client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileConnect)).WithObservedGeneration(42)) if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" { reason := "Errors connecting to the provider should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { return nil, errBoom })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalDisconnectError": { reason: "Error disconnecting from the provider should not trigger requeue.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful no-op reconcile should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return errBoom }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ExternalObserveError": { reason: "Errors observing the external resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileObserve)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors observing the managed resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "CreationGracePeriod": { reason: "If our resource appears not to exist during the creation grace period we should return early.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalCreateSucceeded(obj, time.Now()) return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithCreationGracePeriod(1 * time.Minute), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalDeleteError": { reason: "Errors deleting the external resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetDeletionTimestamp(&now) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileDelete)).WithObservedGeneration(42)) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "An error deleting an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { return ExternalDelete{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalDeleteSuccessful": { reason: "A deleted managed resource with the 'delete' reclaim policy should delete its external resource then requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetDeletionTimestamp(&now) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A deleted external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { return ExternalDelete{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "UnpublishConnectionDetailsDeletionPolicyDeleteError": { reason: "Errors unpublishing connection details should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetDeletionTimestamp(&now) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors unpublishing connection details should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), withLocalConnectionPublishers(LocalConnectionPublisherFns{ UnpublishConnectionFn: func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ ConnectionDetails) error { return errBoom }, }), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "RemoveFinalizerErrorDeletionPolicyDelete": { reason: "Errors removing the managed resource finalizer should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetDeletionTimestamp(&now) want.SetConditions(xpv2.Deleting().WithObservedGeneration(42)) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors removing the managed resource finalizer should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return errBoom }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "DeleteSuccessfulDeletionPolicyDelete": { reason: "Successful managed resource deletion with deletion policy Delete should not trigger a requeue or status update.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: false}}, }, "PublishObservationConnectionDetailsError": { reason: "Errors publishing connection details after observation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors publishing connection details after observation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), withLocalConnectionPublishers(LocalConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ ConnectionDetails) (bool, error) { return false, errBoom }, }), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "AddFinalizerError": { reason: "Errors adding a finalizer should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors adding a finalizer should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return errBoom }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "UpdateCreatePendingError": { reason: "Errors while updating our external-create-pending annotation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(errBoom), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) meta.SetExternalCreatePending(want, time.Now()) want.SetConditions( xpv2.Creating().WithObservedGeneration(42), xpv2.ReconcileError(errors.Wrap(errBoom, errUpdateManaged)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Errors while creating an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { return ExternalCreation{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "CreateExternalError": { reason: "Errors while creating an external resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateFailed(want, time.Now()) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileCreate)).WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Errors while creating an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { return ExternalCreation{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), // We simulate our critical annotation update failing too here. // This is mostly just to exercise the code, which just creates a log and an event. WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return errBoom })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "UpdateCriticalAnnotationsError": { reason: "Errors updating critical annotations after creation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errUpdateManagedAnnotations)).WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Errors updating critical annotations after creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { return ExternalCreation{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return errBoom })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "PublishCreationConnectionDetailsError": { reason: "Errors publishing connection details after creation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Errors publishing connection details after creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { cd := ConnectionDetails{"create": []byte{}} return ExternalCreation{ConnectionDetails: cd}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), withLocalConnectionPublishers(LocalConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.LocalConnectionSecretOwner, cd ConnectionDetails) (bool, error) { // We're called after observe, create, and update // but we only want to fail when publishing details // after a creation. if _, ok := cd["create"]; ok { return false, errBoom } return true, nil }, }), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "CreateSuccessful": { reason: "Successful managed resource creation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Successful managed resource creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "CreateSuccessfulAfterExternalCreatePendingAndDeterministicName": { reason: "Successful managed resource creation which was previously pending and has a deterministic external name should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { asModernManaged(obj, 42) meta.SetExternalCreatePending(obj, now.Time) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Successful managed resource creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithDeterministicExternalName(true), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "LateInitializeUpdateError": { reason: "Errors updating a managed resource to persist late initialized fields should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(errBoom), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errUpdateManaged)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors updating a managed resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true, ResourceLateInitialized: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ExternalResourceUpToDate": { reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful no-op reconcile should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ExternalResourceUpToDateWithJitter": { reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait with jitter added.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithPollJitterHook(time.Second), }, }, want: want{ result: reconcile.Result{RequeueAfter: defaultPollInterval}, resultCmpOpts: []cmp.Option{cmp.Comparer(func(l, r time.Duration) bool { diff := l - r if diff < 0 { diff = -diff } return diff < time.Second })}, }, }, "ExternalResourceUpToDateWithPollIntervalHook": { reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait processed by the interval hook.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithPollIntervalHook(func(_ resource.Managed, pollInterval time.Duration) time.Duration { return 2 * pollInterval }), }, }, want: want{ result: reconcile.Result{RequeueAfter: 2 * defaultPollInterval}, }, }, "ExternalResourceUpToDateWithMultiplePollIntervalHooks": { reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait processed by the latest interval hook.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), WithPollJitterHook(time.Second), WithPollIntervalHook(func(_ resource.Managed, pollInterval time.Duration) time.Duration { return 2 * pollInterval }), WithPollIntervalHook(func(_ resource.Managed, pollInterval time.Duration) time.Duration { return 3 * pollInterval }), }, }, want: want{ result: reconcile.Result{RequeueAfter: 3 * defaultPollInterval}, }, }, "UpdateExternalError": { reason: "Errors while updating an external resource should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errBoom, errReconcileUpdate)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors while updating an external resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "PublishUpdateConnectionDetailsError": { reason: "Errors publishing connection details after an update should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors publishing connection details after an update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { cd := ConnectionDetails{"update": []byte{}} return ExternalUpdate{ConnectionDetails: cd}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), withLocalConnectionPublishers(LocalConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.LocalConnectionSecretOwner, cd ConnectionDetails) (bool, error) { // We're called after observe, create, and update // but we only want to fail when publishing details // after an update. if _, ok := cd["update"]; ok { return false, errBoom } return false, nil }, }), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "UpdateSuccessful": { reason: "A successful managed resource update should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful managed resource update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "TypedReconcilerUpdateSuccessful": { reason: "A successful managed resource update should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful managed resource update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithTypedExternalConnector(TypedExternalConnectorFn[*fake.ModernManaged](func(_ context.Context, _ *fake.ModernManaged) (TypedExternalClient[*fake.ModernManaged], error) { c := &TypedExternalClientFns[*fake.ModernManaged]{ ObserveFn: func(_ context.Context, _ *fake.ModernManaged) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ *fake.ModernManaged) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{ result: reconcile.Result{RequeueAfter: defaultPollInterval}, }, }, "ReconciliationPausedSuccessful": { reason: `If a managed resource has the pause annotation with value "true", there should be no further requeue requests.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "true"}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "true"}) want.SetConditions(xpv2.ReconcilePaused().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has the pause annotation with value "true", it should acquire "Synced" status condition with the status "False" and the reason "ReconcilePaused".` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), }, want: want{result: reconcile.Result{}}, }, "ManagementPolicyReconciliationPausedSuccessful": { reason: `If a managed resource has the pause annotation with value "true", there should be no further requeue requests.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{}) want.SetConditions(xpv2.ReconcilePaused().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has the pause annotation with value "true", it should acquire "Synced" status condition with the status "False" and the reason "ReconcilePaused".` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithInitializers(), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{}}, }, "ReconciliationResumes": { reason: `If a managed resource has the pause annotation with some value other than "true" and the Synced=False/ReconcilePaused status condition, reconciliation should resume with requeueing.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "false"}) mg.SetConditions(xpv2.ReconcilePaused()) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "false"}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `Managed resource should acquire Synced=False/ReconcileSuccess status condition after a resume.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ReconciliationPausedError": { reason: `If a managed resource has the pause annotation with value "true" and the status update due to reconciliation being paused fails, error should be reported causing an exponentially backed-off requeue.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetAnnotations(map[string]string{meta.AnnotationKeyReconciliationPaused: "true"}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return errBoom }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), }, want: want{err: errors.Wrap(errBoom, errUpdateManagedStatus)}, }, "ManagementPoliciesUsedButNotEnabled": { reason: `If management policies tried to be used without enabling the feature, we should throw an error.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionCreate}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionCreate}) want.SetConditions(xpv2.ReconcileError(fmt.Errorf(errFmtManagementPolicyNonDefault, xpv2.ManagementPolicies{xpv2.ManagementActionCreate})).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has a non default management policy but feature not enabled, it should return a proper error.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), }, want: want{result: reconcile.Result{}}, }, "ManagementPolicyNotSupported": { reason: `If an unsupported management policy is used, we should throw an error.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionCreate}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionCreate}) want.SetConditions(xpv2.ReconcileError(fmt.Errorf(errFmtManagementPolicyNotSupported, xpv2.ManagementPolicies{xpv2.ManagementActionCreate})).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has non supported management policy, it should return a proper error.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), }, }, want: want{result: reconcile.Result{}}, }, "CustomManagementPolicyNotSupported": { reason: `If a custom unsupported management policy is used, we should throw an error.`, args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) want.SetConditions(xpv2.ReconcileError(fmt.Errorf(errFmtManagementPolicyNotSupported, xpv2.ManagementPolicies{xpv2.ManagementActionAll})).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `If managed resource has non supported management policy, it should return a proper error.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithReconcilerSupportedManagementPolicies([]sets.Set[xpv2.ManagementAction]{sets.New(xpv2.ManagementActionObserve)}), }, }, want: want{result: reconcile.Result{}}, }, "ObserveOnlyResourceDoesNotExist": { reason: "With only Observe management action, observing a resource that does not exist should be reported as a conditioned status error.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) want.SetConditions(xpv2.ReconcileError(errors.Wrap(errors.New(errExternalResourceNotExist), errReconcileObserve)).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Resource does not exist should be reported as a conditioned status when ObserveOnly." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: false}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ObserveOnlyPublishConnectionDetailsError": { reason: "With Observe, errors publishing connection details after observation should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) want.SetConditions(xpv2.ReconcileError(errBoom).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors publishing connection details after observation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), withLocalConnectionPublishers(LocalConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ ConnectionDetails) (bool, error) { return false, errBoom }, }), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ObserveOnlySuccessfulObserve": { reason: "With Observe, a successful managed resource observe should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "With ObserveOnly, a successful managed resource observation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), withLocalConnectionPublishers(LocalConnectionPublisherFns{ PublishConnectionFn: func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ ConnectionDetails) (bool, error) { return false, nil }, }), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ManagementPolicyAllCreateSuccessful": { reason: "Successful managed resource creation using management policy all should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Successful managed resource creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ManagementPolicyCreateCreateSuccessful": { reason: "Successful managed resource creation using management policy Create should trigger a requeue after a short wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) meta.SetExternalCreatePending(want, time.Now()) meta.SetExternalCreateSucceeded(want, time.Now()) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) want.SetConditions(xpv2.Creating().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" { reason := "Successful managed resource creation should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(&NopConnector{}), WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(_ context.Context, _ client.Object) error { return nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{Requeue: true}}, }, "ManagementPolicyImmutable": { reason: "Successful reconciliation skipping update should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionLateInitialize, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete}) return nil }), MockUpdate: test.NewMockUpdateFn(errBoom), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionLateInitialize, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := `Managed resource should acquire Synced=False/ReconcileSuccess status condition.` t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ManagementPolicyAllUpdateSuccessful": { reason: "A successful managed resource update using management policies should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42).WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful managed resource update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ManagementPolicyUpdateUpdateSuccessful": { reason: "A successful managed resource update using management policies should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionAll}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "A successful managed resource update should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ManagementPolicySkipLateInitialize": { reason: "Should skip updating a managed resource to persist late initialized fields and should trigger a requeue after a long wait.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionUpdate, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete}) return nil }), MockUpdate: test.NewMockUpdateFn(errBoom), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { want := newModernManaged(42) want.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionUpdate, xpv2.ManagementActionCreate, xpv2.ManagementActionDelete}) want.SetConditions(xpv2.ReconcileSuccess().WithObservedGeneration(42)) if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" { reason := "Errors updating a managed resource should be reported as a conditioned status." t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithInitializers(), WithManagementPolicies(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true, ResourceLateInitialized: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ObserveAndLateInitializePolicy": { reason: "If management policy is set to Observe and LateInitialize, reconciliation should proceed", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{xpv2.ManagementActionObserve, xpv2.ManagementActionLateInitialize}) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithReconcilerSupportedManagementPolicies(defaultSupportedManagementPolicies()), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, "ObserveUpdateAndLateInitializePolicy": { reason: "If management policy is set to Observe, Update and LateInitialize, reconciliation should proceed", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetManagementPolicies(xpv2.ManagementPolicies{ xpv2.ManagementActionObserve, xpv2.ManagementActionUpdate, xpv2.ManagementActionLateInitialize, }) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithManagementPolicies(), WithReconcilerSupportedManagementPolicies(defaultSupportedManagementPolicies()), }, }, want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewReconciler(tc.args.m, tc.args.mg, tc.args.o...) got, err := r.Reconcile(context.Background(), reconcile.Request{}) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.result, got, tc.want.resultCmpOpts...); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestManagementPoliciesResolverIsPaused(t *testing.T) { type args struct { enabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "Disabled": { reason: "Should return false if management policies are disabled", args: args{ enabled: false, policy: xpv2.ManagementPolicies{}, }, want: false, }, "EnabledEmptyPolicies": { reason: "Should return true if the management policies are enabled and empty", args: args{ enabled: true, policy: xpv2.ManagementPolicies{}, }, want: true, }, "EnabledNonEmptyPolicies": { reason: "Should return true if the management policies are enabled and non empty", args: args{ enabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewManagementPoliciesResolver(tc.args.enabled, tc.args.policy) if diff := cmp.Diff(tc.want, r.IsPaused()); diff != "" { t.Errorf("\nReason: %s\nIsPaused(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestManagementPoliciesResolverValidate(t *testing.T) { type args struct { enabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want error }{ "Enabled": { reason: "Should return nil if the management policy is enabled.", args: args{ enabled: true, policy: xpv2.ManagementPolicies{}, }, want: nil, }, "DisabledNonDefault": { reason: "Should return error if the management policy is non-default and disabled.", args: args{ enabled: false, policy: xpv2.ManagementPolicies{xpv2.ManagementActionCreate}, }, want: fmt.Errorf(errFmtManagementPolicyNonDefault, []xpv2.ManagementAction{xpv2.ManagementActionCreate}), }, "DisabledDefault": { reason: "Should return nil if the management policy is default and disabled.", args: args{ enabled: false, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: nil, }, "EnabledSupported": { reason: "Should return nil if the management policy is supported.", args: args{ enabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: nil, }, "EnabledNotSupported": { reason: "Should return err if the management policy is not supported.", args: args{ enabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionDelete}, }, want: fmt.Errorf(errFmtManagementPolicyNotSupported, []xpv2.ManagementAction{xpv2.ManagementActionDelete}), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewManagementPoliciesResolver(tc.args.enabled, tc.args.policy) if diff := cmp.Diff(tc.want, r.Validate(), test.EquateErrors()); diff != "" { t.Errorf("\nReason: %s\nIsNonDefault(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestManagementPoliciesResolverShouldCreate(t *testing.T) { type args struct { managementPoliciesEnabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "ManagementPoliciesDisabled": { reason: "Should return true if management policies are disabled", args: args{ managementPoliciesEnabled: false, }, want: true, }, "ManagementPoliciesEnabledHasCreate": { reason: "Should return true if management policies are enabled and managementPolicies has action Create", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionCreate}, }, want: true, }, "ManagementPoliciesEnabledHasCreateAll": { reason: "Should return true if management policies are enabled and managementPolicies has action All", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: true, }, "ManagementPoliciesEnabledActionNotAllowed": { reason: "Should return false if management policies are enabled and managementPolicies does not have Create", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy) if diff := cmp.Diff(tc.want, r.ShouldCreate()); diff != "" { t.Errorf("\nReason: %s\nShouldCreate(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestManagementPoliciesResolverShouldUpdate(t *testing.T) { type args struct { managementPoliciesEnabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "ManagementPoliciesDisabled": { reason: "Should return true if management policies are disabled", args: args{ managementPoliciesEnabled: false, }, want: true, }, "ManagementPoliciesEnabledHasUpdate": { reason: "Should return true if management policies are enabled and managementPolicies has action Update", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionUpdate}, }, want: true, }, "ManagementPoliciesEnabledHasUpdateAll": { reason: "Should return true if management policies are enabled and managementPolicies has action All", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: true, }, "ManagementPoliciesEnabledActionNotAllowed": { reason: "Should return false if management policies are enabled and managementPolicies does not have Update", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy) if diff := cmp.Diff(tc.want, r.ShouldUpdate()); diff != "" { t.Errorf("\nReason: %s\nShouldUpdate(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestManagementPoliciesResolverShouldLateInitialize(t *testing.T) { type args struct { managementPoliciesEnabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "ManagementPoliciesDisabled": { reason: "Should return true if management policies are disabled", args: args{ managementPoliciesEnabled: false, }, want: true, }, "ManagementPoliciesEnabledHasLateInitialize": { reason: "Should return true if management policies are enabled and managementPolicies has action LateInitialize", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionLateInitialize}, }, want: true, }, "ManagementPoliciesEnabledHasLateInitializeAll": { reason: "Should return true if management policies are enabled and managementPolicies has action All", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, want: true, }, "ManagementPoliciesEnabledActionNotAllowed": { reason: "Should return false if management policies are enabled and managementPolicies does not have LateInitialize", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy) if diff := cmp.Diff(tc.want, r.ShouldLateInitialize()); diff != "" { t.Errorf("\nReason: %s\nShouldLateInitialize(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestManagementPoliciesResolverOnlyObserve(t *testing.T) { type args struct { managementPoliciesEnabled bool policy xpv2.ManagementPolicies } cases := map[string]struct { reason string args args want bool }{ "ManagementPoliciesDisabled": { reason: "Should return false if management policies are disabled", args: args{ managementPoliciesEnabled: false, }, want: false, }, "ManagementPoliciesEnabledHasOnlyObserve": { reason: "Should return true if management policies are enabled and managementPolicies has action LateInitialize", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, want: true, }, "ManagementPoliciesEnabledHasMultipleActions": { reason: "Should return false if management policies are enabled and managementPolicies has multiple actions", args: args{ managementPoliciesEnabled: true, policy: xpv2.ManagementPolicies{xpv2.ManagementActionLateInitialize, xpv2.ManagementActionObserve}, }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy) if diff := cmp.Diff(tc.want, r.ShouldOnlyObserve()); diff != "" { t.Errorf("\nReason: %s\nShouldOnlyObserve(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestShouldDelete(t *testing.T) { type args struct { managementPoliciesEnabled bool managed resource.Managed } type want struct { delete bool } cases := map[string]struct { reason string args args want want }{ "ManagementPoliciesDisabled": { reason: "Should delete if management policies are disabled", args: args{ managementPoliciesEnabled: false, managed: &fake.ModernManaged{}, }, want: want{delete: true}, }, "DeleteManagementActionAll": { reason: "Should delete if management policies are enabled and management policy is set to All.", args: args{ managementPoliciesEnabled: true, managed: &fake.ModernManaged{ Manageable: fake.Manageable{ Policy: xpv2.ManagementPolicies{xpv2.ManagementActionAll}, }, }, }, want: want{delete: true}, }, "ManagementActionDelete": { reason: "Should delete if management policies are enabled and management policy has action Delete.", args: args{ managementPoliciesEnabled: true, managed: &fake.ModernManaged{ Manageable: fake.Manageable{ Policy: xpv2.ManagementPolicies{xpv2.ManagementActionDelete}, }, }, }, want: want{delete: true}, }, "ManagementActionNoDelete": { reason: "Should orphan if management policies are enabled and management policy does not have action Delete.", args: args{ managementPoliciesEnabled: true, managed: &fake.ModernManaged{ Manageable: fake.Manageable{ Policy: xpv2.ManagementPolicies{xpv2.ManagementActionObserve}, }, }, }, want: want{delete: false}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.managed.GetManagementPolicies()) if diff := cmp.Diff(tc.want.delete, r.ShouldDelete()); diff != "" { t.Errorf("\nReason: %s\nShouldDelete(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestReconcilerChangeLogs(t *testing.T) { type args struct { m manager.Manager mg resource.ManagedKind o []ReconcilerOption c *changeLogServiceClient } type want struct { callCount int opType v1alpha1.OperationType errMessage string } now := metav1.Now() errBoom := errors.New("boom") cases := map[string]struct { reason string args args want want }{ "CreateSuccessfulWithChangeLogs": { reason: "Successful managed resource creation should send a create change log entry when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource doesn't exist, which should trigger a create operation return ExternalObservation{ResourceExists: false, ResourceUpToDate: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { return ExternalCreation{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_CREATE, errMessage: "", }, }, "CreateFailureWithChangeLogs": { reason: "Failed managed resource creation should send a create change log entry with the error when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource doesn't exist, which should trigger a create operation return ExternalObservation{ResourceExists: false, ResourceUpToDate: false}, nil }, CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { // return an error from Create to simulate a failed creation return ExternalCreation{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_CREATE, errMessage: errBoom.Error(), }, }, "UpdateSuccessfulWithChangeLogs": { reason: "Successful managed resource update should send an update change log entry when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource exists but isn't up to date, which should trigger an update operation return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { return ExternalUpdate{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_UPDATE, errMessage: "", }, }, "UpdateFailureWithChangeLogs": { reason: "Failed managed resource update should send an update change log entry with the error when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: modernManagedMockGetFn(nil, 42), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource exists but isn't up to date, which should trigger an update operation return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil }, UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { // return an error from Update to simulate a failed update return ExternalUpdate{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_UPDATE, errMessage: errBoom.Error(), }, }, "DeleteSuccessfulWithChangeLogs": { reason: "Successful managed resource delete should send a delete change log entry when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { // set a deletion timestamp, which should trigger a delete operation mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource exists but we set a deletion timestamp above, which should trigger a delete operation return ExternalObservation{ResourceExists: true}, nil }, DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { return ExternalDelete{}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_DELETE, errMessage: "", }, }, "DeleteFailureWithChangeLogs": { reason: "Failed managed resource delete should send a delete change log entry with the error when change logs are enabled.", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { // set a deletion timestamp, which should trigger a delete operation mg := asModernManaged(obj, 42) mg.SetDeletionTimestamp(&now) return nil }), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), o: []ReconcilerOption{ WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { c := &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { // resource exists but we set a deletion timestamp above, which should trigger a delete operation return ExternalObservation{ResourceExists: true}, nil }, DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { // return an error from Delete to simulate a failed delete return ExternalDelete{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil }, } return c, nil })), }, c: &changeLogServiceClient{}, }, want: want{ callCount: 1, opType: v1alpha1.OperationType_OPERATION_TYPE_DELETE, errMessage: errBoom.Error(), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.args.o = append(tc.args.o, WithChangeLogger(NewGRPCChangeLogger(tc.args.c, WithProviderVersion("provider-cool:v9.99.999")))) r := NewReconciler(tc.args.m, tc.args.mg, tc.args.o...) r.Reconcile(context.Background(), reconcile.Request{}) if diff := cmp.Diff(tc.want.callCount, len(tc.args.c.requests)); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want callCount, +got callCount:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.opType, tc.args.c.requests[0].GetEntry().GetOperation()); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want opType, +got opType:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.errMessage, tc.args.c.requests[0].GetEntry().GetErrorMessage()); diff != "" { t.Errorf("\nReason: %s\nr.Reconcile(...): -want errMessage, +got errMessage:\n%s", tc.reason, diff) } }) } } func TestEffectivePollInterval(t *testing.T) { cases := map[string]struct { reason string pollInterval time.Duration minPollInterval time.Duration annotation string want time.Duration }{ "NoAnnotationReturnsDefault": { reason: "When no annotation is set, the default poll interval is returned.", pollInterval: 10 * time.Minute, minPollInterval: 1 * time.Second, want: 10 * time.Minute, }, "ValidAnnotationAboveMinimumReturnsAnnotation": { reason: "When the annotation is at or above the minimum, it overrides the default.", pollInterval: 10 * time.Minute, minPollInterval: 1 * time.Second, annotation: "24h", want: 24 * time.Hour, }, "ValidAnnotationAtMinimumReturnsAnnotation": { reason: "When the annotation equals the minimum exactly, it is returned as-is.", pollInterval: 10 * time.Minute, minPollInterval: 30 * time.Second, annotation: "30s", want: 30 * time.Second, }, "ValidAnnotationBelowMinimumReturnsMinimum": { reason: "When the annotation is below the minimum, the minimum is returned.", pollInterval: 10 * time.Minute, minPollInterval: 30 * time.Second, annotation: "1s", want: 30 * time.Second, }, "InvalidAnnotationReturnsDefault": { reason: "When the annotation cannot be parsed, the default poll interval is returned.", pollInterval: 5 * time.Minute, minPollInterval: 1 * time.Second, annotation: "not-a-duration", want: 5 * time.Minute, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := &Reconciler{ pollInterval: tc.pollInterval, minPollInterval: tc.minPollInterval, } mg := &fake.ModernManaged{} if tc.annotation != "" { mg.SetAnnotations(map[string]string{ meta.AnnotationKeyPollInterval: tc.annotation, }) } got := r.effectivePollInterval(mg) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\n%s\neffectivePollInterval(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestReconcilePollIntervalAnnotation(t *testing.T) { cases := map[string]struct { reason string pollInterval time.Duration minPollInterval time.Duration annotation string wantApprox time.Duration wantTolerance time.Duration }{ "AnnotationOverridesPollInterval": { reason: "When a valid poll interval annotation is set, it should override the controller-level poll interval.", pollInterval: 1 * time.Minute, minPollInterval: 1 * time.Second, annotation: "24h", wantApprox: 24 * time.Hour, wantTolerance: 1 * time.Second, }, "InvalidAnnotationFallsBack": { reason: "When an invalid poll interval annotation is set, the controller-level poll interval should be used.", pollInterval: 5 * time.Minute, minPollInterval: 1 * time.Second, annotation: "not-a-duration", wantApprox: 5 * time.Minute, wantTolerance: 1 * time.Second, }, "AnnotationBelowMinimumClampsToMin": { reason: "When a poll interval annotation is below the configured minimum, the minimum should be used.", pollInterval: 10 * time.Minute, minPollInterval: 30 * time.Second, annotation: "1s", wantApprox: 30 * time.Second, wantTolerance: 1 * time.Second, }, "NoAnnotationUsesDefault": { reason: "When no poll interval annotation is set, the controller-level poll interval should be used.", pollInterval: 10 * time.Minute, minPollInterval: 1 * time.Second, wantApprox: 10 * time.Minute, wantTolerance: 1 * time.Second, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewReconciler(&fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) if tc.annotation != "" { mg.SetAnnotations(map[string]string{ meta.AnnotationKeyPollInterval: tc.annotation, }) } return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), WithPollInterval(tc.pollInterval), WithMinPollInterval(tc.minPollInterval), WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { return &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, }, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), ) got, err := r.Reconcile(context.Background(), reconcile.Request{}) if diff := cmp.Diff(nil, err, cmpopts.EquateErrors()); diff != "" { t.Fatalf("\n%s\nr.Reconcile(...): -want error, +got error:\n%s", tc.reason, diff) } diff := got.RequeueAfter - tc.wantApprox if diff < 0 { diff = -diff } if diff > tc.wantTolerance { t.Errorf("\n%s\nr.Reconcile(...): want RequeueAfter ~%v (±%v), got %v", tc.reason, tc.wantApprox, tc.wantTolerance, got.RequeueAfter) } }) } } func TestReconcileRequestAnnotation(t *testing.T) { type want struct { result reconcile.Result err error token string } cases := map[string]struct { reason string annotation string handled string want want }{ "NewReconcileRequestRecordsToken": { reason: "A new reconcile-requested-at token should be recorded in status.lastHandledReconcileAt.", annotation: "1705312200", want: want{ result: reconcile.Result{RequeueAfter: defaultPollInterval}, token: "1705312200", }, }, "AlreadyHandledReconcileRequestIsNoOp": { reason: "When the token matches lastHandledReconcileAt, status should remain unchanged.", annotation: "already-handled", handled: "already-handled", want: want{ result: reconcile.Result{RequeueAfter: defaultPollInterval}, token: "already-handled", }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewReconciler(&fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { mg := asModernManaged(obj, 42) mg.SetAnnotations(map[string]string{ meta.AnnotationKeyReconcileRequestedAt: tc.annotation, }) if tc.handled != "" { mg.SetLastHandledReconcileAt(tc.handled) } return nil }), MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { mg := obj.(*fake.ModernManaged) got := mg.GetLastHandledReconcileAt() if diff := cmp.Diff(tc.want.token, got); diff != "" { t.Errorf("\n%s\nstatus.lastHandledReconcileAt: -want, +got:\n%s", tc.reason, diff) } return nil }), }, Scheme: fake.SchemeWith(&fake.ModernManaged{}), }, resource.ManagedKind(fake.GVK(&fake.ModernManaged{})), WithInitializers(), WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })), WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { return &ExternalClientFns{ ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil }, DisconnectFn: func(_ context.Context) error { return nil }, }, nil })), WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}), ) got, err := r.Reconcile(context.Background(), reconcile.Request{}) if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { t.Errorf("\n%s\nr.Reconcile(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.result, got); diff != "" { t.Errorf("\n%s\nr.Reconcile(...): -want result, +got result:\n%s", tc.reason, diff) } }) } } func asModernManaged(obj client.Object, generation int64) *fake.ModernManaged { mg := obj.(*fake.ModernManaged) mg.Generation = generation return mg } func newModernManaged(generation int64) *fake.ModernManaged { mg := &fake.ModernManaged{} mg.Generation = generation return mg } func modernManagedMockGetFn(err error, generation int64) test.MockGetFn { return test.NewMockGetFn(err, func(obj client.Object) error { asModernManaged(obj, generation) return nil }) } ================================================ FILE: pkg/reconciler/managed/reconciler_typed.go ================================================ package managed import ( "context" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) const errFmtUnexpectedObjectType = "unexpected object type %T" // typedExternalConnectDisconnectorWrapper wraps a TypedExternalConnector to a // common ExternalConnector. type typedExternalConnectDisconnectorWrapper[managed resource.Managed] struct { c TypedExternalConnectDisconnector[managed] } func (c *typedExternalConnectDisconnectorWrapper[managed]) Connect(ctx context.Context, mg resource.Managed) (ExternalClient, error) { cr, ok := mg.(managed) if !ok { return nil, errors.Errorf(errFmtUnexpectedObjectType, mg) } external, err := c.c.Connect(ctx, cr) if err != nil { return nil, err } return &typedExternalClientWrapper[managed]{c: external}, nil } func (c *typedExternalConnectDisconnectorWrapper[managed]) Disconnect(ctx context.Context) error { return c.c.Disconnect(ctx) } // typedExternalClientWrapper wraps a TypedExternalClient to a common // ExternalClient. type typedExternalClientWrapper[managed resource.Managed] struct { c TypedExternalClient[managed] } func (c *typedExternalClientWrapper[managed]) Observe(ctx context.Context, mg resource.Managed) (ExternalObservation, error) { cr, ok := mg.(managed) if !ok { return ExternalObservation{}, errors.Errorf(errFmtUnexpectedObjectType, mg) } return c.c.Observe(ctx, cr) } func (c *typedExternalClientWrapper[managed]) Create(ctx context.Context, mg resource.Managed) (ExternalCreation, error) { cr, ok := mg.(managed) if !ok { return ExternalCreation{}, errors.Errorf(errFmtUnexpectedObjectType, mg) } return c.c.Create(ctx, cr) } func (c *typedExternalClientWrapper[managed]) Update(ctx context.Context, mg resource.Managed) (ExternalUpdate, error) { cr, ok := mg.(managed) if !ok { return ExternalUpdate{}, errors.Errorf(errFmtUnexpectedObjectType, mg) } return c.c.Update(ctx, cr) } func (c *typedExternalClientWrapper[managed]) Delete(ctx context.Context, mg resource.Managed) (ExternalDelete, error) { cr, ok := mg.(managed) if !ok { return ExternalDelete{}, errors.Errorf(errFmtUnexpectedObjectType, mg) } return c.c.Delete(ctx, cr) } func (c *typedExternalClientWrapper[managed]) Disconnect(ctx context.Context) error { return c.c.Disconnect(ctx) } ================================================ FILE: pkg/reconciler/providerconfig/reconciler.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package providerconfig provides a reconciler that manages the lifecycle of a // ProviderConfig. package providerconfig import ( "context" "strings" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/event" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) const ( finalizer = "in-use.crossplane.io" shortWait = 30 * time.Second timeout = 2 * time.Minute errGetPC = "cannot get ProviderConfig" errListPCUs = "cannot list ProviderConfigUsages" errDeletePCU = "cannot delete ProviderConfigUsage" errUpdate = "cannot update ProviderConfig" errUpdateStatus = "cannot update ProviderConfig status" ) // Event reasons. const ( reasonAccount event.Reason = "UsageAccounting" ) // Condition types and reasons. const ( TypeTerminating xpv2.ConditionType = "Terminating" ReasonInUse xpv2.ConditionReason = "InUse" ) // Terminating indicates a ProviderConfig has been deleted, but that the // deletion is being blocked because it is still in use. func Terminating() xpv2.Condition { return xpv2.Condition{ Type: TypeTerminating, Status: corev1.ConditionTrue, LastTransitionTime: metav1.Now(), Reason: ReasonInUse, } } // ControllerName returns the recommended name for controllers that use this // package to reconcile a particular kind of managed resource. func ControllerName(kind string) string { return "providerconfig/" + strings.ToLower(kind) } // A Reconciler reconciles managed resources by creating and managing the // lifecycle of an external resource, i.e. a resource in an external system such // as a cloud provider API. Each controller must watch the managed resource kind // for which it is responsible. type Reconciler struct { client client.Client newConfig func() resource.ProviderConfig newUsageList func() resource.ProviderConfigUsageList legacyPCU bool log logging.Logger record event.Recorder } // A ReconcilerOption configures a Reconciler. type ReconcilerOption func(*Reconciler) // WithLogger specifies how the Reconciler should log messages. func WithLogger(l logging.Logger) ReconcilerOption { return func(r *Reconciler) { r.log = l } } // WithRecorder specifies how the Reconciler should record events. func WithRecorder(er event.Recorder) ReconcilerOption { return func(r *Reconciler) { r.record = er } } // NewReconciler returns a Reconciler of ProviderConfigs. func NewReconciler(m manager.Manager, of resource.ProviderConfigKinds, o ...ReconcilerOption) *Reconciler { nc := func() resource.ProviderConfig { //nolint:forcetypeassert // If this isn't a ProviderConfig it's a programming error and we want to panic. return resource.MustCreateObject(of.Config, m.GetScheme()).(resource.ProviderConfig) } nul := func() resource.ProviderConfigUsageList { //nolint:forcetypeassert // If this isn't a ProviderConfigUsage it's a programming error and we want to panic. return resource.MustCreateObject(of.UsageList, m.GetScheme()).(resource.ProviderConfigUsageList) } _, isLegacyPCU := resource.MustCreateObject(of.Usage, m.GetScheme()).(resource.LegacyProviderConfigUsage) // Panic early if we've been asked to reconcile a resource kind that has not // been registered with our controller manager's scheme. _, _ = nc(), nul() r := &Reconciler{ client: m.GetClient(), newConfig: nc, newUsageList: nul, legacyPCU: isLegacyPCU, log: logging.NewNopLogger(), record: event.NewNopRecorder(), } for _, ro := range o { ro(r) } return r } // Reconcile a ProviderConfig by accounting for the managed resources that are // using it, and ensuring it cannot be deleted until it is no longer in use. func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := r.log.WithValues("request", req) log.Debug("Reconciling") ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() pc := r.newConfig() if err := r.client.Get(ctx, req.NamespacedName, pc); err != nil { // In case object is not found, most likely the object was deleted and // then disappeared while the event was in the processing queue. We // don't need to take any action in that case. log.Debug(errGetPC, "error", err) return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetPC) } log = log.WithValues( "uid", pc.GetUID(), "version", pc.GetResourceVersion(), "name", pc.GetName(), "namespace", pc.GetNamespace(), ) l := r.newUsageList() matchingLabels := client.MatchingLabels{ xpv2.LabelKeyProviderName: pc.GetName(), } if !r.legacyPCU { matchingLabels[xpv2.LabelKeyProviderKind] = pc.GetObjectKind().GroupVersionKind().Kind } listOpts := []client.ListOption{matchingLabels} if pc.GetNamespace() != "" { listOpts = append(listOpts, client.InNamespace(pc.GetNamespace())) } if err := r.client.List(ctx, l, listOpts...); err != nil { log.Debug(errListPCUs, "error", err) r.record.Event(pc, event.Warning(reasonAccount, errors.Wrap(err, errListPCUs))) return reconcile.Result{RequeueAfter: shortWait}, nil } users := int64(len(l.GetItems())) for _, pcu := range l.GetItems() { if metav1.GetControllerOf(pcu) == nil { // Usages should always have a controller reference. If this one has // none it's probably been stripped off (e.g. by a Velero restore). // We can safely delete it - it's either stale, or will be recreated // next time the relevant managed resource connects. if err := r.client.Delete(ctx, pcu); resource.IgnoreNotFound(err) != nil { log.Debug(errDeletePCU, "error", err) r.record.Event(pc, event.Warning(reasonAccount, errors.Wrap(err, errDeletePCU))) return reconcile.Result{RequeueAfter: shortWait}, nil } users-- } } log = log.WithValues("usages", users) if meta.WasDeleted(pc) { if users > 0 { msg := "Blocking deletion while usages still exist" log.Debug(msg) r.record.Event(pc, event.Warning(reasonAccount, errors.New(msg))) // We're watching our usages, so we'll be requeued when they go. pc.SetUsers(users) pc.SetConditions(Terminating().WithMessage(msg)) return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, pc), errUpdateStatus) } meta.RemoveFinalizer(pc, finalizer) if err := r.client.Update(ctx, pc); err != nil { r.log.Debug(errUpdate, "error", err) return reconcile.Result{RequeueAfter: shortWait}, nil } // We've been deleted - there's no more work to do. return reconcile.Result{Requeue: false}, nil } meta.AddFinalizer(pc, finalizer) if err := r.client.Update(ctx, pc); err != nil { r.log.Debug(errUpdate, "error", err) return reconcile.Result{RequeueAfter: shortWait}, nil } // There's no need to requeue explicitly - we're watching all PCs. pc.SetUsers(users) return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, pc), errUpdateStatus) } ================================================ FILE: pkg/reconciler/providerconfig/reconciler_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package providerconfig import ( "context" "encoding/json" "testing" "github.com/google/go-cmp/cmp" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) // This can't live in fake, because it would cause an import cycle due to // GetItems returning managed.ProviderConfigUsage. type ProviderConfigUsageList struct { client.ObjectList Items []resource.ProviderConfigUsage } func (p *ProviderConfigUsageList) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } func (p *ProviderConfigUsageList) DeepCopyObject() runtime.Object { out := &ProviderConfigUsageList{} j, err := json.Marshal(p) //nolint:musttag // We're just using this to round-trip convert. if err != nil { panic(err) } _ = json.Unmarshal(j, out) //nolint:musttag // We're just using this to round-trip convert. return out } func (p *ProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage { return p.Items } func TestReconciler(t *testing.T) { errBoom := errors.New("boom") now := metav1.Now() uid := types.UID("so-unique") ctrl := true type args struct { m manager.Manager of resource.ProviderConfigKinds } type want struct { result reconcile.Result err error request reconcile.Request } cases := map[string]struct { reason string args args want want }{ "GetProviderConfigError": { reason: "Errors getting a provider config should be returned", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(errBoom), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{}, err: errors.Wrap(errBoom, errGetPC), }, }, "ProviderConfigNotFound": { reason: "We should return without requeueing if the provider config no longer exists", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{}, err: nil, }, }, "ListProviderConfigUsageError": { reason: "We should requeue after a short wait if we encounter an error listing provider config usages", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil), MockList: test.NewMockListFn(errBoom), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{RequeueAfter: shortWait}, }, }, "DeleteProviderConfigUsageError": { reason: "We should requeue after a short wait if we encounter an error deleting a provider config usage", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil), MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error { l := obj.(*ProviderConfigUsageList) l.Items = []resource.ProviderConfigUsage{ &fake.ProviderConfigUsage{}, } return nil }), MockDelete: test.NewMockDeleteFn(errBoom), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{RequeueAfter: shortWait}, }, }, "BlockDeleteWhileInUse": { reason: "We should return without requeueing if the provider config is still in use", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { pc := obj.(*fake.ProviderConfig) pc.SetDeletionTimestamp(&now) pc.SetUID(uid) return nil }), MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error { l := obj.(*ProviderConfigUsageList) l.Items = []resource.ProviderConfigUsage{ &fake.ProviderConfigUsage{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{{ UID: uid, Controller: &ctrl, }}, }, }, } return nil }), MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{Requeue: false}, }, }, "RemoveFinalizerError": { reason: "We should requeue after a short wait if we encounter an error while removing our finalizer", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { pc := obj.(*fake.ProviderConfig) pc.SetDeletionTimestamp(&now) return nil }), MockList: test.NewMockListFn(nil), MockUpdate: test.NewMockUpdateFn(errBoom), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{RequeueAfter: shortWait}, }, }, "SuccessfulDelete": { reason: "We should return without requeueing when we successfully remove our finalizer", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { pc := obj.(*fake.ProviderConfig) pc.SetDeletionTimestamp(&now) return nil }), MockList: test.NewMockListFn(nil), MockUpdate: test.NewMockUpdateFn(nil), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{Requeue: false}, }, }, "AddFinalizerError": { reason: "We should requeue after a short wait if we encounter an error while adding our finalizer", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil), MockList: test.NewMockListFn(nil), MockUpdate: test.NewMockUpdateFn(errBoom), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{RequeueAfter: shortWait}, }, }, "UpdateStatusError": { reason: "We return errors encountered while updating our status", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil), MockList: test.NewMockListFn(nil), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.NewMockSubResourceUpdateFn(errBoom), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{Requeue: false}, err: errors.Wrap(errBoom, errUpdateStatus), }, }, "SuccessfulSetUsers": { reason: "We should return without requeuing if we successfully update our user count", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil), MockList: test.NewMockListFn(nil), MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{Requeue: false}, }, }, "ListUsagesScopedToNamespaceWhenNamespaced": { reason: "When ProviderConfig is namespaced, List should be called with InNamespace option", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { pc := obj.(*fake.ProviderConfig) pc.SetNamespace("test-ns") pc.SetName("my-pc") return nil }), MockList: func(_ context.Context, _ client.ObjectList, opts ...client.ListOption) error { // Capture and verify list options: should include InNamespace("test-ns") listOpts := &client.ListOptions{} for _, opt := range opts { opt.ApplyToList(listOpts) } if listOpts.Namespace != "test-ns" { t.Errorf("List called with namespace %q, want InNamespace(\"test-ns\")", listOpts.Namespace) } return nil }, MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{Requeue: false}, request: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-pc", Namespace: "test-ns"}}, }, }, "ListUsagesNotScopedToNamespaceWhenClusterScoped": { reason: "When ProviderConfig is cluster-scoped (empty namespace), List should not include InNamespace", args: args{ m: &fake.Manager{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { pc := obj.(*fake.ProviderConfig) pc.SetNamespace("") // cluster-scoped pc.SetName("my-pc") return nil }), MockList: func(_ context.Context, _ client.ObjectList, opts ...client.ListOption) error { listOpts := &client.ListOptions{} for _, opt := range opts { opt.ApplyToList(listOpts) } if listOpts.Namespace != "" { t.Errorf("List called with namespace %q for cluster-scoped ProviderConfig, want empty", listOpts.Namespace) } return nil }, MockUpdate: test.NewMockUpdateFn(nil), MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil), }, Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &fake.ProviderConfigUsage{}, &ProviderConfigUsageList{}), }, of: resource.ProviderConfigKinds{ Config: fake.GVK(&fake.ProviderConfig{}), Usage: fake.GVK(&fake.ProviderConfigUsage{}), UsageList: fake.GVK(&ProviderConfigUsageList{}), }, }, want: want{ result: reconcile.Result{Requeue: false}, request: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-pc"}}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewReconciler(tc.args.m, tc.args.of) req := tc.want.request got, err := r.Reconcile(context.Background(), req) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nr.Reconcile(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.result, got); diff != "" { t.Errorf("\n%s\nr.Reconcile(...): -want, +got:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/reference/namespaced_reference.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package reference import ( "context" "maps" "slices" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) // A NamespacedResolutionRequest requests that a reference to a particular kind of // managed resource be resolved. type NamespacedResolutionRequest struct { CurrentValue string Reference *xpv2.NamespacedReference Selector *xpv2.NamespacedSelector To To Extract ExtractValueFn Namespace string } // IsNoOp returns true if the supplied NamespacedResolutionRequest cannot or should not be // processed. func (rr *NamespacedResolutionRequest) IsNoOp() bool { isAlways := false if rr.Selector != nil { if rr.Selector.Policy.IsResolvePolicyAlways() { rr.Reference = nil isAlways = true } } else if rr.Reference != nil { if rr.Reference.Policy.IsResolvePolicyAlways() { isAlways = true } } // We don't resolve values that are already set (if reference resolution policy // is not set to Always); we effectively cache resolved values. The CR author // can invalidate the cache and trigger a new resolution by explicitly clearing // the resolved value. if rr.CurrentValue != "" && !isAlways { return true } // We can't resolve anything if neither a reference nor a selector were // provided. return rr.Reference == nil && rr.Selector == nil } // A NamespacedResolutionResponse returns the result of a reference resolution. The // returned values are always safe to set if resolution was successful. type NamespacedResolutionResponse struct { ResolvedValue string ResolvedReference *xpv2.NamespacedReference } // Validate this NamespacedResolutionResponse. func (rr NamespacedResolutionResponse) Validate() error { if rr.ResolvedValue == "" { return errors.New(errNoValue) } return nil } // A MultiNamespacedResolutionRequest requests that several references to a particular // kind of managed resource be resolved. type MultiNamespacedResolutionRequest struct { CurrentValues []string References []xpv2.NamespacedReference Selector *xpv2.NamespacedSelector To To Extract ExtractValueFn Namespace string } // IsNoOp returns true if the supplied MultiNamespacedResolutionRequest cannot or should // not be processed. func (rr *MultiNamespacedResolutionRequest) IsNoOp() bool { isAlways := false if rr.Selector != nil { if rr.Selector.Policy.IsResolvePolicyAlways() { rr.References = nil isAlways = true } } else { for _, r := range rr.References { if r.Policy.IsResolvePolicyAlways() { isAlways = true break } } } // We don't resolve values that are already set (if reference resolution policy // is not set to Always); we effectively cache resolved values. The CR author // can invalidate the cache and trigger a new resolution by explicitly clearing // the resolved values. This is a little unintuitive for the APIMultiResolver // but mimics the UX of the MultiNamespacedResolutionRequest and simplifies the overall mental model. if len(rr.CurrentValues) > 0 && !isAlways { return true } // We can't resolve anything if neither a reference nor a selector were // provided. return len(rr.References) == 0 && rr.Selector == nil } // A MultiNamespacedResolutionResponse returns the result of several reference // resolutions. The returned values are always safe to set if resolution was // successful. type MultiNamespacedResolutionResponse struct { ResolvedValues []string ResolvedReferences []xpv2.NamespacedReference } // Validate this MultiNamespacedResolutionResponse. func (rr MultiNamespacedResolutionResponse) Validate() error { if len(rr.ResolvedValues) == 0 { return errors.New(errNoMatches) } for i, v := range rr.ResolvedValues { if v == "" { return getResolutionError(rr.ResolvedReferences[i].Policy, errors.New(errNoValue)) } } return nil } // An APINamespacedResolver selects and resolves references to managed resources in the // Kubernetes API server. type APINamespacedResolver struct { client client.Reader from resource.Managed } // NewAPINamespacedResolver returns a Resolver that selects and resolves references from // the supplied managed resource to other managed resources in the Kubernetes // API server. func NewAPINamespacedResolver(c client.Reader, from resource.Managed) *APINamespacedResolver { return &APINamespacedResolver{client: c, from: from} } // Resolve the supplied NamespacedResolutionRequest. The returned NamespacedResolutionResponse // always contains valid values unless an error was returned. func (r *APINamespacedResolver) Resolve(ctx context.Context, req NamespacedResolutionRequest) (NamespacedResolutionResponse, error) { // Return early if from is being deleted, or the request is a no-op. if meta.WasDeleted(r.from) || req.IsNoOp() { return NamespacedResolutionResponse{ResolvedValue: req.CurrentValue, ResolvedReference: req.Reference}, nil } // The reference is already set - resolve it. if req.Reference != nil { // default to same namespace ns := req.Reference.Namespace if ns == "" { ns = r.from.GetNamespace() } if err := r.client.Get(ctx, types.NamespacedName{Name: req.Reference.Name, Namespace: ns}, req.To.Managed); err != nil { if kerrors.IsNotFound(err) { return NamespacedResolutionResponse{}, getResolutionError(req.Reference.Policy, errors.Wrap(err, errGetManaged)) } return NamespacedResolutionResponse{}, errors.Wrap(err, errGetManaged) } rsp := NamespacedResolutionResponse{ResolvedValue: req.Extract(req.To.Managed), ResolvedReference: req.Reference} return rsp, getResolutionError(req.Reference.Policy, rsp.Validate()) } // The reference was not set, but a selector was. Select a reference. If the // request has no namespace, then InNamespace is a no-op. ns := req.Selector.Namespace if ns == "" { ns = r.from.GetNamespace() } if err := r.client.List(ctx, req.To.List, client.MatchingLabels(req.Selector.MatchLabels), client.InNamespace(ns)); err != nil { return NamespacedResolutionResponse{}, errors.Wrap(err, errListManaged) } for _, to := range req.To.List.GetItems() { if ControllersMustMatchNamespaced(req.Selector) && !meta.HaveSameController(r.from, to) { continue } rsp := NamespacedResolutionResponse{ResolvedValue: req.Extract(to), ResolvedReference: &xpv2.NamespacedReference{Name: to.GetName(), Namespace: ns}} return rsp, getResolutionError(req.Selector.Policy, rsp.Validate()) } // We couldn't resolve anything. return NamespacedResolutionResponse{}, getResolutionError(req.Selector.Policy, errors.New(errNoMatches)) } // ResolveMultiple resolves the supplied MultiNamespacedResolutionRequest. The returned // MultiNamespacedResolutionResponse always contains valid values unless an error was // returned. func (r *APINamespacedResolver) ResolveMultiple(ctx context.Context, req MultiNamespacedResolutionRequest) (MultiNamespacedResolutionResponse, error) { //nolint: gocyclo // Only at 11. // Return early if from is being deleted, or the request is a no-op. if meta.WasDeleted(r.from) || req.IsNoOp() { return MultiNamespacedResolutionResponse{ResolvedValues: req.CurrentValues, ResolvedReferences: req.References}, nil } // The references are already set - resolve them. if len(req.References) > 0 { resolvedVals := make([]string, len(req.References)) for i := range req.References { ns := req.References[i].Namespace if ns == "" { ns = r.from.GetNamespace() } if err := r.client.Get(ctx, types.NamespacedName{Name: req.References[i].Name, Namespace: ns}, req.To.Managed); err != nil { if kerrors.IsNotFound(err) { return MultiNamespacedResolutionResponse{}, getResolutionError(req.References[i].Policy, errors.Wrap(err, errGetManaged)) } return MultiNamespacedResolutionResponse{}, errors.Wrap(err, errGetManaged) } resolvedVals[i] = req.Extract(req.To.Managed) } rsp := MultiNamespacedResolutionResponse{ResolvedValues: resolvedVals, ResolvedReferences: req.References} return rsp, rsp.Validate() } // No references were set, but a selector was. Select and resolve // references. If the request has no namespace, then InNamespace is a no-op. ns := req.Selector.Namespace if ns == "" { ns = r.from.GetNamespace() } if err := r.client.List(ctx, req.To.List, client.MatchingLabels(req.Selector.MatchLabels), client.InNamespace(ns)); err != nil { return MultiNamespacedResolutionResponse{}, errors.Wrap(err, errListManaged) } valueMap := make(map[string]xpv2.NamespacedReference) for _, to := range req.To.List.GetItems() { if ControllersMustMatchNamespaced(req.Selector) && !meta.HaveSameController(r.from, to) { continue } valueMap[req.Extract(to)] = xpv2.NamespacedReference{Name: to.GetName(), Namespace: ns} } sortedKeys, sortedRefs := sortGenericMapByKeys(valueMap) rsp := MultiNamespacedResolutionResponse{ResolvedValues: sortedKeys, ResolvedReferences: sortedRefs} return rsp, getResolutionError(req.Selector.Policy, rsp.Validate()) } func sortGenericMapByKeys[T any](m map[string]T) ([]string, []T) { keys := slices.Sorted(maps.Keys(m)) values := make([]T, 0, len(keys)) for _, k := range keys { values = append(values, m[k]) } return keys, values } // ControllersMustMatchNamespaced returns true if the supplied Selector requires that a // reference be to a managed resource whose controller reference matches the // referencing resource. func ControllersMustMatchNamespaced(s *xpv2.NamespacedSelector) bool { if s == nil { return false } return s.MatchControllerRef != nil && *s.MatchControllerRef } ================================================ FILE: pkg/reference/namespaced_reference_test.go ================================================ package reference import ( "context" "fmt" "strings" "testing" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func prepareTestExamplesNamespaced(numExamples int) ([]string, []xpv2.NamespacedReference, []*fake.Managed) { values := make([]string, numExamples) refs := make([]xpv2.NamespacedReference, numExamples) controlledObj := make([]*fake.Managed, numExamples) for i := range numExamples { values[i] = fmt.Sprintf("%s%d", testValuePrefix, i) refs[i] = xpv2.NamespacedReference{ Name: fmt.Sprintf("%s%d", testResourceNamePrefix, i), } controlled := &fake.Managed{} controlled.SetName(refs[i].Name) meta.SetExternalName(controlled, values[i]) _ = meta.AddControllerReference(controlled, meta.AsController(&xpv2.TypedReference{UID: testControllerUID})) controlledObj[i] = controlled } return values, refs, controlledObj } var nsTestValues, nsTestRefs, nsTestControlled = prepareTestExamplesNamespaced(10) func TestNamespacedResolve(t *testing.T) { errBoom := errors.New("boom") now := metav1.Now() value := "coolv" ref := &xpv2.NamespacedReference{Name: "cool", Namespace: "cool-ns"} nsOmittedRef := &xpv2.NamespacedReference{Name: "cool"} optionalPolicy := xpv2.ResolutionPolicyOptional alwaysPolicy := xpv2.ResolvePolicyAlways optionalRef := &xpv2.NamespacedReference{Name: "cool", Namespace: "cool-ns", Policy: &xpv2.Policy{Resolution: &optionalPolicy}} alwaysRef := &xpv2.NamespacedReference{Name: "cool", Namespace: "cool-ns", Policy: &xpv2.Policy{Resolve: &alwaysPolicy}} controlled := &fake.Managed{} controlled.SetName(value) meta.SetExternalName(controlled, value) meta.AddControllerReference(controlled, meta.AsController(&xpv2.TypedReference{UID: types.UID("very-unique")})) type args struct { ctx context.Context req NamespacedResolutionRequest } type want struct { rsp NamespacedResolutionResponse err error } cases := map[string]struct { reason string c client.Reader from resource.Managed args args want want }{ "FromDeleted": { reason: "Should return early if the referencing managed resource was deleted", from: &fake.Managed{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}}, args: args{ req: NamespacedResolutionRequest{}, }, want: want{ rsp: NamespacedResolutionResponse{}, err: nil, }, }, "AlreadyResolved": { reason: "Should return early if the current value is non-zero", from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{CurrentValue: value}, }, want: want{ rsp: NamespacedResolutionResponse{ResolvedValue: value}, err: nil, }, }, "AlwaysResolveReference": { reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + "Always", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Reference: alwaysRef, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), CurrentValue: "oldValue", }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedValue: value, ResolvedReference: alwaysRef, }, err: nil, }, }, "Unresolvable": { reason: "Should return early if neither a reference or selector were provided", from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{}, }, want: want{ err: nil, }, }, "GetError": { reason: "Should return errors encountered while getting the referenced resource", c: &test.MockClient{ MockGet: test.NewMockGetFn(errBoom), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Reference: ref, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ err: errors.Wrap(errBoom, errGetManaged), }, }, "ResolvedNoValue": { reason: "Should return an error if the extract function returns the empty string", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Reference: ref, To: To{Managed: &fake.Managed{}}, Extract: func(resource.Managed) string { return "" }, }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedReference: ref, }, err: errors.New(errNoValue), }, }, "SuccessfulResolve": { reason: "No error should be returned when the value is successfully extracted", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Reference: ref, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedValue: value, ResolvedReference: ref, }, }, }, "SuccessfulResolveNamespaced": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Reference: ref, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), Namespace: "cool-ns", }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedValue: value, ResolvedReference: ref, }, }, }, "SuccessfulResolveInferredNamespace": { reason: "Resolve should be successful with namespace inferred from MR, when reference omits namespace", c: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { if key.Namespace == "from-ns" { meta.SetExternalName(obj.(metav1.Object), value) return nil } t.Errorf("Resolve did not infer to the MR namespace: %v", key) return errBoom }, }, from: &fake.Managed{ ObjectMeta: metav1.ObjectMeta{ Name: "some-mr", Namespace: "from-ns", }, }, args: args{ req: NamespacedResolutionRequest{ Reference: nsOmittedRef, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedValue: value, ResolvedReference: nsOmittedRef, }, }, }, "SuccessfulResolveCrossNamespace": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { if key.Namespace == "cool-ns" { meta.SetExternalName(obj.(metav1.Object), value) return nil } t.Errorf("Resolve did not infer to the other namespace: %v", key) return errBoom }, }, from: &fake.Managed{ ObjectMeta: metav1.ObjectMeta{ Name: "some-mr", Namespace: "from-ns", }, }, args: args{ req: NamespacedResolutionRequest{ Reference: ref, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedValue: value, ResolvedReference: ref, }, }, }, "OptionalReference": { reason: "No error should be returned when the resolution policy is Optional", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Reference: optionalRef, To: To{Managed: &fake.Managed{}}, Extract: func(resource.Managed) string { return "" }, }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedReference: optionalRef, }, err: nil, }, }, "ListError": { reason: "Should return errors encountered while listing potential referenced resources", c: &test.MockClient{ MockList: test.NewMockListFn(errBoom), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{}, }, }, want: want{ rsp: NamespacedResolutionResponse{}, err: errors.Wrap(errBoom, errListManaged), }, }, "NoMatches": { reason: "Should return an error when no managed resources match the selector", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{}, To: To{List: &FakeManagedList{}}, }, }, want: want{ rsp: NamespacedResolutionResponse{}, err: errors.New(errNoMatches), }, }, "OptionalSelector": { reason: "No error should be returned when the resolution policy is Optional", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{ Policy: &xpv2.Policy{Resolution: &optionalPolicy}, }, To: To{List: &FakeManagedList{}}, }, }, want: want{ rsp: NamespacedResolutionResponse{}, err: nil, }, }, "SuccessfulSelect": { reason: "A managed resource with a matching controller reference should be selected and returned", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: NamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedValue: value, ResolvedReference: &xpv2.NamespacedReference{Name: value}, }, err: nil, }, }, "SuccessfulSelectNamespaced": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: NamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), Namespace: "cool-ns", }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedValue: value, ResolvedReference: &xpv2.NamespacedReference{Name: value}, }, err: nil, }, }, "AlwaysResolveSelector": { reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + "Always", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: NamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), Policy: &xpv2.Policy{Resolve: &alwaysPolicy}, }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), CurrentValue: "oldValue", }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedValue: value, ResolvedReference: &xpv2.NamespacedReference{Name: value}, }, err: nil, }, }, "BothReferenceSelector": { reason: "When both Reference and Selector fields set and Policy is not set, the Reference must be resolved", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: NamespacedResolutionRequest{ Reference: ref, Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: NamespacedResolutionResponse{ ResolvedValue: value, ResolvedReference: ref, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewAPINamespacedResolver(tc.c, tc.from) got, err := r.Resolve(tc.args.ctx, tc.args.req) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nControllersMustMatch(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.rsp, got); diff != "" { t.Errorf("\n%s\nControllersMustMatch(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestNamespacedResolveMultiple(t *testing.T) { errBoom := errors.New("boom") now := metav1.Now() value := "coolv" value2 := "cooler" ref := xpv2.NamespacedReference{Name: "cool", Namespace: "cool-ns"} nsOmittedRef := xpv2.NamespacedReference{Name: "cool"} optionalPolicy := xpv2.ResolutionPolicyOptional alwaysPolicy := xpv2.ResolvePolicyAlways optionalRef := xpv2.NamespacedReference{Name: "cool", Policy: &xpv2.Policy{Resolution: &optionalPolicy}} alwaysRef := xpv2.NamespacedReference{Name: "cool", Policy: &xpv2.Policy{Resolve: &alwaysPolicy}} controlled := &fake.Managed{} controlled.SetName(value) meta.SetExternalName(controlled, value) meta.AddControllerReference(controlled, meta.AsController(&xpv2.TypedReference{UID: types.UID("very-unique")})) controlled2 := &fake.Managed{} controlled2.SetName(value2) meta.SetExternalName(controlled2, value2) meta.AddControllerReference(controlled2, meta.AsController(&xpv2.TypedReference{UID: types.UID("very-unique")})) type args struct { ctx context.Context req MultiNamespacedResolutionRequest } type want struct { rsp MultiNamespacedResolutionResponse err error } cases := map[string]struct { reason string c client.Reader from resource.Managed args args want want }{ "FromDeleted": { reason: "Should return early if the referencing managed resource was deleted", from: &fake.Managed{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}}, args: args{ req: MultiNamespacedResolutionRequest{}, }, want: want{ rsp: MultiNamespacedResolutionResponse{}, err: nil, }, }, "AlreadyResolved": { reason: "Should return early if the current value is non-zero", from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{CurrentValues: []string{value}}, }, want: want{ rsp: MultiNamespacedResolutionResponse{ResolvedValues: []string{value}}, err: nil, }, }, "AlwaysResolveReference": { reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + "Always", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{alwaysRef}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), CurrentValues: []string{"oldValue"}, }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.NamespacedReference{alwaysRef}, }, err: nil, }, }, "Unresolvable": { reason: "Should return early if neither a reference or selector were provided", from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{}, }, want: want{ err: nil, }, }, "GetError": { reason: "Should return errors encountered while getting the referenced resource", c: &test.MockClient{ MockGet: test.NewMockGetFn(errBoom), }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{ref}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ err: errors.Wrap(errBoom, errGetManaged), }, }, "ResolvedNoValue": { reason: "Should return an error if the extract function returns the empty string", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{ref}, To: To{Managed: &fake.Managed{}}, Extract: func(resource.Managed) string { return "" }, }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{""}, ResolvedReferences: []xpv2.NamespacedReference{ref}, }, err: errors.New(errNoValue), }, }, "SuccessfulResolve": { reason: "No error should be returned when the value is successfully extracted", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{ref}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.NamespacedReference{ref}, }, }, }, "SuccessfulResolveNamespaced": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { if key.Namespace == ref.Namespace { meta.SetExternalName(obj.(metav1.Object), value) return nil } t.Errorf("Resolve did not infer to the MR namespace: %v", key) return errBoom }, }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{ref}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.NamespacedReference{ref}, }, }, }, "SuccessfulResolveInferredNamespace": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { if key.Namespace == "from-ns" { meta.SetExternalName(obj.(metav1.Object), value) return nil } t.Errorf("Resolve did not infer to the MR namespace: %v", key) return errBoom }, }, from: &fake.Managed{ ObjectMeta: metav1.ObjectMeta{ Name: "some-mr", Namespace: "from-ns", }, }, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{nsOmittedRef}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.NamespacedReference{nsOmittedRef}, }, }, }, "SuccessfulResolveCrossNamespace": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { if key.Namespace == ref.Namespace { meta.SetExternalName(obj.(metav1.Object), value) return nil } t.Errorf("Resolve did not infer to the MR namespace: %v", key) return errBoom }, }, from: &fake.Managed{ ObjectMeta: metav1.ObjectMeta{ Name: "some-mr", Namespace: "from-ns", }, }, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{ref}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.NamespacedReference{ref}, }, }, }, "OptionalReference": { reason: "No error should be returned when the resolution policy is Optional", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{optionalRef}, To: To{Managed: &fake.Managed{}}, Extract: func(resource.Managed) string { return "" }, }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{""}, ResolvedReferences: []xpv2.NamespacedReference{optionalRef}, }, err: nil, }, }, "ListError": { reason: "Should return errors encountered while listing potential referenced resources", c: &test.MockClient{ MockList: test.NewMockListFn(errBoom), }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{}, }, }, want: want{ rsp: MultiNamespacedResolutionResponse{}, err: errors.Wrap(errBoom, errListManaged), }, }, "NoMatches": { reason: "Should return an error when no managed resources match the selector", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{}, To: To{List: &FakeManagedList{}}, }, }, want: want{ rsp: MultiNamespacedResolutionResponse{}, err: errors.New(errNoMatches), }, }, "OptionalSelector": { reason: "No error should be returned when the resolution policy is Optional", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{ Policy: &xpv2.Policy{Resolution: &optionalPolicy}, }, To: To{List: &FakeManagedList{}}, }, }, want: want{ rsp: MultiNamespacedResolutionResponse{}, err: nil, }, }, "SuccessfulSelect": { reason: "A managed resource with a matching controller reference should be selected and returned", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: MultiNamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.NamespacedReference{{Name: value}}, }, err: nil, }, }, "SuccessfulSelectNamespaced": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: MultiNamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), Namespace: "cool-ns", }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.NamespacedReference{{Name: value}}, }, err: nil, }, }, "AlwaysResolveSelector": { reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + "Always", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: MultiNamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), Policy: &xpv2.Policy{Resolve: &alwaysPolicy}, }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), CurrentValues: []string{"oldValue"}, }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.NamespacedReference{{Name: value}}, }, err: nil, }, }, "BothReferenceSelector": { reason: "When both Reference and Selector fields set and Policy is not set, the Reference must be resolved", c: &test.MockClient{ MockList: test.NewMockListFn(errors.New("unexpected call to List when resolving Refs only")), MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{ref}, Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.NamespacedReference{ref}, }, }, }, "SelectorOrderOutput": { reason: "Resolved values should be ordered when resolving a selector", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: MultiNamespacedResolutionRequest{ Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{ Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. &fake.Managed{}, // A resource that does not match. controlled2, // A resource with a matching controller reference. }, }}, Extract: ExternalName(), }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{value2, value}, ResolvedReferences: []xpv2.NamespacedReference{{Name: value2}, {Name: value}}, }, err: nil, }, }, "NoSelectorOnlyRefs": { reason: "Refs should not be re-ordered when selector is omitted", c: &test.MockClient{ MockList: test.NewMockListFn(errors.New("unexpected call to List when resolving Refs only")), MockGet: func(_ context.Context, objKey client.ObjectKey, obj client.Object) error { if !strings.HasPrefix(objKey.Name, testResourceNamePrefix) { return errors.New("test resource not found") } val := strings.Replace(objKey.Name, testResourceNamePrefix, testValuePrefix, 1) meta.SetExternalName(obj.(metav1.Object), val) return nil }, }, from: controlled, args: args{ req: MultiNamespacedResolutionRequest{ References: []xpv2.NamespacedReference{nsTestRefs[2], nsTestRefs[3], nsTestRefs[0], nsTestRefs[1]}, To: To{ Managed: &fake.Managed{}, }, Extract: ExternalName(), }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ ResolvedValues: []string{nsTestValues[2], nsTestValues[3], nsTestValues[0], nsTestValues[1]}, ResolvedReferences: []xpv2.NamespacedReference{nsTestRefs[2], nsTestRefs[3], nsTestRefs[0], nsTestRefs[1]}, }, err: nil, }, }, "AlwaysResolveSelector_NewValuesOrdered": { reason: "Must resolve new matches and reorder resolved values & refs, when Selector policy is Always and have existing refs and values", c: &test.MockClient{ MockList: test.NewMockListFn(nil), MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: controlled, args: args{ req: MultiNamespacedResolutionRequest{ CurrentValues: []string{nsTestValues[1], nsTestValues[4]}, References: []xpv2.NamespacedReference{nsTestRefs[1], nsTestRefs[4]}, Selector: &xpv2.NamespacedSelector{ MatchControllerRef: func() *bool { t := true; return &t }(), Policy: &xpv2.Policy{Resolve: &alwaysPolicy}, }, To: To{ Managed: &fake.Managed{}, List: &FakeManagedList{ // List result is not ordered Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. nsTestControlled[2], // A resource with a matching controller reference. &fake.Managed{}, // A resource that does not match. nsTestControlled[4], // A resource with a matching controller reference. nsTestControlled[1], // A resource with a matching controller reference and newly introduced }, }, }, Extract: ExternalName(), }, }, want: want{ rsp: MultiNamespacedResolutionResponse{ // expect ordered resolved values ResolvedValues: []string{nsTestValues[1], nsTestValues[2], nsTestValues[4]}, ResolvedReferences: []xpv2.NamespacedReference{nsTestRefs[1], nsTestRefs[2], nsTestRefs[4]}, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewAPINamespacedResolver(tc.c, tc.from) got, err := r.ResolveMultiple(tc.args.ctx, tc.args.req) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nControllersMustMatch(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.rsp, got, cmpopts.EquateEmpty()); diff != "" { t.Errorf("\n%s\nControllersMustMatch(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestNamespacedControllersMustMatch(t *testing.T) { cases := map[string]struct { s *xpv2.NamespacedSelector want bool }{ "NilSelector": { s: nil, want: false, }, "NilMatchControllerRef": { s: &xpv2.NamespacedSelector{}, want: false, }, "False": { s: &xpv2.NamespacedSelector{MatchControllerRef: func() *bool { f := false; return &f }()}, want: false, }, "True": { s: &xpv2.NamespacedSelector{MatchControllerRef: func() *bool { t := true; return &t }()}, want: true, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := ControllersMustMatchNamespaced(tc.s) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("ControllersMustMatch(...): -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/reference/reference.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package reference contains utilities for working with cross-resource // references. package reference import ( "context" "maps" "slices" "strconv" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) // Error strings. const ( errGetManaged = "cannot get referenced resource" errListManaged = "cannot list resources that match selector" errNoMatches = "no resources matched selector" errNoValue = "referenced field was empty (referenced resource may not yet be ready)" ) // NOTE(negz): There are many equivalents of FromPtrValue and ToPtrValue // throughout the Crossplane codebase. We duplicate them here to reduce the // number of packages our API types have to import to support references. // FromPtrValue adapts a string pointer field for use as a CurrentValue. func FromPtrValue(v *string) string { if v == nil { return "" } return *v } // FromFloatPtrValue adapts a float pointer field for use as a CurrentValue. func FromFloatPtrValue(v *float64) string { if v == nil { return "" } return strconv.FormatFloat(*v, 'f', 0, 64) } // FromIntPtrValue adapts an int pointer field for use as a CurrentValue. func FromIntPtrValue(v *int64) string { if v == nil { return "" } return strconv.FormatInt(*v, 10) } // ToPtrValue adapts a ResolvedValue for use as a string pointer field. func ToPtrValue(v string) *string { if v == "" { return nil } return &v } // ToFloatPtrValue adapts a ResolvedValue for use as a float64 pointer field. func ToFloatPtrValue(v string) *float64 { if v == "" { return nil } vParsed, err := strconv.ParseFloat(v, 64) if err != nil { return nil } return &vParsed } // ToIntPtrValue adapts a ResolvedValue for use as an int pointer field. func ToIntPtrValue(v string) *int64 { if v == "" { return nil } vParsed, err := strconv.ParseInt(v, 10, 64) if err != nil { return nil } return &vParsed } // FromPtrValues adapts a slice of string pointer fields for use as CurrentValues. // NOTE: Do not use this utility function unless you have to. // Using pointer slices does not adhere to our current API practices. // The current use case is where generated code creates reference-able fields in a provider which are // string pointers and need to be resolved as part of `ResolveMultiple`. func FromPtrValues(v []*string) []string { res := make([]string, len(v)) for i := range v { res[i] = FromPtrValue(v[i]) } return res } // FromFloatPtrValues adapts a slice of float64 pointer fields for use as CurrentValues. func FromFloatPtrValues(v []*float64) []string { res := make([]string, len(v)) for i := range v { res[i] = FromFloatPtrValue(v[i]) } return res } // FromIntPtrValues adapts a slice of int64 pointer fields for use as CurrentValues. func FromIntPtrValues(v []*int64) []string { res := make([]string, len(v)) for i := range v { res[i] = FromIntPtrValue(v[i]) } return res } // ToPtrValues adapts ResolvedValues for use as a slice of string pointer fields. // NOTE: Do not use this utility function unless you have to. // Using pointer slices does not adhere to our current API practices. // The current use case is where generated code creates reference-able fields in a provider which are // string pointers and need to be resolved as part of `ResolveMultiple`. func ToPtrValues(v []string) []*string { res := make([]*string, len(v)) for i := range v { res[i] = ToPtrValue(v[i]) } return res } // ToFloatPtrValues adapts ResolvedValues for use as a slice of float64 pointer fields. func ToFloatPtrValues(v []string) []*float64 { res := make([]*float64, len(v)) for i := range v { res[i] = ToFloatPtrValue(v[i]) } return res } // ToIntPtrValues adapts ResolvedValues for use as a slice of int64 pointer fields. func ToIntPtrValues(v []string) []*int64 { res := make([]*int64, len(v)) for i := range v { res[i] = ToIntPtrValue(v[i]) } return res } // To indicates the kind of managed resource a reference is to. type To struct { Managed resource.Managed List resource.ManagedList } // An ExtractValueFn specifies how to extract a value from the resolved managed // resource. type ExtractValueFn func(resource.Managed) string // ExternalName extracts the resolved managed resource's external name from its // external name annotation. func ExternalName() ExtractValueFn { return func(mg resource.Managed) string { return meta.GetExternalName(mg) } } // A ResolutionRequest requests that a reference to a particular kind of // managed resource be resolved. type ResolutionRequest struct { CurrentValue string Reference *xpv2.Reference Selector *xpv2.Selector To To Extract ExtractValueFn Namespace string } // IsNoOp returns true if the supplied ResolutionRequest cannot or should not be // processed. func (rr *ResolutionRequest) IsNoOp() bool { isAlways := false if rr.Selector != nil { if rr.Selector.Policy.IsResolvePolicyAlways() { rr.Reference = nil isAlways = true } } else if rr.Reference != nil { if rr.Reference.Policy.IsResolvePolicyAlways() { isAlways = true } } // We don't resolve values that are already set (if reference resolution policy // is not set to Always); we effectively cache resolved values. The CR author // can invalidate the cache and trigger a new resolution by explicitly clearing // the resolved value. if rr.CurrentValue != "" && !isAlways { return true } // We can't resolve anything if neither a reference nor a selector were // provided. return rr.Reference == nil && rr.Selector == nil } // A ResolutionResponse returns the result of a reference resolution. The // returned values are always safe to set if resolution was successful. type ResolutionResponse struct { ResolvedValue string ResolvedReference *xpv2.Reference } // Validate this ResolutionResponse. func (rr ResolutionResponse) Validate() error { if rr.ResolvedValue == "" { return errors.New(errNoValue) } return nil } // A MultiResolutionRequest requests that several references to a particular // kind of managed resource be resolved. type MultiResolutionRequest struct { CurrentValues []string References []xpv2.Reference Selector *xpv2.Selector To To Extract ExtractValueFn Namespace string } // IsNoOp returns true if the supplied MultiResolutionRequest cannot or should // not be processed. func (rr *MultiResolutionRequest) IsNoOp() bool { isAlways := false if rr.Selector != nil { if rr.Selector.Policy.IsResolvePolicyAlways() { rr.References = nil isAlways = true } } else { for _, r := range rr.References { if r.Policy.IsResolvePolicyAlways() { isAlways = true break } } } // We don't resolve values that are already set (if reference resolution policy // is not set to Always); we effectively cache resolved values. The CR author // can invalidate the cache and trigger a new resolution by explicitly clearing // the resolved values. This is a little unintuitive for the APIMultiResolver // but mimics the UX of the APIResolver and simplifies the overall mental model. if len(rr.CurrentValues) > 0 && !isAlways { return true } // We can't resolve anything if neither a reference nor a selector were // provided. return len(rr.References) == 0 && rr.Selector == nil } // A MultiResolutionResponse returns the result of several reference // resolutions. The returned values are always safe to set if resolution was // successful. type MultiResolutionResponse struct { ResolvedValues []string ResolvedReferences []xpv2.Reference } // Validate this MultiResolutionResponse. func (rr MultiResolutionResponse) Validate() error { if len(rr.ResolvedValues) == 0 { return errors.New(errNoMatches) } for i, v := range rr.ResolvedValues { if v == "" { return getResolutionError(rr.ResolvedReferences[i].Policy, errors.New(errNoValue)) } } return nil } // An APIResolver selects and resolves references to managed resources in the // Kubernetes API server. type APIResolver struct { client client.Reader from resource.Managed } // NewAPIResolver returns a Resolver that selects and resolves references from // the supplied managed resource to other managed resources in the Kubernetes // API server. func NewAPIResolver(c client.Reader, from resource.Managed) *APIResolver { return &APIResolver{client: c, from: from} } // Resolve the supplied ResolutionRequest. The returned ResolutionResponse // always contains valid values unless an error was returned. func (r *APIResolver) Resolve(ctx context.Context, req ResolutionRequest) (ResolutionResponse, error) { // Return early if from is being deleted, or the request is a no-op. if meta.WasDeleted(r.from) || req.IsNoOp() { return ResolutionResponse{ResolvedValue: req.CurrentValue, ResolvedReference: req.Reference}, nil } // The reference is already set - resolve it. if req.Reference != nil { if err := r.client.Get(ctx, types.NamespacedName{Name: req.Reference.Name, Namespace: req.Namespace}, req.To.Managed); err != nil { if kerrors.IsNotFound(err) { return ResolutionResponse{}, getResolutionError(req.Reference.Policy, errors.Wrap(err, errGetManaged)) } return ResolutionResponse{}, errors.Wrap(err, errGetManaged) } rsp := ResolutionResponse{ResolvedValue: req.Extract(req.To.Managed), ResolvedReference: req.Reference} return rsp, getResolutionError(req.Reference.Policy, rsp.Validate()) } // The reference was not set, but a selector was. Select a reference. If the // request has no namespace, then InNamespace is a no-op. if err := r.client.List(ctx, req.To.List, client.MatchingLabels(req.Selector.MatchLabels), client.InNamespace(req.Namespace)); err != nil { return ResolutionResponse{}, errors.Wrap(err, errListManaged) } for _, to := range req.To.List.GetItems() { if ControllersMustMatch(req.Selector) && !meta.HaveSameController(r.from, to) { continue } rsp := ResolutionResponse{ResolvedValue: req.Extract(to), ResolvedReference: &xpv2.Reference{Name: to.GetName()}} return rsp, getResolutionError(req.Selector.Policy, rsp.Validate()) } // We couldn't resolve anything. return ResolutionResponse{}, getResolutionError(req.Selector.Policy, errors.New(errNoMatches)) } // ResolveMultiple resolves the supplied MultiResolutionRequest. The returned // MultiResolutionResponse always contains valid values unless an error was // returned. func (r *APIResolver) ResolveMultiple(ctx context.Context, req MultiResolutionRequest) (MultiResolutionResponse, error) { //nolint: gocyclo // Only at 11. // Return early if from is being deleted, or the request is a no-op. if meta.WasDeleted(r.from) || req.IsNoOp() { return MultiResolutionResponse{ResolvedValues: req.CurrentValues, ResolvedReferences: req.References}, nil } // The references are already set - resolve them. if len(req.References) > 0 { resolvedVals := make([]string, len(req.References)) for i := range req.References { if err := r.client.Get(ctx, types.NamespacedName{Name: req.References[i].Name, Namespace: req.Namespace}, req.To.Managed); err != nil { if kerrors.IsNotFound(err) { return MultiResolutionResponse{}, getResolutionError(req.References[i].Policy, errors.Wrap(err, errGetManaged)) } return MultiResolutionResponse{}, errors.Wrap(err, errGetManaged) } resolvedVals[i] = req.Extract(req.To.Managed) } rsp := MultiResolutionResponse{ResolvedValues: resolvedVals, ResolvedReferences: req.References} return rsp, rsp.Validate() } // No references were set, but a selector was. Select and resolve // references. If the request has no namespace, then InNamespace is a no-op. if err := r.client.List(ctx, req.To.List, client.MatchingLabels(req.Selector.MatchLabels), client.InNamespace(req.Namespace)); err != nil { return MultiResolutionResponse{}, errors.Wrap(err, errListManaged) } valueMap := make(map[string]xpv2.Reference) for _, to := range req.To.List.GetItems() { if ControllersMustMatch(req.Selector) && !meta.HaveSameController(r.from, to) { continue } valueMap[req.Extract(to)] = xpv2.Reference{Name: to.GetName()} } sortedKeys, sortedRefs := sortMapByKeys(valueMap) rsp := MultiResolutionResponse{ResolvedValues: sortedKeys, ResolvedReferences: sortedRefs} return rsp, getResolutionError(req.Selector.Policy, rsp.Validate()) } func getResolutionError(p *xpv2.Policy, err error) error { if !p.IsResolutionPolicyOptional() { return err } return nil } func sortMapByKeys(m map[string]xpv2.Reference) ([]string, []xpv2.Reference) { keys := slices.Sorted(maps.Keys(m)) values := make([]xpv2.Reference, 0, len(keys)) for _, k := range keys { values = append(values, m[k]) } return keys, values } // ControllersMustMatch returns true if the supplied Selector requires that a // reference be to a managed resource whose controller reference matches the // referencing resource. func ControllersMustMatch(s *xpv2.Selector) bool { if s == nil { return false } return s.MatchControllerRef != nil && *s.MatchControllerRef } ================================================ FILE: pkg/reference/reference_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at htcp://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package reference import ( "context" "fmt" "strings" "testing" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) const ( testResourceNamePrefix = "cool-resource-" testValuePrefix = "cool-value-" testControllerUID = types.UID("very-unique") ) func prepareTestExamples(numExamples int) ([]string, []xpv2.Reference, []*fake.Managed) { values := make([]string, numExamples) refs := make([]xpv2.Reference, numExamples) controlledObj := make([]*fake.Managed, numExamples) for i := range numExamples { values[i] = fmt.Sprintf("%s%d", testValuePrefix, i) refs[i] = xpv2.Reference{ Name: fmt.Sprintf("%s%d", testResourceNamePrefix, i), } controlled := &fake.Managed{} controlled.SetName(refs[i].Name) meta.SetExternalName(controlled, values[i]) _ = meta.AddControllerReference(controlled, meta.AsController(&xpv2.TypedReference{UID: testControllerUID})) controlledObj[i] = controlled } return values, refs, controlledObj } var testValues, testRefs, testControlled = prepareTestExamples(10) // TODO(negz): Find a better home for this. It can't currently live alongside // its contemporaries in pkg/resource/fake because it would cause an import // cycle. type FakeManagedList struct { client.ObjectList Items []resource.Managed } func (fml *FakeManagedList) GetItems() []resource.Managed { return fml.Items } func TestToAndFromPtr(t *testing.T) { cases := map[string]struct { want string }{ "Zero": {want: ""}, "NonZero": {want: "pointy"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := FromPtrValue(ToPtrValue(tc.want)) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("FromPtrValue(ToPtrValue(%s): -want, +got: %s", tc.want, diff) } }) } } func TestToAndFromFloatPtr(t *testing.T) { cases := map[string]struct { want string }{ "Zero": {want: ""}, "NonZero": {want: "1123581321"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := FromFloatPtrValue(ToFloatPtrValue(tc.want)) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("FromPtrValue(ToPtrValue(%s): -want, +got: %s", tc.want, diff) } }) } } func TestToAndFromPtrValues(t *testing.T) { cases := map[string]struct { want []string }{ "Nil": {want: []string{}}, "Zero": {want: []string{""}}, "NonZero": {want: []string{"pointy"}}, "Multiple": {want: []string{"pointy", "pointers"}}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := FromPtrValues(ToPtrValues(tc.want)) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("FromPtrValues(ToPtrValues(%s): -want, +got: %s", tc.want, diff) } }) } } func TestToAndFromFloatPtrValues(t *testing.T) { cases := map[string]struct { want []string }{ "Nil": {want: []string{}}, "Zero": {want: []string{""}}, "NonZero": {want: []string{"1123581321"}}, "Multiple": {want: []string{"1123581321", "1234567890"}}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := FromFloatPtrValues(ToFloatPtrValues(tc.want)) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("FromPtrValues(ToPtrValues(%s): -want, +got: %s", tc.want, diff) } }) } } func TestToAndFromIntPtrValues(t *testing.T) { cases := map[string]struct { want []string }{ "Nil": {want: []string{}}, "Zero": {want: []string{""}}, "NonZero": {want: []string{"1123581321"}}, "Multiple": {want: []string{"1123581321", "1234567890"}}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := FromIntPtrValues(ToIntPtrValues(tc.want)) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("FromIntPtrValues(ToIntPtrValues(%s): -want, +got: %s", tc.want, diff) } }) } } func TestResolve(t *testing.T) { errBoom := errors.New("boom") now := metav1.Now() value := "coolv" ref := &xpv2.Reference{Name: "cool"} optionalPolicy := xpv2.ResolutionPolicyOptional alwaysPolicy := xpv2.ResolvePolicyAlways optionalRef := &xpv2.Reference{Name: "cool", Policy: &xpv2.Policy{Resolution: &optionalPolicy}} alwaysRef := &xpv2.Reference{Name: "cool", Policy: &xpv2.Policy{Resolve: &alwaysPolicy}} controlled := &fake.Managed{} controlled.SetName(value) meta.SetExternalName(controlled, value) meta.AddControllerReference(controlled, meta.AsController(&xpv2.TypedReference{UID: types.UID("very-unique")})) type args struct { ctx context.Context req ResolutionRequest } type want struct { rsp ResolutionResponse err error } cases := map[string]struct { reason string c client.Reader from resource.Managed args args want want }{ "FromDeleted": { reason: "Should return early if the referencing managed resource was deleted", from: &fake.Managed{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}}, args: args{ req: ResolutionRequest{}, }, want: want{ rsp: ResolutionResponse{}, err: nil, }, }, "AlreadyResolved": { reason: "Should return early if the current value is non-zero", from: &fake.Managed{}, args: args{ req: ResolutionRequest{CurrentValue: value}, }, want: want{ rsp: ResolutionResponse{ResolvedValue: value}, err: nil, }, }, "AlwaysResolveReference": { reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + "Always", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Reference: alwaysRef, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), CurrentValue: "oldValue", }, }, want: want{ rsp: ResolutionResponse{ ResolvedValue: value, ResolvedReference: alwaysRef, }, err: nil, }, }, "Unresolvable": { reason: "Should return early if neither a reference or selector were provided", from: &fake.Managed{}, args: args{ req: ResolutionRequest{}, }, want: want{ err: nil, }, }, "GetError": { reason: "Should return errors encountered while getting the referenced resource", c: &test.MockClient{ MockGet: test.NewMockGetFn(errBoom), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Reference: ref, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ err: errors.Wrap(errBoom, errGetManaged), }, }, "ResolvedNoValue": { reason: "Should return an error if the extract function returns the empty string", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Reference: ref, To: To{Managed: &fake.Managed{}}, Extract: func(resource.Managed) string { return "" }, }, }, want: want{ rsp: ResolutionResponse{ ResolvedReference: ref, }, err: errors.New(errNoValue), }, }, "SuccessfulResolve": { reason: "No error should be returned when the value is successfully extracted", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Reference: ref, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: ResolutionResponse{ ResolvedValue: value, ResolvedReference: ref, }, }, }, "SuccessfulResolveNamespaced": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Reference: ref, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), Namespace: "cool-ns", }, }, want: want{ rsp: ResolutionResponse{ ResolvedValue: value, ResolvedReference: ref, }, }, }, "OptionalReference": { reason: "No error should be returned when the resolution policy is Optional", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Reference: optionalRef, To: To{Managed: &fake.Managed{}}, Extract: func(resource.Managed) string { return "" }, }, }, want: want{ rsp: ResolutionResponse{ ResolvedReference: optionalRef, }, err: nil, }, }, "ListError": { reason: "Should return errors encountered while listing potential referenced resources", c: &test.MockClient{ MockList: test.NewMockListFn(errBoom), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Selector: &xpv2.Selector{}, }, }, want: want{ rsp: ResolutionResponse{}, err: errors.Wrap(errBoom, errListManaged), }, }, "NoMatches": { reason: "Should return an error when no managed resources match the selector", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Selector: &xpv2.Selector{}, To: To{List: &FakeManagedList{}}, }, }, want: want{ rsp: ResolutionResponse{}, err: errors.New(errNoMatches), }, }, "OptionalSelector": { reason: "No error should be returned when the resolution policy is Optional", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Selector: &xpv2.Selector{ Policy: &xpv2.Policy{Resolution: &optionalPolicy}, }, To: To{List: &FakeManagedList{}}, }, }, want: want{ rsp: ResolutionResponse{}, err: nil, }, }, "SuccessfulSelect": { reason: "A managed resource with a matching controller reference should be selected and returned", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: ResolutionRequest{ Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), }, }, want: want{ rsp: ResolutionResponse{ ResolvedValue: value, ResolvedReference: &xpv2.Reference{Name: value}, }, err: nil, }, }, "SuccessfulSelectNamespaced": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: ResolutionRequest{ Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), Namespace: "cool-ns", }, }, want: want{ rsp: ResolutionResponse{ ResolvedValue: value, ResolvedReference: &xpv2.Reference{Name: value}, }, err: nil, }, }, "AlwaysResolveSelector": { reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + "Always", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: ResolutionRequest{ Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), Policy: &xpv2.Policy{Resolve: &alwaysPolicy}, }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), CurrentValue: "oldValue", }, }, want: want{ rsp: ResolutionResponse{ ResolvedValue: value, ResolvedReference: &xpv2.Reference{Name: value}, }, err: nil, }, }, "BothReferenceSelector": { reason: "When both Reference and Selector fields set and Policy is not set, the Reference must be resolved", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: ResolutionRequest{ Reference: ref, Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: ResolutionResponse{ ResolvedValue: value, ResolvedReference: ref, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewAPIResolver(tc.c, tc.from) got, err := r.Resolve(tc.args.ctx, tc.args.req) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nControllersMustMatch(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.rsp, got); diff != "" { t.Errorf("\n%s\nControllersMustMatch(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestResolveMultiple(t *testing.T) { errBoom := errors.New("boom") now := metav1.Now() value := "coolv" value2 := "cooler" ref := xpv2.Reference{Name: "cool"} optionalPolicy := xpv2.ResolutionPolicyOptional alwaysPolicy := xpv2.ResolvePolicyAlways optionalRef := xpv2.Reference{Name: "cool", Policy: &xpv2.Policy{Resolution: &optionalPolicy}} alwaysRef := xpv2.Reference{Name: "cool", Policy: &xpv2.Policy{Resolve: &alwaysPolicy}} controlled := &fake.Managed{} controlled.SetName(value) meta.SetExternalName(controlled, value) meta.AddControllerReference(controlled, meta.AsController(&xpv2.TypedReference{UID: types.UID("very-unique")})) controlled2 := &fake.Managed{} controlled2.SetName(value2) meta.SetExternalName(controlled2, value2) meta.AddControllerReference(controlled2, meta.AsController(&xpv2.TypedReference{UID: types.UID("very-unique")})) type args struct { ctx context.Context req MultiResolutionRequest } type want struct { rsp MultiResolutionResponse err error } cases := map[string]struct { reason string c client.Reader from resource.Managed args args want want }{ "FromDeleted": { reason: "Should return early if the referencing managed resource was deleted", from: &fake.Managed{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}}, args: args{ req: MultiResolutionRequest{}, }, want: want{ rsp: MultiResolutionResponse{}, err: nil, }, }, "AlreadyResolved": { reason: "Should return early if the current value is non-zero", from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{CurrentValues: []string{value}}, }, want: want{ rsp: MultiResolutionResponse{ResolvedValues: []string{value}}, err: nil, }, }, "AlwaysResolveReference": { reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + "Always", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ References: []xpv2.Reference{alwaysRef}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), CurrentValues: []string{"oldValue"}, }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.Reference{alwaysRef}, }, err: nil, }, }, "Unresolvable": { reason: "Should return early if neither a reference or selector were provided", from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{}, }, want: want{ err: nil, }, }, "GetError": { reason: "Should return errors encountered while getting the referenced resource", c: &test.MockClient{ MockGet: test.NewMockGetFn(errBoom), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ References: []xpv2.Reference{ref}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ err: errors.Wrap(errBoom, errGetManaged), }, }, "ResolvedNoValue": { reason: "Should return an error if the extract function returns the empty string", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ References: []xpv2.Reference{ref}, To: To{Managed: &fake.Managed{}}, Extract: func(resource.Managed) string { return "" }, }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{""}, ResolvedReferences: []xpv2.Reference{ref}, }, err: errors.New(errNoValue), }, }, "SuccessfulResolve": { reason: "No error should be returned when the value is successfully extracted", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ References: []xpv2.Reference{ref}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.Reference{ref}, }, }, }, "SuccessfulResolveNamespaced": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ References: []xpv2.Reference{ref}, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), Namespace: "cool-ns", }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.Reference{ref}, }, }, }, "OptionalReference": { reason: "No error should be returned when the resolution policy is Optional", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ References: []xpv2.Reference{optionalRef}, To: To{Managed: &fake.Managed{}}, Extract: func(resource.Managed) string { return "" }, }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{""}, ResolvedReferences: []xpv2.Reference{optionalRef}, }, err: nil, }, }, "ListError": { reason: "Should return errors encountered while listing potential referenced resources", c: &test.MockClient{ MockList: test.NewMockListFn(errBoom), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ Selector: &xpv2.Selector{}, }, }, want: want{ rsp: MultiResolutionResponse{}, err: errors.Wrap(errBoom, errListManaged), }, }, "NoMatches": { reason: "Should return an error when no managed resources match the selector", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ Selector: &xpv2.Selector{}, To: To{List: &FakeManagedList{}}, }, }, want: want{ rsp: MultiResolutionResponse{}, err: errors.New(errNoMatches), }, }, "OptionalSelector": { reason: "No error should be returned when the resolution policy is Optional", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ Selector: &xpv2.Selector{ Policy: &xpv2.Policy{Resolution: &optionalPolicy}, }, To: To{List: &FakeManagedList{}}, }, }, want: want{ rsp: MultiResolutionResponse{}, err: nil, }, }, "SuccessfulSelect": { reason: "A managed resource with a matching controller reference should be selected and returned", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: MultiResolutionRequest{ Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.Reference{{Name: value}}, }, err: nil, }, }, "SuccessfulSelectNamespaced": { reason: "Resolve should be successful when a namespace is given", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: MultiResolutionRequest{ Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), Namespace: "cool-ns", }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.Reference{{Name: value}}, }, err: nil, }, }, "AlwaysResolveSelector": { reason: "Should not return early if the current value is non-zero, when the resolve policy is set to" + "Always", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: MultiResolutionRequest{ Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), Policy: &xpv2.Policy{Resolve: &alwaysPolicy}, }, To: To{List: &FakeManagedList{Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. }}}, Extract: ExternalName(), CurrentValues: []string{"oldValue"}, }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.Reference{{Name: value}}, }, err: nil, }, }, "BothReferenceSelector": { reason: "When both Reference and Selector fields set and Policy is not set, the Reference must be resolved", c: &test.MockClient{ MockList: test.NewMockListFn(errors.New("unexpected call to List when resolving Refs only")), MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: &fake.Managed{}, args: args{ req: MultiResolutionRequest{ References: []xpv2.Reference{ref}, Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{Managed: &fake.Managed{}}, Extract: ExternalName(), }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{value}, ResolvedReferences: []xpv2.Reference{ref}, }, }, }, "SelectorOrderOutput": { reason: "Resolved values should be ordered when resolving a selector", c: &test.MockClient{ MockList: test.NewMockListFn(nil), }, from: controlled, args: args{ req: MultiResolutionRequest{ Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), }, To: To{List: &FakeManagedList{ Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. controlled, // A resource with a matching controller reference. &fake.Managed{}, // A resource that does not match. controlled2, // A resource with a matching controller reference. }, }}, Extract: ExternalName(), }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{value2, value}, ResolvedReferences: []xpv2.Reference{{Name: value2}, {Name: value}}, }, err: nil, }, }, "NoSelectorOnlyRefs": { reason: "Refs should not be re-ordered when selector is omitted", c: &test.MockClient{ MockList: test.NewMockListFn(errors.New("unexpected call to List when resolving Refs only")), MockGet: func(_ context.Context, objKey client.ObjectKey, obj client.Object) error { if !strings.HasPrefix(objKey.Name, testResourceNamePrefix) { return errors.New("test resource not found") } val := strings.Replace(objKey.Name, testResourceNamePrefix, testValuePrefix, 1) meta.SetExternalName(obj.(metav1.Object), val) return nil }, }, from: controlled, args: args{ req: MultiResolutionRequest{ References: []xpv2.Reference{testRefs[2], testRefs[3], testRefs[0], testRefs[1]}, To: To{ Managed: &fake.Managed{}, }, Extract: ExternalName(), }, }, want: want{ rsp: MultiResolutionResponse{ ResolvedValues: []string{testValues[2], testValues[3], testValues[0], testValues[1]}, ResolvedReferences: []xpv2.Reference{testRefs[2], testRefs[3], testRefs[0], testRefs[1]}, }, err: nil, }, }, "AlwaysResolveSelector_NewValuesOrdered": { reason: "Must resolve new matches and reorder resolved values & refs, when Selector policy is Always and have existing refs and values", c: &test.MockClient{ MockList: test.NewMockListFn(nil), MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { meta.SetExternalName(obj.(metav1.Object), value) return nil }), }, from: controlled, args: args{ req: MultiResolutionRequest{ CurrentValues: []string{testValues[1], testValues[4]}, References: []xpv2.Reference{testRefs[1], testRefs[4]}, Selector: &xpv2.Selector{ MatchControllerRef: func() *bool { t := true; return &t }(), Policy: &xpv2.Policy{Resolve: &alwaysPolicy}, }, To: To{ Managed: &fake.Managed{}, List: &FakeManagedList{ // List result is not ordered Items: []resource.Managed{ &fake.Managed{}, // A resource that does not match. testControlled[2], // A resource with a matching controller reference. &fake.Managed{}, // A resource that does not match. testControlled[4], // A resource with a matching controller reference. testControlled[1], // A resource with a matching controller reference and newly introduced }, }, }, Extract: ExternalName(), }, }, want: want{ rsp: MultiResolutionResponse{ // expect ordered resolved values ResolvedValues: []string{testValues[1], testValues[2], testValues[4]}, ResolvedReferences: []xpv2.Reference{testRefs[1], testRefs[2], testRefs[4]}, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r := NewAPIResolver(tc.c, tc.from) got, err := r.ResolveMultiple(tc.args.ctx, tc.args.req) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nControllersMustMatch(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.rsp, got, cmpopts.EquateEmpty()); diff != "" { t.Errorf("\n%s\nControllersMustMatch(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestControllersMustMatch(t *testing.T) { cases := map[string]struct { s *xpv2.Selector want bool }{ "NilSelector": { s: nil, want: false, }, "NilMatchControllerRef": { s: &xpv2.Selector{}, want: false, }, "False": { s: &xpv2.Selector{MatchControllerRef: func() *bool { f := false; return &f }()}, want: false, }, "True": { s: &xpv2.Selector{MatchControllerRef: func() *bool { t := true; return &t }()}, want: true, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := ControllersMustMatch(tc.s) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("ControllersMustMatch(...): -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/resource/api.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "context" "encoding/json" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" ) // Error strings. const ( errUpdateObject = "cannot update object" ) // An APIPatchingApplicator applies changes to an object by either creating or // patching it in a Kubernetes API server. type APIPatchingApplicator struct { client client.Client } // NewAPIPatchingApplicator returns an Applicator that applies changes to an // object by either creating or patching it in a Kubernetes API server. func NewAPIPatchingApplicator(c client.Client) *APIPatchingApplicator { return &APIPatchingApplicator{client: c} } // Apply changes to the supplied object. The object will be created if it does // not exist, or patched if it does. If the object does exist, it will only be // patched if the passed object has the same or an empty resource version. func (a *APIPatchingApplicator) Apply(ctx context.Context, o client.Object, ao ...ApplyOption) error { m, ok := o.(metav1.Object) if !ok { return errors.New("cannot access object metadata") } if m.GetName() == "" && m.GetGenerateName() != "" { return errors.Wrap(a.client.Create(ctx, o), "cannot create object") } desired := o.DeepCopyObject() err := a.client.Get(ctx, types.NamespacedName{Name: m.GetName(), Namespace: m.GetNamespace()}, o) if kerrors.IsNotFound(err) { // TODO(negz): Apply ApplyOptions here too? return errors.Wrap(a.client.Create(ctx, o), "cannot create object") } if err != nil { return errors.Wrap(err, "cannot get object") } for _, fn := range ao { if err := fn(ctx, o, desired); err != nil { return err } } // TODO(negz): Allow callers to override the kind of patch used. return errors.Wrap(a.client.Patch(ctx, o, &patch{desired}), "cannot patch object") } type patch struct{ from runtime.Object } func (p *patch) Type() types.PatchType { return types.MergePatchType } func (p *patch) Data(_ client.Object) ([]byte, error) { return json.Marshal(p.from) } // An APIUpdatingApplicator applies changes to an object by either creating or // updating it in a Kubernetes API server. type APIUpdatingApplicator struct { client client.Client } // NewAPIUpdatingApplicator returns an Applicator that applies changes to an // object by either creating or updating it in a Kubernetes API server. func NewAPIUpdatingApplicator(c client.Client) *APIUpdatingApplicator { return &APIUpdatingApplicator{client: c} } // Apply changes to the supplied object. The object will be created if it does // not exist, or updated if it does. func (a *APIUpdatingApplicator) Apply(ctx context.Context, o client.Object, ao ...ApplyOption) error { m, ok := o.(Object) if !ok { return errors.New("cannot access object metadata") } if m.GetName() == "" && m.GetGenerateName() != "" { return errors.Wrap(a.client.Create(ctx, o), "cannot create object") } //nolint:forcetypeassert // Will always be a client.Object. current := o.DeepCopyObject().(client.Object) err := a.client.Get(ctx, types.NamespacedName{Name: m.GetName(), Namespace: m.GetNamespace()}, current) if kerrors.IsNotFound(err) { // TODO(negz): Apply ApplyOptions here too? return errors.Wrap(a.client.Create(ctx, m), "cannot create object") } if err != nil { return errors.Wrap(err, "cannot get object") } for _, fn := range ao { if err := fn(ctx, current, m); err != nil { return err } } // NOTE(hasheddan): we must set the resource version of the desired object // to that of the current or the update will always fail. m.SetResourceVersion(current.GetResourceVersion()) return errors.Wrap(a.client.Update(ctx, m), "cannot update object") } // An APIFinalizer adds and removes finalizers to and from a resource. type APIFinalizer struct { client client.Client finalizer string } // NewNopFinalizer returns a Finalizer that does nothing. func NewNopFinalizer() Finalizer { return nopFinalizer{} } type nopFinalizer struct{} func (f nopFinalizer) AddFinalizer(_ context.Context, _ Object) error { return nil } func (f nopFinalizer) RemoveFinalizer(_ context.Context, _ Object) error { return nil } // NewAPIFinalizer returns a new APIFinalizer. func NewAPIFinalizer(c client.Client, finalizer string) *APIFinalizer { return &APIFinalizer{client: c, finalizer: finalizer} } // AddFinalizer to the supplied Managed resource. func (a *APIFinalizer) AddFinalizer(ctx context.Context, obj Object) error { if meta.FinalizerExists(obj, a.finalizer) { return nil } meta.AddFinalizer(obj, a.finalizer) return errors.Wrap(a.client.Update(ctx, obj), errUpdateObject) } // RemoveFinalizer from the supplied Managed resource. func (a *APIFinalizer) RemoveFinalizer(ctx context.Context, obj Object) error { if !meta.FinalizerExists(obj, a.finalizer) { return nil } meta.RemoveFinalizer(obj, a.finalizer) return errors.Wrap(IgnoreNotFound(a.client.Update(ctx, obj)), errUpdateObject) } // A FinalizerFns satisfy the Finalizer interface. type FinalizerFns struct { AddFinalizerFn func(ctx context.Context, obj Object) error RemoveFinalizerFn func(ctx context.Context, obj Object) error } // AddFinalizer to the supplied resource. func (f FinalizerFns) AddFinalizer(ctx context.Context, obj Object) error { return f.AddFinalizerFn(ctx, obj) } // RemoveFinalizer from the supplied resource. func (f FinalizerFns) RemoveFinalizer(ctx context.Context, obj Object) error { return f.RemoveFinalizerFn(ctx, obj) } ================================================ FILE: pkg/resource/api_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "context" "testing" "github.com/google/go-cmp/cmp" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestAPIPatchingApplicator(t *testing.T) { errBoom := errors.New("boom") desired := &object{} desired.SetName("desired") type args struct { ctx context.Context o client.Object ao []ApplyOption } type want struct { o client.Object err error } cases := map[string]struct { reason string c client.Client args args want want }{ "GetError": { reason: "An error should be returned if we can't get the object", c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)}, args: args{ o: &object{}, }, want: want{ o: &object{}, err: errors.Wrap(errBoom, "cannot get object"), }, }, "CreateError": { reason: "No error should be returned if we successfully create a new object", c: &test.MockClient{ MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")), MockCreate: test.NewMockCreateFn(errBoom), }, args: args{ o: &object{}, }, want: want{ o: &object{}, err: errors.Wrap(errBoom, "cannot create object"), }, }, "ApplyOptionError": { reason: "Any errors from an apply option should be returned", c: &test.MockClient{MockGet: test.NewMockGetFn(nil)}, args: args{ o: &object{}, ao: []ApplyOption{func(_ context.Context, _, _ runtime.Object) error { return errBoom }}, }, want: want{ o: &object{}, err: errBoom, }, }, "PatchError": { reason: "An error should be returned if we can't patch the object", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), MockPatch: test.NewMockPatchFn(errBoom), }, args: args{ o: &object{}, }, want: want{ o: &object{}, err: errors.Wrap(errBoom, "cannot patch object"), }, }, "Created": { reason: "No error should be returned if we successfully create a new object", c: &test.MockClient{ MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")), MockCreate: test.NewMockCreateFn(nil, func(o client.Object) error { *o.(*object) = *desired return nil }), }, args: args{ o: desired, }, want: want{ o: desired, }, }, "Patched": { reason: "No error should be returned if we successfully patch an existing object", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), MockPatch: test.NewMockPatchFn(nil, func(o client.Object) error { *o.(*object) = *desired return nil }), }, args: args{ o: desired, }, want: want{ o: desired, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { a := NewAPIPatchingApplicator(tc.c) err := a.Apply(tc.args.ctx, tc.args.o, tc.args.ao...) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nApply(...): -want error, +got error\n%s\n", tc.reason, diff) } if diff := cmp.Diff(tc.want.o, tc.args.o); diff != "" { t.Errorf("\n%s\nApply(...): -want, +got\n%s\n", tc.reason, diff) } }) } } func TestAPIUpdatingApplicator(t *testing.T) { errBoom := errors.New("boom") desired := &object{} desired.SetName("desired") current := &object{} current.SetName("current") type args struct { ctx context.Context o client.Object ao []ApplyOption } type want struct { o client.Object err error } cases := map[string]struct { reason string c client.Client args args want want }{ "GetError": { reason: "An error should be returned if we can't get the object", c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)}, args: args{ o: &object{}, }, want: want{ o: &object{}, err: errors.Wrap(errBoom, "cannot get object"), }, }, "CreateError": { reason: "No error should be returned if we successfully create a new object", c: &test.MockClient{ MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")), MockCreate: test.NewMockCreateFn(errBoom), }, args: args{ o: &object{}, }, want: want{ o: &object{}, err: errors.Wrap(errBoom, "cannot create object"), }, }, "ApplyOptionError": { reason: "Any errors from an apply option should be returned", c: &test.MockClient{MockGet: test.NewMockGetFn(nil)}, args: args{ o: &object{}, ao: []ApplyOption{func(_ context.Context, _, _ runtime.Object) error { return errBoom }}, }, want: want{ o: &object{}, err: errBoom, }, }, "UpdateError": { reason: "An error should be returned if we can't update the object", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil), MockUpdate: test.NewMockUpdateFn(errBoom), }, args: args{ o: &object{}, }, want: want{ o: &object{}, err: errors.Wrap(errBoom, "cannot update object"), }, }, "Created": { reason: "No error should be returned if we successfully create a new object", c: &test.MockClient{ MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")), MockCreate: test.NewMockCreateFn(nil, func(o client.Object) error { *o.(*object) = *desired return nil }), }, args: args{ o: desired, }, want: want{ o: desired, }, }, "Updated": { reason: "No error should be returned if we successfully update an existing object. If no ApplyOption is passed the existing should not be modified", c: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(o client.Object) error { *o.(*object) = *current return nil }), MockUpdate: test.NewMockUpdateFn(nil, func(o client.Object) error { if diff := cmp.Diff(*desired, *o.(*object)); diff != "" { t.Errorf("r: -want, +got:\n%s", diff) } return nil }), }, args: args{ o: desired, }, want: want{ o: desired, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { a := NewAPIUpdatingApplicator(tc.c) err := a.Apply(tc.args.ctx, tc.args.o, tc.args.ao...) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nApply(...): -want error, +got error\n%s\n", tc.reason, diff) } if diff := cmp.Diff(tc.want.o, tc.args.o); diff != "" { t.Errorf("\n%s\nApply(...): -want, +got\n%s\n", tc.reason, diff) } }) } } func TestManagedRemoveFinalizer(t *testing.T) { finalizer := "veryfinal" type args struct { ctx context.Context obj Object } type want struct { err error obj Object } errBoom := errors.New("boom") cases := map[string]struct { client client.Client args args want want }{ "UpdateError": { client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(errBoom)}, args: args{ ctx: context.Background(), obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{finalizer}}}, }, want: want{ err: errors.Wrap(errBoom, errUpdateObject), obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{}}}, }, }, "Successful": { client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)}, args: args{ ctx: context.Background(), obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{finalizer}}}, }, want: want{ err: nil, obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{}}}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { api := NewAPIFinalizer(tc.client, finalizer) err := api.RemoveFinalizer(tc.args.ctx, tc.args.obj) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("api.RemoveFinalizer(...): -want error, +got error:\n%s", diff) } if diff := cmp.Diff(tc.want.obj, tc.args.obj, test.EquateConditions()); diff != "" { t.Errorf("api.RemoveFinalizer(...) Managed: -want, +got:\n%s", diff) } }) } } func TestAPIFinalizerAdder(t *testing.T) { finalizer := "veryfinal" type args struct { ctx context.Context obj Object } type want struct { err error obj Object } errBoom := errors.New("boom") cases := map[string]struct { client client.Client args args want want }{ "UpdateError": { client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(errBoom)}, args: args{ ctx: context.Background(), obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{}}}, }, want: want{ err: errors.Wrap(errBoom, errUpdateObject), obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{finalizer}}}, }, }, "Successful": { client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)}, args: args{ ctx: context.Background(), obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{}}}, }, want: want{ err: nil, obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{finalizer}}}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { api := NewAPIFinalizer(tc.client, finalizer) err := api.AddFinalizer(tc.args.ctx, tc.args.obj) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("api.Initialize(...): -want error, +got error:\n%s", diff) } if diff := cmp.Diff(tc.want.obj, tc.args.obj, test.EquateConditions()); diff != "" { t.Errorf("api.Initialize(...) Managed: -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/resource/doc.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package resource provides types and functions that can be used to build // Kubernetes controllers that reconcile Crossplane resources. package resource ================================================ FILE: pkg/resource/enqueue_handlers.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "context" "strings" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) type adder interface { Add(item reconcile.Request) } // RateLimitingInterface for an EnqueueRequestForProviderConfig. type RateLimitingInterface = workqueue.TypedRateLimitingInterface[reconcile.Request] // EnqueueRequestForProviderConfig enqueues a reconcile.Request for a referenced // ProviderConfig. type EnqueueRequestForProviderConfig struct { // Kind is the expected ProviderConfig kind this handler should process. // If empty, all kinds are processed (backward compatibility). Kind string } // Create adds a NamespacedName for the supplied CreateEvent if its Object is a // ProviderConfigReferencer. func (e *EnqueueRequestForProviderConfig) Create(_ context.Context, evt event.CreateEvent, q RateLimitingInterface) { e.addProviderConfig(evt.Object, q) } // Update adds a NamespacedName for the supplied UpdateEvent if its Objects are // a ProviderConfigReferencer. func (e *EnqueueRequestForProviderConfig) Update(_ context.Context, evt event.UpdateEvent, q RateLimitingInterface) { e.addProviderConfig(evt.ObjectOld, q) e.addProviderConfig(evt.ObjectNew, q) } // Delete adds a NamespacedName for the supplied DeleteEvent if its Object is a // ProviderConfigReferencer. func (e *EnqueueRequestForProviderConfig) Delete(_ context.Context, evt event.DeleteEvent, q RateLimitingInterface) { e.addProviderConfig(evt.Object, q) } // Generic adds a NamespacedName for the supplied GenericEvent if its Object is // a ProviderConfigReferencer. func (e *EnqueueRequestForProviderConfig) Generic(_ context.Context, evt event.GenericEvent, q RateLimitingInterface) { e.addProviderConfig(evt.Object, q) } func (e *EnqueueRequestForProviderConfig) addProviderConfig(obj runtime.Object, queue adder) { switch pcr := obj.(type) { case TypedProviderConfigUsage: ref := pcr.GetProviderConfigReference() refKind := ref.Kind if refKind == "" { refKind = "ProviderConfig" } if e.Kind != "" && refKind != e.Kind { return } if strings.HasPrefix(refKind, "Cluster") { queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: ref.Name}}) } else { queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: ref.Name, Namespace: pcr.GetNamespace()}}) } case LegacyProviderConfigUsage: queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: pcr.GetProviderConfigReference().Name}}) } } ================================================ FILE: pkg/resource/enqueue_handlers_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "testing" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" ) var _ handler.EventHandler = &EnqueueRequestForProviderConfig{} type addFn func(item any) func (fn addFn) Add(item reconcile.Request) { fn(item) } func TestAddProviderConfig(t *testing.T) { name := "coolname" cases := map[string]struct { handler *EnqueueRequestForProviderConfig obj runtime.Object queue adder }{ "NotProviderConfigReferencer": { handler: &EnqueueRequestForProviderConfig{}, queue: addFn(func(_ any) { t.Errorf("queue.Add() called unexpectedly") }), }, "IsLegacyProviderConfigReferencer": { handler: &EnqueueRequestForProviderConfig{}, obj: &fake.LegacyProviderConfigUsage{ RequiredProviderConfigReferencer: fake.RequiredProviderConfigReferencer{ Ref: xpv2.Reference{Name: name}, }, }, queue: addFn(func(got any) { want := reconcile.Request{NamespacedName: types.NamespacedName{Name: name}} if diff := cmp.Diff(want, got); diff != "" { t.Errorf("-want, +got:\n%s", diff) } }), }, "IsTypedProviderConfigReferencer": { handler: &EnqueueRequestForProviderConfig{}, obj: &fake.ProviderConfigUsage{ ObjectMeta: metav1.ObjectMeta{ Name: "some-pcu", Namespace: "foo", }, RequiredTypedProviderConfigReferencer: fake.RequiredTypedProviderConfigReferencer{ Ref: xpv2.ProviderConfigReference{Name: name, Kind: "ProviderConfig"}, }, }, queue: addFn(func(got any) { want := reconcile.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: "foo"}} if diff := cmp.Diff(want, got); diff != "" { t.Errorf("-want, +got:\n%s", diff) } }), }, "ClusterScopedProviderConfigOmitsNamespace": { handler: &EnqueueRequestForProviderConfig{Kind: "ClusterProviderConfig"}, obj: &fake.ProviderConfigUsage{ ObjectMeta: metav1.ObjectMeta{ Name: "some-pcu", Namespace: "foo", }, RequiredTypedProviderConfigReferencer: fake.RequiredTypedProviderConfigReferencer{ Ref: xpv2.ProviderConfigReference{Name: name, Kind: "ClusterProviderConfig"}, }, }, queue: addFn(func(got any) { want := reconcile.Request{NamespacedName: types.NamespacedName{Name: name}} if diff := cmp.Diff(want, got); diff != "" { t.Errorf("-want, +got:\n%s", diff) } }), }, "KindFilterMatchesKind": { handler: &EnqueueRequestForProviderConfig{Kind: "ProviderConfig"}, obj: &fake.ProviderConfigUsage{ ObjectMeta: metav1.ObjectMeta{ Name: "some-pcu", Namespace: "bar", }, RequiredTypedProviderConfigReferencer: fake.RequiredTypedProviderConfigReferencer{ Ref: xpv2.ProviderConfigReference{Name: name, Kind: "ProviderConfig"}, }, }, queue: addFn(func(got any) { want := reconcile.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: "bar"}} if diff := cmp.Diff(want, got); diff != "" { t.Errorf("-want, +got:\n%s", diff) } }), }, "KindFilterSkipsNonMatchingKind": { handler: &EnqueueRequestForProviderConfig{Kind: "ProviderConfig"}, obj: &fake.ProviderConfigUsage{ ObjectMeta: metav1.ObjectMeta{ Name: "some-pcu", Namespace: "bar", }, RequiredTypedProviderConfigReferencer: fake.RequiredTypedProviderConfigReferencer{ Ref: xpv2.ProviderConfigReference{Name: name, Kind: "OtherProviderConfig"}, }, }, queue: addFn(func(_ any) { t.Errorf("queue.Add() called unexpectedly for non-matching kind") }), }, "EmptyRefKindDefaultsToProviderConfig": { handler: &EnqueueRequestForProviderConfig{Kind: "ProviderConfig"}, obj: &fake.ProviderConfigUsage{ ObjectMeta: metav1.ObjectMeta{ Name: "some-pcu", Namespace: "baz", }, RequiredTypedProviderConfigReferencer: fake.RequiredTypedProviderConfigReferencer{ Ref: xpv2.ProviderConfigReference{Name: name, Kind: ""}, }, }, queue: addFn(func(got any) { want := reconcile.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: "baz"}} if diff := cmp.Diff(want, got); diff != "" { t.Errorf("-want, +got:\n%s", diff) } }), }, } for name, tc := range cases { t.Run(name, func(_ *testing.T) { tc.handler.addProviderConfig(tc.obj, tc.queue) }) } } ================================================ FILE: pkg/resource/fake/mocks.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package fake provides fake Crossplane resources for use in tests. // //nolint:musttag // We only use JSON to round-trip convert these mocks. package fake import ( "encoding/json" "reflect" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/reference" ) // Conditioned is a mock that implements Conditioned interface. type Conditioned struct{ Conditions []xpv2.Condition } // SetConditions sets the Conditions. func (m *Conditioned) SetConditions(c ...xpv2.Condition) { m.Conditions = c } // GetCondition get the Condition with the given ConditionType. func (m *Conditioned) GetCondition(ct xpv2.ConditionType) xpv2.Condition { return xpv2.Condition{Type: ct, Status: corev1.ConditionUnknown} } // ClaimReferencer is a mock that implements ClaimReferencer interface. type ClaimReferencer struct{ Ref *reference.Claim } // SetClaimReference sets the ClaimReference. func (m *ClaimReferencer) SetClaimReference(r *reference.Claim) { m.Ref = r } // GetClaimReference gets the ClaimReference. func (m *ClaimReferencer) GetClaimReference() *reference.Claim { return m.Ref } // ManagedResourceReferencer is a mock that implements ManagedResourceReferencer interface. type ManagedResourceReferencer struct{ Ref *corev1.ObjectReference } // SetResourceReference sets the ResourceReference. func (m *ManagedResourceReferencer) SetResourceReference(r *corev1.ObjectReference) { m.Ref = r } // GetResourceReference gets the ResourceReference. func (m *ManagedResourceReferencer) GetResourceReference() *corev1.ObjectReference { return m.Ref } // TypedProviderConfigReferencer is a mock that implements resource.TypedProviderConfigReferencer interface. type TypedProviderConfigReferencer struct{ Ref *xpv2.ProviderConfigReference } // SetProviderConfigReference sets the ProviderConfigReference. func (m *TypedProviderConfigReferencer) SetProviderConfigReference(p *xpv2.ProviderConfigReference) { m.Ref = p } // GetProviderConfigReference gets the ProviderConfigReference. func (m *TypedProviderConfigReferencer) GetProviderConfigReference() *xpv2.ProviderConfigReference { return m.Ref } // LegacyProviderConfigReferencer is a mock that implements resource.ProviderConfigReferencer interface. type LegacyProviderConfigReferencer struct{ Ref *xpv2.Reference } // SetProviderConfigReference sets the ProviderConfigReference. func (m *LegacyProviderConfigReferencer) SetProviderConfigReference(p *xpv2.Reference) { m.Ref = p } // GetProviderConfigReference gets the ProviderConfigReference. func (m *LegacyProviderConfigReferencer) GetProviderConfigReference() *xpv2.Reference { return m.Ref } // RequiredProviderConfigReferencer is a mock that implements the // RequiredProviderConfigReferencer interface. type RequiredProviderConfigReferencer struct{ Ref xpv2.Reference } // SetProviderConfigReference sets the ProviderConfigReference. func (m *RequiredProviderConfigReferencer) SetProviderConfigReference(p xpv2.Reference) { m.Ref = p } // GetProviderConfigReference gets the ProviderConfigReference. func (m *RequiredProviderConfigReferencer) GetProviderConfigReference() xpv2.Reference { return m.Ref } // RequiredTypedProviderConfigReferencer is a mock that implements the // RequiredTypedProviderConfigReferencer interface. type RequiredTypedProviderConfigReferencer struct{ Ref xpv2.ProviderConfigReference } // SetProviderConfigReference sets the ProviderConfigReference. func (m *RequiredTypedProviderConfigReferencer) SetProviderConfigReference(p xpv2.ProviderConfigReference) { m.Ref = p } // GetProviderConfigReference gets the ProviderConfigReference. func (m *RequiredTypedProviderConfigReferencer) GetProviderConfigReference() xpv2.ProviderConfigReference { return m.Ref } // RequiredTypedResourceReferencer is a mock that implements the // RequiredTypedResourceReferencer interface. type RequiredTypedResourceReferencer struct{ Ref xpv2.TypedReference } // SetResourceReference sets the ResourceReference. func (m *RequiredTypedResourceReferencer) SetResourceReference(p xpv2.TypedReference) { m.Ref = p } // GetResourceReference gets the ResourceReference. func (m *RequiredTypedResourceReferencer) GetResourceReference() xpv2.TypedReference { return m.Ref } // LocalConnectionSecretWriterTo is a mock that implements LocalConnectionSecretWriterTo interface. type LocalConnectionSecretWriterTo struct { Ref *xpv2.LocalSecretReference } // SetWriteConnectionSecretToReference sets the WriteConnectionSecretToReference. func (m *LocalConnectionSecretWriterTo) SetWriteConnectionSecretToReference(r *xpv2.LocalSecretReference) { m.Ref = r } // GetWriteConnectionSecretToReference gets the WriteConnectionSecretToReference. func (m *LocalConnectionSecretWriterTo) GetWriteConnectionSecretToReference() *xpv2.LocalSecretReference { return m.Ref } // ConnectionSecretWriterTo is a mock that implements ConnectionSecretWriterTo interface. type ConnectionSecretWriterTo struct{ Ref *xpv2.SecretReference } // SetWriteConnectionSecretToReference sets the WriteConnectionSecretToReference. func (m *ConnectionSecretWriterTo) SetWriteConnectionSecretToReference(r *xpv2.SecretReference) { m.Ref = r } // GetWriteConnectionSecretToReference gets the WriteConnectionSecretToReference. func (m *ConnectionSecretWriterTo) GetWriteConnectionSecretToReference() *xpv2.SecretReference { return m.Ref } // Manageable implements the Manageable interface. type Manageable struct{ Policy xpv2.ManagementPolicies } // SetManagementPolicies sets the ManagementPolicies. func (m *Manageable) SetManagementPolicies(p xpv2.ManagementPolicies) { m.Policy = p } // GetManagementPolicies gets the ManagementPolicies. func (m *Manageable) GetManagementPolicies() xpv2.ManagementPolicies { return m.Policy } // Orphanable implements the Orphanable interface. type Orphanable struct{ Policy xpv2.DeletionPolicy } // SetDeletionPolicy sets the DeletionPolicy. func (m *Orphanable) SetDeletionPolicy(p xpv2.DeletionPolicy) { m.Policy = p } // GetDeletionPolicy gets the DeletionPolicy. func (m *Orphanable) GetDeletionPolicy() xpv2.DeletionPolicy { return m.Policy } // CompositionReferencer is a mock that implements CompositionReferencer interface. type CompositionReferencer struct{ Ref *corev1.ObjectReference } // SetCompositionReference sets the CompositionReference. func (m *CompositionReferencer) SetCompositionReference(r *corev1.ObjectReference) { m.Ref = r } // GetCompositionReference gets the CompositionReference. func (m *CompositionReferencer) GetCompositionReference() *corev1.ObjectReference { return m.Ref } // CompositionSelector is a mock that implements CompositionSelector interface. type CompositionSelector struct{ Sel *metav1.LabelSelector } // SetCompositionSelector sets the CompositionSelector. func (m *CompositionSelector) SetCompositionSelector(s *metav1.LabelSelector) { m.Sel = s } // GetCompositionSelector gets the CompositionSelector. func (m *CompositionSelector) GetCompositionSelector() *metav1.LabelSelector { return m.Sel } // CompositionRevisionReferencer is a mock that implements CompositionRevisionReferencer interface. type CompositionRevisionReferencer struct{ Ref *corev1.LocalObjectReference } // SetCompositionRevisionReference sets the CompositionRevisionReference. func (m *CompositionRevisionReferencer) SetCompositionRevisionReference(r *corev1.LocalObjectReference) { m.Ref = r } // GetCompositionRevisionReference gets the CompositionRevisionReference. func (m *CompositionRevisionReferencer) GetCompositionRevisionReference() *corev1.LocalObjectReference { return m.Ref } // CompositionRevisionSelector is a mock that implements CompositionRevisionSelector interface. type CompositionRevisionSelector struct{ Sel *metav1.LabelSelector } // SetCompositionRevisionSelector sets the CompositionRevisionSelector. func (m *CompositionRevisionSelector) SetCompositionRevisionSelector(ls *metav1.LabelSelector) { m.Sel = ls } // GetCompositionRevisionSelector gets the CompositionRevisionSelector. func (m *CompositionRevisionSelector) GetCompositionRevisionSelector() *metav1.LabelSelector { return m.Sel } // CompositionUpdater is a mock that implements CompositionUpdater interface. type CompositionUpdater struct{ Policy *xpv2.UpdatePolicy } // SetCompositionUpdatePolicy sets the CompositionUpdatePolicy. func (m *CompositionUpdater) SetCompositionUpdatePolicy(p *xpv2.UpdatePolicy) { m.Policy = p } // GetCompositionUpdatePolicy gets the CompositionUpdatePolicy. func (m *CompositionUpdater) GetCompositionUpdatePolicy() *xpv2.UpdatePolicy { return m.Policy } // CompositeResourceDeleter is a mock that implements CompositeResourceDeleter interface. type CompositeResourceDeleter struct{ Policy *xpv2.CompositeDeletePolicy } // SetCompositeDeletePolicy sets the CompositeDeletePolicy. func (m *CompositeResourceDeleter) SetCompositeDeletePolicy(p *xpv2.CompositeDeletePolicy) { m.Policy = p } // GetCompositeDeletePolicy gets the CompositeDeletePolicy. func (m *CompositeResourceDeleter) GetCompositeDeletePolicy() *xpv2.CompositeDeletePolicy { return m.Policy } // CompositeResourceReferencer is a mock that implements CompositeResourceReferencer interface. type CompositeResourceReferencer struct{ Ref *reference.Composite } // SetResourceReference sets the composite resource reference. func (m *CompositeResourceReferencer) SetResourceReference(p *reference.Composite) { m.Ref = p } // GetResourceReference gets the composite resource reference. func (m *CompositeResourceReferencer) GetResourceReference() *reference.Composite { return m.Ref } // ComposedResourcesReferencer is a mock that implements ComposedResourcesReferencer interface. type ComposedResourcesReferencer struct{ Refs []corev1.ObjectReference } // SetResourceReferences sets the composed references. func (m *ComposedResourcesReferencer) SetResourceReferences(r []corev1.ObjectReference) { m.Refs = r } // GetResourceReferences gets the composed references. func (m *ComposedResourcesReferencer) GetResourceReferences() []corev1.ObjectReference { return m.Refs } // An EnvironmentConfigReferencer is a mock that implements the // EnvironmentConfigReferencer interface. type EnvironmentConfigReferencer struct{ Refs []corev1.ObjectReference } // SetEnvironmentConfigReferences sets the EnvironmentConfig references. func (m *EnvironmentConfigReferencer) SetEnvironmentConfigReferences(refs []corev1.ObjectReference) { m.Refs = refs } // GetEnvironmentConfigReferences gets the EnvironmentConfig references. func (m *EnvironmentConfigReferencer) GetEnvironmentConfigReferences() []corev1.ObjectReference { return m.Refs } // ConnectionDetailsLastPublishedTimer is a mock that implements the // ConnectionDetailsLastPublishedTimer interface. type ConnectionDetailsLastPublishedTimer struct { // NOTE: runtime.DefaultUnstructuredConverter.ToUnstructured // cannot currently handle if `Time` is nil here. // The `omitempty` json tag is a workaround that // prevents a panic. Time *metav1.Time `json:"lastPublishedTime,omitempty"` } // SetConnectionDetailsLastPublishedTime sets the published time. func (c *ConnectionDetailsLastPublishedTimer) SetConnectionDetailsLastPublishedTime(t *metav1.Time) { c.Time = t } // GetConnectionDetailsLastPublishedTime gets the published time. func (c *ConnectionDetailsLastPublishedTimer) GetConnectionDetailsLastPublishedTime() *metav1.Time { return c.Time } // UserCounter is a mock that satisfies UserCounter // interface. type UserCounter struct{ Users int64 } // SetUsers sets the count of users. func (m *UserCounter) SetUsers(i int64) { m.Users = i } // GetUsers gets the count of users. func (m *UserCounter) GetUsers() int64 { return m.Users } // Object is a mock that implements Object interface. type Object struct { metav1.ObjectMeta runtime.Object } // GetObjectKind returns schema.ObjectKind. func (o *Object) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (o *Object) DeepCopyObject() runtime.Object { out := &Object{} j, err := json.Marshal(o) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // Managed is a mock that implements Managed interface. type Managed struct { metav1.ObjectMeta Manageable xpv2.ConditionedStatus } // GetObjectKind returns schema.ObjectKind. func (m *Managed) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (m *Managed) DeepCopyObject() runtime.Object { out := &Managed{} j, err := json.Marshal(m) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // ModernManaged is a mock that implements ModernManaged interface. type ModernManaged struct { metav1.ObjectMeta TypedProviderConfigReferencer LocalConnectionSecretWriterTo Manageable xpv2.ConditionedStatus xpv2.ObservedStatus } // GetObjectKind returns schema.ObjectKind. func (m *ModernManaged) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (m *ModernManaged) DeepCopyObject() runtime.Object { out := &ModernManaged{} j, err := json.Marshal(m) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // LegacyManaged is a mock that implements LegacyManaged interface. type LegacyManaged struct { metav1.ObjectMeta LegacyProviderConfigReferencer ConnectionSecretWriterTo Manageable Orphanable xpv2.ConditionedStatus } // GetObjectKind returns schema.ObjectKind. func (m *LegacyManaged) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (m *LegacyManaged) DeepCopyObject() runtime.Object { out := &LegacyManaged{} j, err := json.Marshal(m) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // Composite is a mock that implements Composite interface. type Composite struct { metav1.ObjectMeta CompositionSelector CompositionReferencer CompositionRevisionReferencer CompositionRevisionSelector CompositionUpdater ComposedResourcesReferencer EnvironmentConfigReferencer ClaimReferencer ConnectionSecretWriterTo xpv2.ConditionedStatus xpv2.ObservedStatus ConnectionDetailsLastPublishedTimer } // GetObjectKind returns schema.ObjectKind. func (m *Composite) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (m *Composite) DeepCopyObject() runtime.Object { out := &Composite{} j, err := json.Marshal(m) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // Composed is a mock that implements Composed interface. type Composed struct { metav1.ObjectMeta ConnectionSecretWriterTo xpv2.ConditionedStatus xpv2.ObservedStatus } // GetObjectKind returns schema.ObjectKind. func (m *Composed) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (m *Composed) DeepCopyObject() runtime.Object { out := &Composed{} j, err := json.Marshal(m) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // CompositeClaim is a mock that implements the CompositeClaim interface. type CompositeClaim struct { metav1.ObjectMeta CompositionSelector CompositionReferencer CompositionRevisionReferencer CompositionRevisionSelector CompositeResourceDeleter CompositionUpdater CompositeResourceReferencer LocalConnectionSecretWriterTo xpv2.ConditionedStatus xpv2.ObservedStatus ConnectionDetailsLastPublishedTimer } // GetObjectKind returns schema.ObjectKind. func (m *CompositeClaim) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (m *CompositeClaim) DeepCopyObject() runtime.Object { out := &CompositeClaim{} j, err := json.Marshal(m) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // Manager is a mock object that satisfies manager.Manager interface. type Manager struct { manager.Manager Cache cache.Cache Client client.Client Scheme *runtime.Scheme Config *rest.Config RESTMapper meta.RESTMapper Logger logr.Logger } // Elected returns a closed channel. func (m *Manager) Elected() <-chan struct{} { e := make(chan struct{}) close(e) return e } // GetCache returns the cache. func (m *Manager) GetCache() cache.Cache { return m.Cache } // GetClient returns the client. func (m *Manager) GetClient() client.Client { return m.Client } // GetScheme returns the scheme. func (m *Manager) GetScheme() *runtime.Scheme { return m.Scheme } // GetConfig returns the config. func (m *Manager) GetConfig() *rest.Config { return m.Config } // GetRESTMapper returns the REST mapper. func (m *Manager) GetRESTMapper() meta.RESTMapper { return m.RESTMapper } // GetLogger returns the logger. func (m *Manager) GetLogger() logr.Logger { return m.Logger } // GV returns a mock schema.GroupVersion. var GV = schema.GroupVersion{Group: "g", Version: "v"} //nolint:gochecknoglobals // We treat this as a constant. // GVK returns the mock GVK of the given object. func GVK(o runtime.Object) schema.GroupVersionKind { return GV.WithKind(reflect.TypeOf(o).Elem().Name()) } // SchemeWith returns a scheme with list of `runtime.Object`s registered. func SchemeWith(o ...runtime.Object) *runtime.Scheme { s := runtime.NewScheme() s.AddKnownTypes(GV, o...) return s } // MockConnectionSecretOwner is a mock object that satisfies ConnectionSecretOwner // interface. type MockConnectionSecretOwner struct { runtime.Object metav1.ObjectMeta WriterTo *xpv2.SecretReference } // GetWriteConnectionSecretToReference returns the connection secret reference. func (m *MockConnectionSecretOwner) GetWriteConnectionSecretToReference() *xpv2.SecretReference { return m.WriterTo } // SetWriteConnectionSecretToReference sets the connection secret reference. func (m *MockConnectionSecretOwner) SetWriteConnectionSecretToReference(r *xpv2.SecretReference) { m.WriterTo = r } // GetObjectKind returns schema.ObjectKind. func (m *MockConnectionSecretOwner) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (m *MockConnectionSecretOwner) DeepCopyObject() runtime.Object { out := &MockConnectionSecretOwner{} j, err := json.Marshal(m) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // MockLocalConnectionSecretOwner is a mock object that satisfies LocalConnectionSecretOwner // interface. type MockLocalConnectionSecretOwner struct { runtime.Object metav1.ObjectMeta Ref *xpv2.LocalSecretReference } // GetWriteConnectionSecretToReference returns the connection secret reference. func (m *MockLocalConnectionSecretOwner) GetWriteConnectionSecretToReference() *xpv2.LocalSecretReference { return m.Ref } // SetWriteConnectionSecretToReference sets the connection secret reference. func (m *MockLocalConnectionSecretOwner) SetWriteConnectionSecretToReference(r *xpv2.LocalSecretReference) { m.Ref = r } // GetObjectKind returns schema.ObjectKind. func (m *MockLocalConnectionSecretOwner) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (m *MockLocalConnectionSecretOwner) DeepCopyObject() runtime.Object { out := &MockLocalConnectionSecretOwner{} j, err := json.Marshal(m) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // ProviderConfig is a mock implementation of the ProviderConfig interface. type ProviderConfig struct { metav1.ObjectMeta UserCounter xpv2.ConditionedStatus } // GetObjectKind returns schema.ObjectKind. func (p *ProviderConfig) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (p *ProviderConfig) DeepCopyObject() runtime.Object { out := &ProviderConfig{} j, err := json.Marshal(p) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // ProviderConfigUsage is a mock implementation of the ProviderConfigUsage // interface. type ProviderConfigUsage struct { metav1.ObjectMeta RequiredTypedProviderConfigReferencer RequiredTypedResourceReferencer } // GetObjectKind returns schema.ObjectKind. func (p *ProviderConfigUsage) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (p *ProviderConfigUsage) DeepCopyObject() runtime.Object { out := &ProviderConfigUsage{} j, err := json.Marshal(p) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } // LegacyProviderConfigUsage is a mock implementation of the LegacyProviderConfigUsage // interface. type LegacyProviderConfigUsage struct { metav1.ObjectMeta RequiredProviderConfigReferencer RequiredTypedResourceReferencer } // GetObjectKind returns schema.ObjectKind. func (p *LegacyProviderConfigUsage) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } // DeepCopyObject returns a copy of the object as runtime.Object. func (p *LegacyProviderConfigUsage) DeepCopyObject() runtime.Object { out := &LegacyProviderConfigUsage{} j, err := json.Marshal(p) if err != nil { panic(err) } _ = json.Unmarshal(j, out) return out } ================================================ FILE: pkg/resource/interfaces.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "context" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/reference" ) // A Conditioned may have conditions set or retrieved. Conditions are typically // indicate the status of both a resource and its reconciliation process. type Conditioned interface { SetConditions(c ...xpv2.Condition) GetCondition(ct xpv2.ConditionType) xpv2.Condition } // A ClaimReferencer may reference a resource claim. type ClaimReferencer interface { SetClaimReference(r *reference.Claim) GetClaimReference() *reference.Claim } // A ManagedResourceReferencer may reference a concrete managed resource. type ManagedResourceReferencer interface { SetResourceReference(r *corev1.ObjectReference) GetResourceReference() *corev1.ObjectReference } // A LocalConnectionSecretWriterTo may write a connection secret to its own // namespace. type LocalConnectionSecretWriterTo interface { SetWriteConnectionSecretToReference(r *xpv2.LocalSecretReference) GetWriteConnectionSecretToReference() *xpv2.LocalSecretReference } // A ConnectionSecretWriterTo may write a connection secret to an arbitrary // namespace. type ConnectionSecretWriterTo interface { SetWriteConnectionSecretToReference(r *xpv2.SecretReference) GetWriteConnectionSecretToReference() *xpv2.SecretReference } // A Manageable resource may specify a ManagementPolicies. type Manageable interface { SetManagementPolicies(p xpv2.ManagementPolicies) GetManagementPolicies() xpv2.ManagementPolicies } // An Orphanable resource may specify a DeletionPolicy. type Orphanable interface { SetDeletionPolicy(p xpv2.DeletionPolicy) GetDeletionPolicy() xpv2.DeletionPolicy } // A ProviderConfigReferencer may reference a provider config resource. type ProviderConfigReferencer interface { GetProviderConfigReference() *xpv2.Reference SetProviderConfigReference(p *xpv2.Reference) } // A TypedProviderConfigReferencer may reference a provider config resource // with its kind. type TypedProviderConfigReferencer interface { GetProviderConfigReference() *xpv2.ProviderConfigReference SetProviderConfigReference(p *xpv2.ProviderConfigReference) } // A RequiredProviderConfigReferencer may reference a provider config resource. // Unlike ProviderConfigReferencer, the reference is required (i.e. not nil). type RequiredProviderConfigReferencer interface { GetProviderConfigReference() xpv2.Reference SetProviderConfigReference(p xpv2.Reference) } // A RequiredTypedProviderConfigReferencer may reference a provider config resource. // Unlike TypedProviderConfigReferencer, the reference is required (i.e. not nil). type RequiredTypedProviderConfigReferencer interface { GetProviderConfigReference() xpv2.ProviderConfigReference SetProviderConfigReference(p xpv2.ProviderConfigReference) } // A RequiredTypedResourceReferencer can reference a resource. type RequiredTypedResourceReferencer interface { SetResourceReference(r xpv2.TypedReference) GetResourceReference() xpv2.TypedReference } // A Finalizer manages the finalizers on the resource. type Finalizer interface { AddFinalizer(ctx context.Context, obj Object) error RemoveFinalizer(ctx context.Context, obj Object) error } // A CompositionSelector may select a composition of resources. type CompositionSelector interface { SetCompositionSelector(s *metav1.LabelSelector) GetCompositionSelector() *metav1.LabelSelector } // A CompositionReferencer may reference a composition of resources. type CompositionReferencer interface { SetCompositionReference(ref *corev1.ObjectReference) GetCompositionReference() *corev1.ObjectReference } // A CompositionRevisionReferencer may reference a specific revision of a // composition of resources. type CompositionRevisionReferencer interface { SetCompositionRevisionReference(ref *corev1.LocalObjectReference) GetCompositionRevisionReference() *corev1.LocalObjectReference } // A CompositionRevisionSelector may reference a set of // composition revisions. type CompositionRevisionSelector interface { SetCompositionRevisionSelector(selector *metav1.LabelSelector) GetCompositionRevisionSelector() *metav1.LabelSelector } // A CompositionUpdater uses a composition, and may update which revision of // that composition it uses. type CompositionUpdater interface { SetCompositionUpdatePolicy(p *xpv2.UpdatePolicy) GetCompositionUpdatePolicy() *xpv2.UpdatePolicy } // A CompositeResourceDeleter creates a composite, and controls the policy // used to delete the composite. type CompositeResourceDeleter interface { SetCompositeDeletePolicy(policy *xpv2.CompositeDeletePolicy) GetCompositeDeletePolicy() *xpv2.CompositeDeletePolicy } // A ComposedResourcesReferencer may reference the resources it composes. type ComposedResourcesReferencer interface { SetResourceReferences(refs []corev1.ObjectReference) GetResourceReferences() []corev1.ObjectReference } // A CompositeResourceReferencer can reference a composite resource. type CompositeResourceReferencer interface { SetResourceReference(r *reference.Composite) GetResourceReference() *reference.Composite } // An EnvironmentConfigReferencer references a list of EnvironmentConfigs. type EnvironmentConfigReferencer interface { SetEnvironmentConfigReferences(refs []corev1.ObjectReference) GetEnvironmentConfigReferences() []corev1.ObjectReference } // A UserCounter can count how many users it has. type UserCounter interface { SetUsers(i int64) GetUsers() int64 } // A ConnectionDetailsPublishedTimer can record the last time its connection // details were published. type ConnectionDetailsPublishedTimer interface { SetConnectionDetailsLastPublishedTime(t *metav1.Time) GetConnectionDetailsLastPublishedTime() *metav1.Time } // ReconciliationObserver can track data observed by resource reconciler. type ReconciliationObserver interface { SetObservedGeneration(generation int64) GetObservedGeneration() int64 } // An Object is a Kubernetes object. type Object interface { metav1.Object runtime.Object } // A Managed is a Kubernetes object representing a concrete managed // resource (e.g. a CloudSQL instance). type Managed interface { Object Manageable Conditioned } // A ModernManaged is a Kubernetes object representing a concrete managed // resource with local connection secret references and typed provider // config reference. type ModernManaged interface { Managed LocalConnectionSecretWriterTo TypedProviderConfigReferencer } // A LegacyManaged is a cluster-scoped Kubernetes object representing a // concrete managed resource, with namespaced connection secret referencers // and untyped provider config reference. // // Deprecated: new namespace-scoped MRs should implement ModernManaged. type LegacyManaged interface { Managed ConnectionSecretWriterTo ProviderConfigReferencer Orphanable } // A ManagedList is a list of managed resources. type ManagedList interface { client.ObjectList // GetItems returns the list of managed resources. GetItems() []Managed } // A LegacyManagedList is a list of managed resources. // // Deprecated: new types should implement ManagedList. type LegacyManagedList interface { client.ObjectList // GetItems returns the list of managed resources. GetItems() []LegacyManaged } // A ProviderConfig configures a Crossplane provider. type ProviderConfig interface { Object UserCounter Conditioned } // A ProviderConfigUsage indicates a usage of a Crossplane provider config. type ProviderConfigUsage interface { Object RequiredTypedResourceReferencer } // A TypedProviderConfigUsage is a ProviderConfigUsage that // has a typed reference to the ProviderConfig. type TypedProviderConfigUsage interface { ProviderConfigUsage RequiredTypedProviderConfigReferencer } // A LegacyProviderConfigUsage is a ProviderConfigUsage that // has an untyped reference to a provider config. // // Deprecated: new PCUs should implement TypedProviderConfigUsage. type LegacyProviderConfigUsage interface { ProviderConfigUsage RequiredProviderConfigReferencer } // A ProviderConfigUsageList is a list of provider config usages. type ProviderConfigUsageList interface { client.ObjectList // GetItems returns the list of provider config usages. GetItems() []ProviderConfigUsage } // A Composite resource (or XR) is composed of other resources. type Composite interface { //nolint:interfacebloat // This interface has to be big. Object CompositionSelector CompositionReferencer CompositionUpdater CompositionRevisionReferencer CompositionRevisionSelector ComposedResourcesReferencer Conditioned ReconciliationObserver } // A LegacyComposite is a Crossplane v1 style legacy XR. type LegacyComposite interface { Composite ClaimReferencer ConnectionSecretWriterTo ConnectionDetailsPublishedTimer } // Composed resources can be a composed into a Composite resource. type Composed interface { Object Conditioned ConnectionSecretWriterTo ReconciliationObserver } // A CompositeClaim of a composite resource (XR). type CompositeClaim interface { //nolint:interfacebloat // This interface has to be big. Object CompositionSelector CompositionReferencer CompositionUpdater CompositionRevisionReferencer CompositionRevisionSelector CompositeResourceDeleter CompositeResourceReferencer LocalConnectionSecretWriterTo Conditioned ConnectionDetailsPublishedTimer ReconciliationObserver } // A Claim of a composite resource (XR). type Claim = CompositeClaim ================================================ FILE: pkg/resource/interfaces_test.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/claim" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/composed" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/composite" ) // We test that our fakes satisfy our interfaces here rather than in the fake // package to avoid a cyclic dependency. var ( _ Managed = &fake.Managed{} _ ProviderConfig = &fake.ProviderConfig{} _ ProviderConfigUsage = &fake.ProviderConfigUsage{} _ ProviderConfigUsage = &fake.LegacyProviderConfigUsage{} _ ModernManaged = &fake.ModernManaged{} _ TypedProviderConfigUsage = &fake.ProviderConfigUsage{} _ LegacyManaged = &fake.LegacyManaged{} _ LegacyProviderConfigUsage = &fake.LegacyProviderConfigUsage{} _ CompositeClaim = &fake.CompositeClaim{} _ Composite = &fake.Composite{} _ Composed = &fake.Composed{} _ CompositeClaim = &claim.Unstructured{} _ Composite = &composite.Unstructured{} _ Composed = &composed.Unstructured{} ) ================================================ FILE: pkg/resource/late_initializer.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // NewLateInitializer returns a new instance of *LateInitializer. func NewLateInitializer() *LateInitializer { return &LateInitializer{} } // LateInitializer contains functions to late initialize two fields with varying // types. The main purpose of LateInitializer is to be able to report whether // anything different from the original value has been returned after all late // initialization calls. type LateInitializer struct { changed bool } // IsChanged reports whether the second argument is ever used in late initialization // function calls. func (li *LateInitializer) IsChanged() bool { return li.changed } // SetChanged marks the LateInitializer such that users can tell whether any // of the late initialization calls returned the non-original argument. func (li *LateInitializer) SetChanged() { li.changed = true } // LateInitializeStringPtr implements late initialization for *string. func (li *LateInitializer) LateInitializeStringPtr(org *string, from *string) *string { if org != nil || from == nil { return org } li.SetChanged() return from } // LateInitializeInt64Ptr implements late initialization for *int64. func (li *LateInitializer) LateInitializeInt64Ptr(org *int64, from *int64) *int64 { if org != nil || from == nil { return org } li.SetChanged() return from } // LateInitializeBoolPtr implements late initialization for *bool. func (li *LateInitializer) LateInitializeBoolPtr(org *bool, from *bool) *bool { if org != nil || from == nil { return org } li.SetChanged() return from } // LateInitializeTimePtr implements late initialization for *metav1.Time from // *time.Time. func (li *LateInitializer) LateInitializeTimePtr(org *metav1.Time, from *time.Time) *metav1.Time { if org != nil || from == nil { return org } li.SetChanged() t := metav1.NewTime(*from) return &t } ================================================ FILE: pkg/resource/late_initializer_test.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "testing" "time" "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestLateInitializeStringPtr(t *testing.T) { s1 := "desired" s2 := "observed" type args struct { org *string from *string } type want struct { result *string changed bool } cases := map[string]struct { args want }{ "Original": { args: args{ org: &s1, from: &s2, }, want: want{ result: &s1, changed: false, }, }, "LateInitialized": { args: args{ org: nil, from: &s2, }, want: want{ result: &s2, changed: true, }, }, "Neither": { args: args{ org: nil, from: nil, }, want: want{ result: nil, changed: false, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { li := NewLateInitializer() got := li.LateInitializeStringPtr(tc.org, tc.from) if diff := cmp.Diff(tc.result, got); diff != "" { t.Errorf("LateInitializeStringPtr(...): -want, +got:\n%s", diff) } if diff := cmp.Diff(tc.changed, li.IsChanged()); diff != "" { t.Errorf("IsChanged(...): -want, +got:\n%s", diff) } }) } } func TestLateInitializeInt64Ptr(t *testing.T) { i1 := int64(10) i2 := int64(20) type args struct { org *int64 from *int64 } type want struct { result *int64 changed bool } cases := map[string]struct { args want }{ "Original": { args: args{ org: &i1, from: &i2, }, want: want{ result: &i1, changed: false, }, }, "LateInitialized": { args: args{ org: nil, from: &i2, }, want: want{ result: &i2, changed: true, }, }, "Neither": { args: args{ org: nil, from: nil, }, want: want{ result: nil, changed: false, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { li := NewLateInitializer() got := li.LateInitializeInt64Ptr(tc.org, tc.from) if diff := cmp.Diff(tc.result, got); diff != "" { t.Errorf("LateInitializeBoolPtr(...): -want, +got:\n%s", diff) } if diff := cmp.Diff(tc.changed, li.IsChanged()); diff != "" { t.Errorf("IsChanged(...): -want, +got:\n%s", diff) } }) } } func TestLateInitializeBoolPtr(t *testing.T) { trueVal := true falseVal := false type args struct { org *bool from *bool } type want struct { result *bool changed bool } cases := map[string]struct { args want }{ "Original": { args: args{ org: &trueVal, from: &falseVal, }, want: want{ result: &trueVal, changed: false, }, }, "LateInitialized": { args: args{ org: nil, from: &trueVal, }, want: want{ result: &trueVal, changed: true, }, }, "Neither": { args: args{ org: nil, from: nil, }, want: want{ result: nil, changed: false, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { li := NewLateInitializer() got := li.LateInitializeBoolPtr(tc.org, tc.from) if diff := cmp.Diff(tc.result, got); diff != "" { t.Errorf("LateInitializeBoolPtr(...): -want, +got:\n%s", diff) } if diff := cmp.Diff(tc.changed, li.IsChanged()); diff != "" { t.Errorf("IsChanged(...): -want, +got:\n%s", diff) } }) } } func TestLateInitializeTimePtr(t *testing.T) { t1 := metav1.Now() t2 := time.Now().Add(time.Minute) t2m := metav1.NewTime(t2) type args struct { org *metav1.Time from *time.Time } type want struct { result *metav1.Time changed bool } cases := map[string]struct { args want }{ "Original": { args: args{ org: &t1, from: &t2, }, want: want{ result: &t1, changed: false, }, }, "LateInitialized": { args: args{ org: nil, from: &t2, }, want: want{ result: &t2m, changed: true, }, }, "Neither": { args: args{ org: nil, from: nil, }, want: want{ result: nil, changed: false, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { li := NewLateInitializer() got := li.LateInitializeTimePtr(tc.org, tc.from) if diff := cmp.Diff(tc.result, got); diff != "" { t.Errorf("LateInitializeTimePtr(...): -want, +got:\n%s", diff) } if diff := cmp.Diff(tc.changed, li.IsChanged()); diff != "" { t.Errorf("IsChanged(...): -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/resource/predicates.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "maps" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" ) // A PredicateFn returns true if the supplied object should be reconciled. // // Deprecated: This type will be removed soon. Please use // controller-runtime's predicate.NewPredicateFuncs instead. type PredicateFn func(obj runtime.Object) bool // NewPredicates returns a set of Funcs that are all satisfied by the supplied // PredicateFn. The PredicateFn is run against the new object during updates. // // Deprecated: This function will be removed soon. Please use // controller-runtime's predicate.NewPredicateFuncs instead. func NewPredicates(fn PredicateFn) predicate.Funcs { return predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { return fn(e.Object) }, DeleteFunc: func(e event.DeleteEvent) bool { return fn(e.Object) }, UpdateFunc: func(e event.UpdateEvent) bool { return fn(e.ObjectNew) }, GenericFunc: func(e event.GenericEvent) bool { return fn(e.Object) }, } } // DesiredStateChanged accepts objects that have changed their desired state, i.e. // the state that is not managed by the controller. // To be more specific, it accepts update events that have changes in one of the followings: // - `metadata.annotations` (except for certain annotations) // - `metadata.labels` // - `spec`. func DesiredStateChanged() predicate.Predicate { return predicate.Or( AnnotationChangedPredicate{ ignored: []string{ // These annotations are managed by the controller and should // not be considered as a change in desired state. The managed // reconciler explicitly requests a new reconcile already after // updating these annotations. meta.AnnotationKeyExternalCreateFailed, meta.AnnotationKeyExternalCreatePending, }, }, predicate.LabelChangedPredicate{}, predicate.GenerationChangedPredicate{}, ) } // AnnotationChangedPredicate implements a default update predicate function on // annotation change by ignoring the given annotation keys, if any. // // This predicate extends controller-runtime's AnnotationChangedPredicate by // being able to ignore certain annotations. type AnnotationChangedPredicate struct { predicate.Funcs ignored []string } func copyAnnotations(an map[string]string) map[string]string { r := make(map[string]string, len(an)) maps.Copy(r, an) return r } // Update implements default UpdateEvent filter for validating annotation change. func (a AnnotationChangedPredicate) Update(e event.UpdateEvent) bool { if e.ObjectOld == nil { // Update event has no old object to update return false } if e.ObjectNew == nil { // Update event has no new object for update return false } na := copyAnnotations(e.ObjectNew.GetAnnotations()) oa := copyAnnotations(e.ObjectOld.GetAnnotations()) for _, k := range a.ignored { delete(na, k) delete(oa, k) } // Below is the same as controller-runtime's AnnotationChangedPredicate // implementation but optimized to avoid using reflect.DeepEqual. if len(na) != len(oa) { // annotation length changed return true } for k, v := range na { if oa[k] != v { // annotation value changed return true } } // annotations unchanged. return false } ================================================ FILE: pkg/resource/predicates_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "testing" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" ) func TestDesiredStateChanged(t *testing.T) { type args struct { old client.Object new client.Object } type want struct { desiredStateChanged bool } cases := map[string]struct { args want }{ "NothingChanged": { args: args{ old: func() client.Object { mg := &fake.Managed{} return mg }(), new: func() client.Object { mg := &fake.Managed{} return mg }(), }, want: want{ desiredStateChanged: false, }, }, "StatusChanged": { args: args{ old: func() client.Object { mg := &fake.Managed{} return mg }(), new: func() client.Object { mg := &fake.Managed{} mg.SetConditions(xpv2.ReconcileSuccess()) return mg }(), }, want: want{ desiredStateChanged: false, }, }, "IgnoredAnnotationsChanged": { args: args{ old: func() client.Object { mg := &fake.Managed{} return mg }(), new: func() client.Object { mg := &fake.Managed{} mg.SetAnnotations(map[string]string{meta.AnnotationKeyExternalCreatePending: time.Now().String()}) return mg }(), }, want: want{ desiredStateChanged: false, }, }, "AnnotationsChanged": { args: args{ old: func() client.Object { mg := &fake.Managed{} return mg }(), new: func() client.Object { mg := &fake.Managed{} mg.SetAnnotations(map[string]string{"foo": "bar"}) return mg }(), }, want: want{ desiredStateChanged: true, }, }, "LabelsChanged": { args: args{ old: func() client.Object { mg := &fake.Managed{} return mg }(), new: func() client.Object { mg := &fake.Managed{} mg.SetLabels(map[string]string{"foo": "bar"}) return mg }(), }, want: want{ desiredStateChanged: true, }, }, // This happens when spec is changed. "GenerationChanged": { args: args{ old: func() client.Object { mg := &fake.Managed{} mg.SetGeneration(1) return mg }(), new: func() client.Object { mg := &fake.Managed{} mg.SetGeneration(2) return mg }(), }, want: want{ desiredStateChanged: true, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := DesiredStateChanged().Update(event.UpdateEvent{ ObjectOld: tc.old, ObjectNew: tc.new, }) if diff := cmp.Diff(tc.desiredStateChanged, got); diff != "" { t.Errorf("DesiredStateChanged(...): -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/resource/providerconfig.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "context" "os" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/spf13/afero" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" ) const ( errExtractEnv = "cannot extract from environment variable when none specified" errExtractFs = "cannot extract from filesystem when no path specified" errExtractSecretKey = "cannot extract from secret key when none specified" errGetCredentialsSecret = "cannot get credentials secret" errNoHandlerForSourceFmt = "no extraction handler registered for source: %s" errMissingPCRef = "managed resource does not reference a ProviderConfig" errMissingPCRefKind = "managed resource ProviderConfig reference has no Kind" errApplyPCU = "cannot apply ProviderConfigUsage" ) type missingRefError struct{ error } func (m missingRefError) MissingReference() bool { return true } // IsMissingReference returns true if an error indicates that a managed // resource is missing a required reference.. func IsMissingReference(err error) bool { _, ok := err.(interface { MissingReference() bool }) return ok } // EnvLookupFn looks up an environment variable. type EnvLookupFn func(string) string // ExtractEnv extracts credentials from an environment variable. func ExtractEnv(_ context.Context, e EnvLookupFn, s xpv2.CommonCredentialSelectors) ([]byte, error) { if s.Env == nil { return nil, errors.New(errExtractEnv) } return []byte(e(s.Env.Name)), nil } // ExtractFs extracts credentials from the filesystem. func ExtractFs(_ context.Context, fs afero.Fs, s xpv2.CommonCredentialSelectors) ([]byte, error) { if s.Fs == nil { return nil, errors.New(errExtractFs) } return afero.ReadFile(fs, s.Fs.Path) } // ExtractSecret extracts credentials from a Kubernetes secret. func ExtractSecret(ctx context.Context, client client.Client, s xpv2.CommonCredentialSelectors) ([]byte, error) { if s.SecretRef == nil { return nil, errors.New(errExtractSecretKey) } secret := &corev1.Secret{} if err := client.Get(ctx, types.NamespacedName{Namespace: s.SecretRef.Namespace, Name: s.SecretRef.Name}, secret); err != nil { return nil, errors.Wrap(err, errGetCredentialsSecret) } return secret.Data[s.SecretRef.Key], nil } // CommonCredentialExtractor extracts credentials from common sources. func CommonCredentialExtractor(ctx context.Context, source xpv2.CredentialsSource, client client.Client, selector xpv2.CommonCredentialSelectors) ([]byte, error) { switch source { case xpv2.CredentialsSourceEnvironment: return ExtractEnv(ctx, os.Getenv, selector) case xpv2.CredentialsSourceFilesystem: return ExtractFs(ctx, afero.NewOsFs(), selector) case xpv2.CredentialsSourceSecret: return ExtractSecret(ctx, client, selector) case xpv2.CredentialsSourceNone: return nil, nil case xpv2.CredentialsSourceInjectedIdentity: // There is no common injected identity extractor. Each provider must // implement their own. fallthrough default: return nil, errors.Errorf(errNoHandlerForSourceFmt, source) } } // A Tracker tracks managed resources. type Tracker interface { // Track the supplied managed resource. Track(ctx context.Context, mg Managed) error } // A TrackerFn is a function that tracks managed resources. type TrackerFn func(ctx context.Context, mg Managed) error // Track the supplied managed resource. func (fn TrackerFn) Track(ctx context.Context, mg Managed) error { return fn(ctx, mg) } // A LegacyTracker tracks legacy managed resources. type LegacyTracker interface { // Track the supplied legacy managed resource. Track(ctx context.Context, mg LegacyManaged) error } // A LegacyTrackerFn is a function that tracks legacy managed resources. type LegacyTrackerFn func(ctx context.Context, mg LegacyManaged) error // Track the supplied legacy managed resource. func (fn LegacyTrackerFn) Track(ctx context.Context, mg LegacyManaged) error { return fn(ctx, mg) } // A ModernTracker tracks modern managed resources. type ModernTracker interface { // Track the supplied modern managed resource. Track(ctx context.Context, mg ModernManaged) error } // A ModernTrackerFn is a function that tracks modern managed resources. type ModernTrackerFn func(ctx context.Context, mg ModernManaged) error // Track the supplied modern managed resource. func (fn ModernTrackerFn) Track(ctx context.Context, mg ModernManaged) error { return fn(ctx, mg) } // A ProviderConfigUsageTracker tracks usages of a ProviderConfig by creating or // updating the appropriate ProviderConfigUsage. type ProviderConfigUsageTracker struct { c Applicator of ProviderConfigUsage } // NewProviderConfigUsageTracker creates a ProviderConfigUsageTracker. func NewProviderConfigUsageTracker(c client.Client, of TypedProviderConfigUsage) *ProviderConfigUsageTracker { return &ProviderConfigUsageTracker{c: NewAPIUpdatingApplicator(c), of: of} } // Track that the supplied Managed resource is using the ProviderConfig it // references by creating or updating a ProviderConfigUsage. Track should be // called _before_ attempting to use the ProviderConfig. This ensures the // managed resource's usage is updated if the managed resource is updated to // reference a misconfigured ProviderConfig. func (u *ProviderConfigUsageTracker) Track(ctx context.Context, mg ModernManaged) error { //nolint:forcetypeassert // Will always be a PCU. pcu := u.of.DeepCopyObject().(TypedProviderConfigUsage) gvk := mg.GetObjectKind().GroupVersionKind() ref := mg.GetProviderConfigReference() if ref == nil { return missingRefError{errors.New(errMissingPCRef)} } if ref.Kind == "" { return missingRefError{errors.New(errMissingPCRefKind)} } pcu.SetName(string(mg.GetUID())) pcu.SetNamespace(mg.GetNamespace()) pcu.SetLabels(map[string]string{xpv2.LabelKeyProviderName: ref.Name, xpv2.LabelKeyProviderKind: ref.Kind}) pcu.SetOwnerReferences([]metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(mg, gvk))}) pcu.SetProviderConfigReference(xpv2.ProviderConfigReference{Name: ref.Name, Kind: ref.Kind}) pcu.SetResourceReference(xpv2.TypedReference{ APIVersion: gvk.GroupVersion().String(), Kind: gvk.Kind, Name: mg.GetName(), }) err := u.c.Apply(ctx, pcu, MustBeControllableBy(mg.GetUID()), AllowUpdateIf(func(current, _ runtime.Object) bool { //nolint:forcetypeassert // Will always be a PCU. return current.(TypedProviderConfigUsage).GetProviderConfigReference() != pcu.GetProviderConfigReference() }), ) return errors.Wrap(Ignore(IsNotAllowed, err), errApplyPCU) } // A LegacyProviderConfigUsageTracker tracks usages of a by creating or // updating the appropriate LegacyProviderConfigUsage. type LegacyProviderConfigUsageTracker struct { c Applicator of LegacyProviderConfigUsage } // NewLegacyProviderConfigUsageTracker tracks usages of a by creating or // updating the appropriate LegacyProviderConfigUsage. func NewLegacyProviderConfigUsageTracker(c client.Client, of LegacyProviderConfigUsage) *LegacyProviderConfigUsageTracker { return &LegacyProviderConfigUsageTracker{c: NewAPIUpdatingApplicator(c), of: of} } // Track that the supplied LegacyManaged resource is using the ProviderConfig it // references by creating or updating a ProviderConfigUsage. Track should be // called _before_ attempting to use the ProviderConfig. This ensures the // managed resource's usage is updated if the managed resource is updated to // reference a misconfigured ProviderConfig. func (u *LegacyProviderConfigUsageTracker) Track(ctx context.Context, mg LegacyManaged) error { //nolint:forcetypeassert // Will always be a legacy PCU. pcu := u.of.DeepCopyObject().(LegacyProviderConfigUsage) gvk := mg.GetObjectKind().GroupVersionKind() ref := mg.GetProviderConfigReference() if ref == nil { return missingRefError{errors.New(errMissingPCRef)} } pcu.SetName(string(mg.GetUID())) pcu.SetLabels(map[string]string{xpv2.LabelKeyProviderName: ref.Name}) pcu.SetOwnerReferences([]metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(mg, gvk))}) pcu.SetProviderConfigReference(xpv2.Reference{Name: ref.Name}) pcu.SetResourceReference(xpv2.TypedReference{ APIVersion: gvk.GroupVersion().String(), Kind: gvk.Kind, Name: mg.GetName(), }) err := u.c.Apply(ctx, pcu, MustBeControllableBy(mg.GetUID()), AllowUpdateIf(func(current, _ runtime.Object) bool { //nolint:forcetypeassert // Will always be a PCU. return current.(LegacyProviderConfigUsage).GetProviderConfigReference() != pcu.GetProviderConfigReference() }), ) return errors.Wrap(Ignore(IsNotAllowed, err), errApplyPCU) } ================================================ FILE: pkg/resource/providerconfig_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "context" "testing" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" "github.com/spf13/afero" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestExtractEnv(t *testing.T) { credentials := []byte("supersecretcreds") type args struct { e EnvLookupFn creds xpv2.CommonCredentialSelectors } type want struct { b []byte err error } cases := map[string]struct { reason string args args want want }{ "EnvVarSuccess": { reason: "Successful extraction of credentials from environment variable", args: args{ e: func(string) string { return string(credentials) }, creds: xpv2.CommonCredentialSelectors{ Env: &xpv2.EnvSelector{ Name: "SECRET_CREDS", }, }, }, want: want{ b: credentials, }, }, "EnvVarFail": { reason: "Failed extraction of credentials from environment variable", args: args{ e: func(string) string { return string(credentials) }, }, want: want{ err: errors.New(errExtractEnv), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := ExtractEnv(context.TODO(), tc.args.e, tc.args.creds) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\npc.ExtractEnv(...): -want error, +got error:\n%s\n", tc.reason, diff) } if diff := cmp.Diff(tc.want.b, got); diff != "" { t.Errorf("\n%s\npc.ExtractEnv(...): -want, +got:\n%s\n", tc.reason, diff) } }) } } func TestExtractFs(t *testing.T) { credentials := []byte("supersecretcreds") mockFs := afero.NewMemMapFs() f, _ := mockFs.Create("credentials.txt") f.Write(credentials) f.Close() type args struct { fs afero.Fs creds xpv2.CommonCredentialSelectors } type want struct { b []byte err error } cases := map[string]struct { reason string args args want want }{ "FsSuccess": { reason: "Successful extraction of credentials from filesystem", args: args{ fs: mockFs, creds: xpv2.CommonCredentialSelectors{ Fs: &xpv2.FsSelector{ Path: "credentials.txt", }, }, }, want: want{ b: credentials, }, }, "FsFailure": { reason: "Failed extraction of credentials from filesystem", args: args{ fs: mockFs, }, want: want{ err: errors.New(errExtractFs), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := ExtractFs(context.TODO(), tc.args.fs, tc.args.creds) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\npc.ExtractFs(...): -want error, +got error:\n%s\n", tc.reason, diff) } if diff := cmp.Diff(tc.want.b, got); diff != "" { t.Errorf("\n%s\npc.ExtractFs(...): -want, +got:\n%s\n", tc.reason, diff) } }) } } func TestExtractSecret(t *testing.T) { errBoom := errors.New("boom") credentials := []byte("supersecretcreds") type args struct { client client.Client creds xpv2.CommonCredentialSelectors } type want struct { b []byte err error } cases := map[string]struct { reason string args args want want }{ "SecretSuccess": { reason: "Successful extraction of credentials from Secret", args: args{ client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(o client.Object) error { s, _ := o.(*corev1.Secret) s.Data = map[string][]byte{ "creds": credentials, } return nil }), }, creds: xpv2.CommonCredentialSelectors{ SecretRef: &xpv2.SecretKeySelector{ SecretReference: xpv2.SecretReference{ Name: "super", Namespace: "secret", }, Key: "creds", }, }, }, want: want{ b: credentials, }, }, "SecretFailureNotDefined": { reason: "Failed extraction of credentials from Secret when key not defined", args: args{}, want: want{ err: errors.New(errExtractSecretKey), }, }, "SecretFailureGet": { reason: "Failed extraction of credentials from Secret when client fails", args: args{ client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(client.Object) error { return errBoom }), }, creds: xpv2.CommonCredentialSelectors{ SecretRef: &xpv2.SecretKeySelector{ SecretReference: xpv2.SecretReference{ Name: "super", Namespace: "secret", }, Key: "creds", }, }, }, want: want{ err: errors.Wrap(errBoom, errGetCredentialsSecret), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := ExtractSecret(context.TODO(), tc.args.client, tc.args.creds) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\npc.ExtractSecret(...): -want error, +got error:\n%s\n", tc.reason, diff) } if diff := cmp.Diff(tc.want.b, got); diff != "" { t.Errorf("\n%s\npc.ExtractSecret(...): -want, +got:\n%s\n", tc.reason, diff) } }) } } func TestTrackLegacy(t *testing.T) { errBoom := errors.New("boom") name := "provisional" type fields struct { c Applicator of LegacyProviderConfigUsage } type args struct { ctx context.Context mg LegacyManaged } cases := map[string]struct { reason string fields fields args args want error }{ "MissingRef": { reason: "An error that satisfies IsMissingReference should be returned if the managed resource has no provider config reference", fields: fields{ of: &fake.LegacyProviderConfigUsage{}, }, args: args{ mg: &fake.LegacyManaged{}, }, want: missingRefError{errors.New(errMissingPCRef)}, }, "NopUpdate": { reason: "No error should be returned if the apply fails because it would be a no-op", fields: fields{ c: ApplyFn(func(ctx context.Context, _ client.Object, ao ...ApplyOption) error { for _, fn := range ao { // Exercise the MustBeControllableBy and AllowUpdateIf // ApplyOptions. The former should pass because the // current object has no controller ref. The latter // should return an error that satisfies IsNotAllowed // because the current object has the same PC ref as the // new one we would apply. current := &fake.LegacyProviderConfigUsage{ RequiredProviderConfigReferencer: fake.RequiredProviderConfigReferencer{ Ref: xpv2.Reference{Name: name}, }, } if err := fn(ctx, current, nil); err != nil { return err } } return errBoom }), of: &fake.LegacyProviderConfigUsage{}, }, args: args{ mg: &fake.LegacyManaged{ LegacyProviderConfigReferencer: fake.LegacyProviderConfigReferencer{ Ref: &xpv2.Reference{Name: name}, }, }, }, want: nil, }, "ApplyError": { reason: "Errors applying the ProviderConfigUsage should be returned", fields: fields{ c: ApplyFn(func(_ context.Context, _ client.Object, _ ...ApplyOption) error { return errBoom }), of: &fake.LegacyProviderConfigUsage{}, }, args: args{ mg: &fake.LegacyManaged{ LegacyProviderConfigReferencer: fake.LegacyProviderConfigReferencer{ Ref: &xpv2.Reference{Name: name}, }, }, }, want: errors.Wrap(errBoom, errApplyPCU), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ut := &LegacyProviderConfigUsageTracker{c: tc.fields.c, of: tc.fields.of} got := ut.Track(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nut.Track(...): -want error, +got error:\n%s\n", tc.reason, diff) } }) } } func TestTrackModern(t *testing.T) { errBoom := errors.New("boom") name := "provisional" type fields struct { c Applicator of TypedProviderConfigUsage } type args struct { ctx context.Context mg ModernManaged } cases := map[string]struct { reason string fields fields args args want error }{ "MissingRef": { reason: "An error that satisfies IsMissingReference should be returned if the managed resource has no provider config reference", fields: fields{ of: &fake.ProviderConfigUsage{}, }, args: args{ mg: &fake.ModernManaged{}, }, want: missingRefError{errors.New(errMissingPCRef)}, }, "NopUpdate": { reason: "No error should be returned if the apply fails because it would be a no-op", fields: fields{ c: ApplyFn(func(ctx context.Context, _ client.Object, ao ...ApplyOption) error { for _, fn := range ao { // Exercise the MustBeControllableBy and AllowUpdateIf // ApplyOptions. The former should pass because the // current object has no controller ref. The latter // should return an error that satisfies IsNotAllowed // because the current object has the same PC ref as the // new one we would apply. current := &fake.ProviderConfigUsage{ RequiredTypedProviderConfigReferencer: fake.RequiredTypedProviderConfigReferencer{ Ref: xpv2.ProviderConfigReference{Name: name, Kind: "ProviderConfig"}, }, } if err := fn(ctx, current, nil); err != nil { return err } } return errBoom }), of: &fake.ProviderConfigUsage{}, }, args: args{ mg: &fake.ModernManaged{ TypedProviderConfigReferencer: fake.TypedProviderConfigReferencer{ Ref: &xpv2.ProviderConfigReference{Name: name, Kind: "ProviderConfig"}, }, }, }, want: nil, }, "ApplyError": { reason: "Errors applying the ProviderConfigUsage should be returned", fields: fields{ c: ApplyFn(func(_ context.Context, _ client.Object, _ ...ApplyOption) error { return errBoom }), of: &fake.ProviderConfigUsage{}, }, args: args{ mg: &fake.ModernManaged{ TypedProviderConfigReferencer: fake.TypedProviderConfigReferencer{ Ref: &xpv2.ProviderConfigReference{Name: name, Kind: "ProviderConfig"}, }, }, }, want: errors.Wrap(errBoom, errApplyPCU), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ut := &ProviderConfigUsageTracker{c: tc.fields.c, of: tc.fields.of} got := ut.Track(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nut.Track(...): -want error, +got error:\n%s\n", tc.reason, diff) } }) } } ================================================ FILE: pkg/resource/reference.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "context" "fmt" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) // ReferenceStatusType is an enum type for the possible values for a Reference Status. type ReferenceStatusType int // Reference statuses. const ( ReferenceStatusUnknown ReferenceStatusType = iota ReferenceNotFound ReferenceNotReady ReferenceReady ) func (t ReferenceStatusType) String() string { return []string{"Unknown", "NotFound", "NotReady", "Ready"}[t] } // ReferenceStatus has the name and status of a reference. type ReferenceStatus struct { Name string Status ReferenceStatusType } func (r ReferenceStatus) String() string { return fmt.Sprintf("{reference:%s status:%s}", r.Name, r.Status) } // A CanReference is a resource that can reference another resource in its // spec in order to automatically resolve corresponding spec field values // by inspecting the referenced resource. type CanReference runtime.Object // An AttributeReferencer resolves cross-resource attribute references. See // https://github.com/crossplane/crossplane/blob/main/design/one-pager-cross-resource-referencing.md // for more information. type AttributeReferencer interface { // GetStatus retries the referenced resource, as well as other non-managed // resources (like a `Provider`) and reports their readiness for use as a // referenced resource. GetStatus(ctx context.Context, res CanReference, r client.Reader) ([]ReferenceStatus, error) // Build retrieves the referenced resource, as well as other non-managed // resources (like a `Provider`), and builds the referenced attribute, // returning it as a string value. Build(ctx context.Context, res CanReference, r client.Reader) (value string, err error) // Assign accepts a managed resource object, and assigns the given value to // its corresponding property. Assign(res CanReference, value string) error } ================================================ FILE: pkg/resource/reference_test.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "fmt" "testing" ) func TestReferenceStatusType_String(t *testing.T) { tests := map[string]struct { t ReferenceStatusType want string }{ "ReferenceStatusUnknown": { t: ReferenceStatusUnknown, want: "Unknown", }, "ReferenceNotFound": { t: ReferenceNotFound, want: "NotFound", }, "ReferenceNotReady": { t: ReferenceNotReady, want: "NotReady", }, "ReferenceReady": { t: ReferenceReady, want: "Ready", }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { if got := tt.t.String(); got != tt.want { t.Errorf("String() = %v, want %v", got, tt.want) } }) } } func TestReferenceStatus_String(t *testing.T) { tests := map[string]struct { rs ReferenceStatus want string }{ "test-name-ready": { rs: ReferenceStatus{ Name: "test-name", Status: ReferenceReady, }, want: fmt.Sprintf("{reference:test-name status:%s}", ReferenceReady.String()), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { if got := tt.rs.String(); got != tt.want { t.Errorf("String() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/resource/resource.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "context" "fmt" "sort" "strings" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "google.golang.org/protobuf/types/known/structpb" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured" ) // SecretTypeConnection is the type of Crossplane connection secrets. const SecretTypeConnection corev1.SecretType = "connection.crossplane.io/v1alpha1" // External resources are tagged/labelled with the following keys in the cloud // provider API if the type supports. const ( ExternalResourceTagKeyKind = "crossplane-kind" ExternalResourceTagKeyName = "crossplane-name" ExternalResourceTagKeyNamespace = "crossplane-namespace" ExternalResourceTagKeyProvider = "crossplane-providerconfig" ExternalResourceTagKeyProviderConfigKind = "crossplane-providerconfig-kind" errMarshalJSON = "cannot marshal to JSON" errUnmarshalJSON = "cannot unmarshal JSON data" errStructFromUnstructured = "cannot create Struct" ) // A ManagedKind contains the type metadata for a kind of managed resource. type ManagedKind schema.GroupVersionKind // A CompositeKind contains the type metadata for a kind of composite resource. type CompositeKind schema.GroupVersionKind // A CompositeClaimKind contains the type metadata for a kind of composite // resource claim. type CompositeClaimKind schema.GroupVersionKind // ProviderConfigKinds contains the type metadata for a kind of provider config. type ProviderConfigKinds struct { Config schema.GroupVersionKind Usage schema.GroupVersionKind UsageList schema.GroupVersionKind } // A ConnectionSecretOwner is a Kubernetes object that owns a connection secret. type ConnectionSecretOwner interface { Object ConnectionSecretWriterTo } // A LocalConnectionSecretOwner may create and manage a connection secret in its // own namespace. type LocalConnectionSecretOwner interface { runtime.Object metav1.Object LocalConnectionSecretWriterTo } // LocalConnectionSecretFor creates a connection secret in the namespace of the // supplied LocalConnectionSecretOwner, assumed to be of the supplied kind. func LocalConnectionSecretFor(o LocalConnectionSecretOwner, kind schema.GroupVersionKind) *corev1.Secret { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: o.GetNamespace(), Name: o.GetWriteConnectionSecretToReference().Name, OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(o, kind))}, }, Type: SecretTypeConnection, Data: make(map[string][]byte), } } // ConnectionSecretFor creates a connection for the supplied // ConnectionSecretOwner, assumed to be of the supplied kind. The secret is // written to 'default' namespace if the ConnectionSecretOwner does not specify // a namespace. func ConnectionSecretFor(o ConnectionSecretOwner, kind schema.GroupVersionKind) *corev1.Secret { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: o.GetWriteConnectionSecretToReference().Namespace, Name: o.GetWriteConnectionSecretToReference().Name, OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(o, kind))}, }, Type: SecretTypeConnection, Data: make(map[string][]byte), } } // MustCreateObject returns a new Object of the supplied kind. It panics if the // kind is unknown to the supplied ObjectCreator. func MustCreateObject(kind schema.GroupVersionKind, oc runtime.ObjectCreater) runtime.Object { obj, err := oc.New(kind) if err != nil { panic(err) } return obj } // GetKind returns the GroupVersionKind of the supplied object. It return an // error if the object is unknown to the supplied ObjectTyper, the object is // unversioned, or the object does not have exactly one registered kind. func GetKind(obj runtime.Object, ot runtime.ObjectTyper) (schema.GroupVersionKind, error) { kinds, unversioned, err := ot.ObjectKinds(obj) if err != nil { return schema.GroupVersionKind{}, errors.Wrap(err, "cannot get kind of supplied object") } if unversioned { return schema.GroupVersionKind{}, errors.New("supplied object is unversioned") } if len(kinds) != 1 { return schema.GroupVersionKind{}, errors.New("supplied object does not have exactly one kind") } return kinds[0], nil } // MustGetKind returns the GroupVersionKind of the supplied object. It panics if // the object is unknown to the supplied ObjectTyper, the object is unversioned, // or the object does not have exactly one registered kind. func MustGetKind(obj runtime.Object, ot runtime.ObjectTyper) schema.GroupVersionKind { gvk, err := GetKind(obj, ot) if err != nil { panic(err) } return gvk } // An ErrorIs function returns true if an error satisfies a particular condition. type ErrorIs func(err error) bool // Ignore any errors that satisfy the supplied ErrorIs function by returning // nil. Errors that do not satisfy the supplied function are returned unmodified. func Ignore(is ErrorIs, err error) error { if is(err) { return nil } return err } // IgnoreAny ignores errors that satisfy any of the supplied ErrorIs functions // by returning nil. Errors that do not satisfy any of the supplied functions // are returned unmodified. func IgnoreAny(err error, is ...ErrorIs) error { for _, f := range is { if f(err) { return nil } } return err } // IgnoreNotFound returns the supplied error, or nil if the error indicates a // Kubernetes resource was not found. func IgnoreNotFound(err error) error { return Ignore(kerrors.IsNotFound, err) } // IsAPIError returns true if the given error's type is of Kubernetes API error. func IsAPIError(err error) bool { _, ok := err.(kerrors.APIStatus) return ok } // IsAPIErrorWrapped returns true if err is a K8s API error, or recursively wraps a K8s API error. func IsAPIErrorWrapped(err error) bool { return IsAPIError(errors.Cause(err)) } // IsConditionTrue returns if condition status is true. func IsConditionTrue(c xpv2.Condition) bool { return c.Status == corev1.ConditionTrue } // An Applicator applies changes to an object. type Applicator interface { Apply(ctx context.Context, obj client.Object, o ...ApplyOption) error } type shouldRetryFunc func(error) bool // An ApplicatorWithRetry applies changes to an object, retrying on transient failures. type ApplicatorWithRetry struct { Applicator shouldRetry shouldRetryFunc backoff wait.Backoff } // Apply invokes nested Applicator's Apply retrying on designated errors. func (awr *ApplicatorWithRetry) Apply(ctx context.Context, c client.Object, opts ...ApplyOption) error { return retry.OnError(awr.backoff, awr.shouldRetry, func() error { return awr.Applicator.Apply(ctx, c, opts...) }) } // NewApplicatorWithRetry returns an ApplicatorWithRetry for the specified // applicator and with the specified retry function. // // If backoff is nil, then retry.DefaultRetry is used as the default. func NewApplicatorWithRetry(applicator Applicator, shouldRetry shouldRetryFunc, backoff *wait.Backoff) *ApplicatorWithRetry { result := &ApplicatorWithRetry{ Applicator: applicator, shouldRetry: shouldRetry, backoff: retry.DefaultRetry, } if backoff != nil { result.backoff = *backoff } return result } // A ClientApplicator may be used to build a single 'client' that satisfies both // client.Client and Applicator. type ClientApplicator struct { client.Client Applicator } // An ApplyFn is a function that satisfies the Applicator interface. type ApplyFn func(context.Context, client.Object, ...ApplyOption) error // Apply changes to the supplied object. func (fn ApplyFn) Apply(ctx context.Context, o client.Object, ao ...ApplyOption) error { return fn(ctx, o, ao...) } // An ApplyOption is called before patching the current object to match the // desired object. ApplyOptions are not called if no current object exists. type ApplyOption func(ctx context.Context, current, desired runtime.Object) error // UpdateFn returns an ApplyOption that is used to modify the current object to // match fields of the desired. func UpdateFn(fn func(current, desired runtime.Object)) ApplyOption { return func(_ context.Context, c, d runtime.Object) error { fn(c, d) return nil } } type notControllableError struct{ error } func (e notControllableError) NotControllable() bool { return true } // IsNotControllable returns true if the supplied error indicates that a // resource is not controllable - i.e. that it another resource is not and may // not become its controller reference. func IsNotControllable(err error) bool { _, ok := err.(interface { NotControllable() bool }) return ok } // MustBeControllableBy requires that the current object is controllable by an // object with the supplied UID. An object is controllable if its controller // reference matches the supplied UID, or it has no controller reference. An // error that satisfies IsNotControllable will be returned if the current object // cannot be controlled by the supplied UID. func MustBeControllableBy(u types.UID) ApplyOption { return func(_ context.Context, current, _ runtime.Object) error { mo, ok := current.(metav1.Object) if !ok { return notControllableError{errors.Errorf("existing object is missing object metadata")} } c := metav1.GetControllerOf(mo) if c == nil { return nil } if c.UID != u { return notControllableError{errors.Errorf("existing object is not controlled by UID %q", u)} } return nil } } // ConnectionSecretMustBeControllableBy requires that the current object is a // connection secret that is controllable by an object with the supplied UID. // Contemporary connection secrets are of SecretTypeConnection, while legacy // connection secrets are of corev1.SecretTypeOpaque. Contemporary connection // secrets are considered controllable if they are already controlled by the // supplied UID, or have no controller reference. Legacy connection secrets are // only considered controllable if they are already controlled by the supplied // UID. It is not safe to assume legacy connection secrets without a controller // reference are controllable because they are indistinguishable from Kubernetes // secrets that have nothing to do with Crossplane. An error that satisfies // IsNotControllable will be returned if the current secret is not a connection // secret or cannot be controlled by the supplied UID. func ConnectionSecretMustBeControllableBy(u types.UID) ApplyOption { return func(_ context.Context, current, _ runtime.Object) error { s, ok := current.(*corev1.Secret) if !ok { return errors.New("current resource is not a Secret") } c := metav1.GetControllerOf(s) switch { case c == nil && s.Type != SecretTypeConnection: return notControllableError{errors.Errorf("refusing to modify uncontrolled secret of type %q", s.Type)} case c == nil: return nil case c.UID != u: return notControllableError{errors.Errorf("existing secret is not controlled by UID %q", u)} } return nil } } type notAllowedError struct{ error } func (e notAllowedError) NotAllowed() bool { return true } // NewNotAllowed returns a new NotAllowed error. func NewNotAllowed(message string) error { return notAllowedError{error: errors.New(message)} } // IsNotAllowed returns true if the supplied error indicates that an operation // was not allowed. func IsNotAllowed(err error) bool { _, ok := err.(interface { NotAllowed() bool }) return ok } // AllowUpdateIf will only update the current object if the supplied fn returns // true. An error that satisfies IsNotAllowed will be returned if the supplied // function returns false. Creation of a desired object that does not currently // exist is always allowed. func AllowUpdateIf(fn func(current, desired runtime.Object) bool) ApplyOption { return func(_ context.Context, current, desired runtime.Object) error { if fn(current, desired) { return nil } return notAllowedError{errors.New("update not allowed")} } } // StoreCurrentRV stores the resource version of the current object in the // supplied string pointer. This is useful to detect whether the Apply call // was a no-op. func StoreCurrentRV(origRV *string) ApplyOption { return func(_ context.Context, current, _ runtime.Object) error { mo, ok := current.(metav1.Object) if !ok { return errors.New("current resource is missing object metadata") } *origRV = mo.GetResourceVersion() return nil } } // GetExternalTags returns the identifying tags to be used to tag the external // resource in provider API. func GetExternalTags(mg Managed) map[string]string { tags := map[string]string{ ExternalResourceTagKeyKind: strings.ToLower(mg.GetObjectKind().GroupVersionKind().GroupKind().String()), ExternalResourceTagKeyName: mg.GetName(), } if namespace := mg.GetNamespace(); namespace != "" { tags[ExternalResourceTagKeyNamespace] = namespace } switch mg := mg.(type) { case TypedProviderConfigReferencer: if pcRef := mg.GetProviderConfigReference(); pcRef != nil { if pcRef.Name != "" { tags[ExternalResourceTagKeyProvider] = pcRef.Name } if pcRef.Kind != "" { tags[ExternalResourceTagKeyProviderConfigKind] = pcRef.Kind } } case ProviderConfigReferencer: if pcRef := mg.GetProviderConfigReference(); pcRef != nil && pcRef.Name != "" { tags[ExternalResourceTagKeyProvider] = pcRef.Name } } return tags } // DefaultFirstN is the default number of names to return in FirstNAndSomeMore. const DefaultFirstN = 3 // FirstNAndSomeMore returns a string that contains the first n names in the // supplied slice, followed by ", and more" if there are more than n. // The slice is not sorted, i.e. the caller must make sure the order is stable // e.g. when using this in conditions. func FirstNAndSomeMore(n int, names []string) string { if n <= 0 { return fmt.Sprintf("%d", len(names)) } if len(names) > n { return fmt.Sprintf("%s, and %d more", strings.Join(names[:n], ", "), len(names)-n) } if len(names) == n { return fmt.Sprintf("%s, and %s", strings.Join(names[:n-1], ", "), names[n-1]) } return strings.Join(names, ", ") } // StableNAndSomeMore is like FirstNAndSomeMore, but sorts the names before. // The input slice is not modified. func StableNAndSomeMore(n int, names []string) string { cpy := make([]string, len(names)) copy(cpy, names) sort.Strings(cpy) return FirstNAndSomeMore(n, cpy) } // AsProtobufStruct converts the given object to a structpb.Struct for usage with gRPC // connections. // Copied from: // https://github.com/crossplane/crossplane/blob/release-1.16/internal/controller/apiextensions/composite/composition_functions.go#L761 func AsProtobufStruct(o runtime.Object) (*structpb.Struct, error) { // If the supplied object is *Unstructured we don't need to round-trip. if u, ok := o.(*kunstructured.Unstructured); ok { s, err := structpb.NewStruct(u.Object) return s, errors.Wrap(err, errStructFromUnstructured) } // If the supplied object wraps *Unstructured we don't need to round-trip. if w, ok := o.(unstructured.Wrapper); ok { s, err := structpb.NewStruct(w.GetUnstructured().Object) return s, errors.Wrap(err, errStructFromUnstructured) } // Fall back to a JSON round-trip. b, err := json.Marshal(o) if err != nil { return nil, errors.Wrap(err, errMarshalJSON) } s := &structpb.Struct{} return s, errors.Wrap(s.UnmarshalJSON(b), errUnmarshalJSON) } ================================================ FILE: pkg/resource/resource_test.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at htcp://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "context" "strings" "testing" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) const ( namespace = "coolns" name = "cool" uid = types.UID("definitely-a-uuid") testSteps = 3 ) var ( MockOwnerGVK = schema.GroupVersionKind{ Group: "cool", Version: "large", Kind: "MockOwner", } testBackoff = wait.Backoff{} errTest = errors.New("test-error") ) func TestLocalConnectionSecretFor(t *testing.T) { secretName := "coolsecret" type args struct { o LocalConnectionSecretOwner kind schema.GroupVersionKind } controller := true cases := map[string]struct { args args want *corev1.Secret }{ "Success": { args: args{ o: &fake.MockLocalConnectionSecretOwner{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, UID: uid, }, Ref: &xpv2.LocalSecretReference{Name: secretName}, }, kind: MockOwnerGVK, }, want: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: secretName, OwnerReferences: []metav1.OwnerReference{{ APIVersion: MockOwnerGVK.GroupVersion().String(), Kind: MockOwnerGVK.Kind, Name: name, UID: uid, Controller: &controller, BlockOwnerDeletion: &controller, }}, }, Type: SecretTypeConnection, Data: map[string][]byte{}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := LocalConnectionSecretFor(tc.args.o, tc.args.kind) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("LocalConnectionSecretFor(): -want, +got:\n%s", diff) } }) } } func TestConnectionSecretFor(t *testing.T) { secretName := "coolsecret" type args struct { o ConnectionSecretOwner kind schema.GroupVersionKind } controller := true cases := map[string]struct { args args want *corev1.Secret }{ "Success": { args: args{ o: &fake.MockConnectionSecretOwner{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, UID: uid, }, WriterTo: &xpv2.SecretReference{Namespace: namespace, Name: secretName}, }, kind: MockOwnerGVK, }, want: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: secretName, OwnerReferences: []metav1.OwnerReference{{ APIVersion: MockOwnerGVK.GroupVersion().String(), Kind: MockOwnerGVK.Kind, Name: name, UID: uid, Controller: &controller, BlockOwnerDeletion: &controller, }}, }, Type: SecretTypeConnection, Data: map[string][]byte{}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := ConnectionSecretFor(tc.args.o, tc.args.kind) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("ConnectionSecretFor(): -want, +got:\n%s", diff) } }) } } type MockTyper struct { GVKs []schema.GroupVersionKind Unversioned bool Error error } func (t MockTyper) ObjectKinds(_ runtime.Object) ([]schema.GroupVersionKind, bool, error) { return t.GVKs, t.Unversioned, t.Error } func (t MockTyper) Recognizes(_ schema.GroupVersionKind) bool { return true } func TestGetKind(t *testing.T) { type args struct { obj runtime.Object ot runtime.ObjectTyper } type want struct { kind schema.GroupVersionKind err error } errBoom := errors.New("boom") cases := map[string]struct { args args want want }{ "KindFound": { args: args{ ot: MockTyper{GVKs: []schema.GroupVersionKind{fake.GVK(&fake.Managed{})}}, }, want: want{ kind: fake.GVK(&fake.Managed{}), }, }, "KindError": { args: args{ ot: MockTyper{Error: errBoom}, }, want: want{ err: errors.Wrap(errBoom, "cannot get kind of supplied object"), }, }, "KindIsUnversioned": { args: args{ ot: MockTyper{Unversioned: true}, }, want: want{ err: errors.New("supplied object is unversioned"), }, }, "NotEnoughKinds": { args: args{ ot: MockTyper{}, }, want: want{ err: errors.New("supplied object does not have exactly one kind"), }, }, "TooManyKinds": { args: args{ ot: MockTyper{GVKs: []schema.GroupVersionKind{ fake.GVK(&fake.Object{}), fake.GVK(&fake.Managed{}), }}, }, want: want{ err: errors.New("supplied object does not have exactly one kind"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := GetKind(tc.args.obj, tc.args.ot) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("GetKind(...): -want error, +got error:\n%s", diff) } if diff := cmp.Diff(tc.want.kind, got); diff != "" { t.Errorf("GetKind(...): -want, +got:\n%s", diff) } }) } } func TestMustCreateObject(t *testing.T) { type args struct { kind schema.GroupVersionKind oc runtime.ObjectCreater } cases := map[string]struct { args args want runtime.Object }{ "KindRegistered": { args: args{ kind: fake.GVK(&fake.Managed{}), oc: fake.SchemeWith(&fake.Managed{}), }, want: &fake.Managed{}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := MustCreateObject(tc.args.kind, tc.args.oc) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("MustCreateObject(...): -want, +got:\n%s", diff) } }) } } func TestIgnore(t *testing.T) { errBoom := errors.New("boom") type args struct { is ErrorIs err error } cases := map[string]struct { args args want error }{ "IgnoreError": { args: args{ is: func(_ error) bool { return true }, err: errBoom, }, want: nil, }, "PropagateError": { args: args{ is: func(_ error) bool { return false }, err: errBoom, }, want: errBoom, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := Ignore(tc.args.is, tc.args.err) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("Ignore(...): -want error, +got error:\n%s", diff) } }) } } func TestIgnoreAny(t *testing.T) { errBoom := errors.New("boom") type args struct { is []ErrorIs err error } cases := map[string]struct { args args want error }{ "IgnoreError": { args: args{ is: []ErrorIs{func(_ error) bool { return true }}, err: errBoom, }, want: nil, }, "IgnoreErrorArr": { args: args{ is: []ErrorIs{ func(_ error) bool { return true }, func(_ error) bool { return false }, }, err: errBoom, }, want: nil, }, "PropagateError": { args: args{ is: []ErrorIs{func(_ error) bool { return false }}, err: errBoom, }, want: errBoom, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := IgnoreAny(tc.args.err, tc.args.is...) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("Ignore(...): -want error, +got error:\n%s", diff) } }) } } func TestIsConditionTrue(t *testing.T) { cases := map[string]struct { c xpv2.Condition want bool }{ "IsTrue": { c: xpv2.Condition{Status: corev1.ConditionTrue}, want: true, }, "IsFalse": { c: xpv2.Condition{Status: corev1.ConditionFalse}, want: false, }, "IsUnknown": { c: xpv2.Condition{Status: corev1.ConditionUnknown}, want: false, }, "IsUnset": { c: xpv2.Condition{}, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := IsConditionTrue(tc.c) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("IsConditionTrue(...): -want, +got:\n%s", diff) } }) } } type object struct { runtime.Object metav1.ObjectMeta } func (o *object) DeepCopyObject() runtime.Object { return &object{ObjectMeta: *o.DeepCopy()} } func TestIsNotControllable(t *testing.T) { cases := map[string]struct { reason string err error want bool }{ "NilError": { reason: "A nil error does not indicate something is not controllable.", err: nil, want: false, }, "UnknownError": { reason: "An that doesn't have a 'NotControllable() bool' method does not indicate something is not controllable.", err: errors.New("boom"), want: false, }, "NotControllableError": { reason: "An that has a 'NotControllable() bool' method indicates something is not controllable.", err: notControllableError{errors.New("boom")}, want: true, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := IsNotControllable(tc.err) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\n%s\nIsNotControllable(...): -want, +got:\n%s\n", tc.reason, diff) } }) } } func TestMustBeControllableBy(t *testing.T) { uid := types.UID("very-unique-string") controller := true type args struct { ctx context.Context current runtime.Object desired runtime.Object } cases := map[string]struct { reason string u types.UID args args want error }{ "Adoptable": { reason: "A current object with no controller reference may be adopted and controlled", u: uid, args: args{ current: &object{}, }, }, "ControlledBySuppliedUID": { reason: "A current object that is already controlled by the supplied UID is controllable", u: uid, args: args{ current: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ UID: uid, Controller: &controller, }}}}, }, }, "ControlledBySomeoneElse": { reason: "A current object that is already controlled by a different UID is not controllable", u: uid, args: args{ current: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ UID: types.UID("some-other-uid"), Controller: &controller, }}}}, }, want: notControllableError{errors.Errorf("existing object is not controlled by UID %q", uid)}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ao := MustBeControllableBy(tc.u) err := ao(tc.args.ctx, tc.args.current, tc.args.desired) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nMustBeControllableBy(...)(...): -want error, +got error\n%s\n", tc.reason, diff) } }) } } func TestConnectionSecretMustBeControllableBy(t *testing.T) { uid := types.UID("very-unique-string") controller := true type args struct { ctx context.Context current runtime.Object desired runtime.Object } cases := map[string]struct { reason string u types.UID args args want error }{ "Adoptable": { reason: "A Secret of SecretTypeConnection with no controller reference may be adopted and controlled", u: uid, args: args{ current: &corev1.Secret{Type: SecretTypeConnection}, }, }, "ControlledBySuppliedUID": { reason: "A Secret of any type that is already controlled by the supplied UID is controllable", u: uid, args: args{ current: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ UID: uid, Controller: &controller, }}}, Type: corev1.SecretTypeOpaque, }, }, }, "ControlledBySomeoneElse": { reason: "A Secret of any type that is already controlled by the another UID is not controllable", u: uid, args: args{ current: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ UID: types.UID("some-other-uid"), Controller: &controller, }}}, Type: SecretTypeConnection, }, }, want: notControllableError{errors.Errorf("existing secret is not controlled by UID %q", uid)}, }, "UncontrolledOpaqueSecret": { reason: "A Secret of corev1.SecretTypeOpqaue with no controller is not controllable", u: uid, args: args{ current: &corev1.Secret{Type: corev1.SecretTypeOpaque}, }, want: notControllableError{errors.Errorf("refusing to modify uncontrolled secret of type %q", corev1.SecretTypeOpaque)}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ao := ConnectionSecretMustBeControllableBy(tc.u) err := ao(tc.args.ctx, tc.args.current, tc.args.desired) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nConnectionSecretMustBeControllableBy(...)(...): -want error, +got error\n%s\n", tc.reason, diff) } }) } } func TestAllowUpdateIf(t *testing.T) { type args struct { ctx context.Context current runtime.Object desired runtime.Object } cases := map[string]struct { reason string fn func(current, desired runtime.Object) bool args args want error }{ "Allowed": { reason: "No error should be returned when the supplied function returns true", fn: func(_, _ runtime.Object) bool { return true }, args: args{ current: &object{}, }, }, "NotAllowed": { reason: "An error that satisfies IsNotAllowed should be returned when the supplied function returns false", fn: func(_, _ runtime.Object) bool { return false }, args: args{ current: &object{}, }, want: notAllowedError{errors.New("update not allowed")}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ao := AllowUpdateIf(tc.fn) err := ao(tc.args.ctx, tc.args.current, tc.args.desired) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nAllowUpdateIf(...)(...): -want error, +got error\n%s\n", tc.reason, diff) } }) } } func TestGetExternalTags(t *testing.T) { provName := "prov" cases := map[string]struct { o Managed want map[string]string }{ "SuccessfulWithTypedProviderConfig": { o: &fake.ModernManaged{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, TypedProviderConfigReferencer: fake.TypedProviderConfigReferencer{ Ref: &xpv2.ProviderConfigReference{Name: provName, Kind: "ProviderConfig"}, }, }, want: map[string]string{ ExternalResourceTagKeyKind: strings.ToLower((&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupKind().String()), ExternalResourceTagKeyName: name, ExternalResourceTagKeyProvider: provName, ExternalResourceTagKeyProviderConfigKind: "ProviderConfig", }, }, "SuccessfulWithNamespacedObject": { o: &fake.ModernManaged{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, TypedProviderConfigReferencer: fake.TypedProviderConfigReferencer{ Ref: &xpv2.ProviderConfigReference{Name: provName, Kind: "ProviderConfig"}, }, }, want: map[string]string{ ExternalResourceTagKeyKind: strings.ToLower((&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupKind().String()), ExternalResourceTagKeyName: name, ExternalResourceTagKeyNamespace: namespace, ExternalResourceTagKeyProvider: provName, ExternalResourceTagKeyProviderConfigKind: "ProviderConfig", }, }, "SuccessfulWithLegacyProviderConfig": { o: &fake.LegacyManaged{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, LegacyProviderConfigReferencer: fake.LegacyProviderConfigReferencer{Ref: &xpv2.Reference{Name: provName}}, }, want: map[string]string{ ExternalResourceTagKeyKind: strings.ToLower((&fake.LegacyManaged{}).GetObjectKind().GroupVersionKind().GroupKind().String()), ExternalResourceTagKeyName: name, ExternalResourceTagKeyProvider: provName, }, }, "NotLegacyOrModernManaged": { o: &fake.Managed{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, }, want: map[string]string{ ExternalResourceTagKeyKind: strings.ToLower((&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupKind().String()), ExternalResourceTagKeyName: name, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := GetExternalTags(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("GetExternalTags(...): -want, +got:\n%s", diff) } }) } } // single test case => not using tables. func Test_notControllableError_NotControllable(t *testing.T) { err := notControllableError{ errors.New("test-error"), } if !err.NotControllable() { t.Errorf("NotControllable(): false") } } // single test case => not using tables. func Test_notAllowedError_NotAllowed(t *testing.T) { err := notAllowedError{ errors.New("test-error"), } if !err.NotAllowed() { t.Errorf("NotAllowed(): false") } } func TestIsAPIErrorWrapped(t *testing.T) { testCases := map[string]struct { err error want bool }{ "NoError": { want: false, }, "NotAPIError": { err: errors.New("test-error"), want: false, }, "APIError": { err: kerrors.NewNotFound(schema.GroupResource{}, "test-resource"), want: true, }, "WrappedAPIError": { err: errors.Wrap( kerrors.NewNotFound(schema.GroupResource{}, "test-resource"), "test-wrapper"), want: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { if got := IsAPIErrorWrapped(tc.err); got != tc.want { t.Errorf("IsAPIErrorWrapped() = %v, want %v", got, tc.want) } }) } } func TestNewApplicatorWithRetry(t *testing.T) { type args struct { applicator Applicator shouldRetry shouldRetryFunc backoff *wait.Backoff } testCases := map[string]struct { args args want Applicator }{ "DefaultBackoff": { args: args{}, want: &ApplicatorWithRetry{ backoff: retry.DefaultRetry, }, }, "CustomBackoff": { args: args{ backoff: &testBackoff, }, want: &ApplicatorWithRetry{ backoff: testBackoff, }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { if diff := cmp.Diff(tc.want, NewApplicatorWithRetry(tc.args.applicator, tc.args.shouldRetry, tc.args.backoff), cmp.AllowUnexported(ApplicatorWithRetry{})); diff != "" { t.Errorf("NewApplicatorWithRetry(...): -want, +got:\n%s", diff) } }) } } type mockApplicator struct { returnError bool count uint } func (m *mockApplicator) Apply(_ context.Context, _ client.Object, _ ...ApplyOption) error { m.count++ if m.returnError { return errTest } return nil } func TestApplicatorWithRetry_Apply(t *testing.T) { type fields struct { applicator Applicator shouldRetry shouldRetryFunc backoff wait.Backoff } type args struct { ctx context.Context c client.Object opts []ApplyOption } testCases := map[string]struct { fields fields args args wantErr error wantCount uint }{ "NoRetry": { fields: fields{ applicator: &mockApplicator{returnError: true}, shouldRetry: func(_ error) bool { return false }, backoff: wait.Backoff{Steps: testSteps}, }, args: args{}, wantErr: errTest, wantCount: 1, }, "ShouldRetry": { fields: fields{ applicator: &mockApplicator{returnError: true}, shouldRetry: func(_ error) bool { return true }, backoff: wait.Backoff{Steps: testSteps}, }, args: args{}, wantErr: errTest, wantCount: testSteps, }, "NoError": { fields: fields{ applicator: &mockApplicator{}, shouldRetry: func(_ error) bool { return true }, backoff: wait.Backoff{Steps: testSteps}, }, args: args{}, wantErr: nil, wantCount: 1, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { awr := &ApplicatorWithRetry{ Applicator: tc.fields.applicator, shouldRetry: tc.fields.shouldRetry, backoff: tc.fields.backoff, } if diff := cmp.Diff(tc.wantErr, awr.Apply(tc.args.ctx, tc.args.c, tc.args.opts...), test.EquateErrors()); diff != "" { t.Fatalf("ApplicatorWithRetry.Apply(...): -want, +got:\n%s", diff) } if diff := cmp.Diff(awr.Applicator.(*mockApplicator).count, tc.wantCount); diff != "" { t.Errorf("Retry count mismatch: -want, +got:\n%s", diff) } }) } } func TestUpdate(t *testing.T) { type args struct { fn func(current, desired runtime.Object) current runtime.Object desired runtime.Object } tests := map[string]struct { args args want runtime.Object }{ "Update": { args: args{ fn: func(current, desired runtime.Object) { c, d := current.(*corev1.Secret), desired.(*corev1.Secret) c.StringData = d.StringData }, current: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "current", }, }, desired: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "desired", }, StringData: map[string]string{ "key": "value", }, }, }, want: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "current", }, StringData: map[string]string{ "key": "value", }, }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { if err := UpdateFn(tt.args.fn)(nil, tt.args.current, tt.args.desired); err != nil { t.Fatalf("ApplyOption() = %v, want %v", err, nil) } if diff := cmp.Diff(tt.want, tt.args.current); diff != "" { t.Errorf("UpdateFn updated object mismatch: -want, +got: %s", diff) } }) } } func TestFirstNAndSomeMore(t *testing.T) { type args struct { n int names []string } tests := []struct { name string args args want string }{ {args: args{n: 3, names: []string{"a", "b", "c", "d", "e"}}, want: "a, b, c, and 2 more"}, {args: args{n: 3, names: []string{"a", "b", "c"}}, want: "a, b, and c"}, {args: args{n: 3, names: []string{"a", "b"}}, want: "a, b"}, {args: args{n: 3, names: []string{"a"}}, want: "a"}, {args: args{n: 3, names: []string{}}, want: ""}, {args: args{n: 3, names: []string{"a", "c", "e", "b", "d"}}, want: "a, c, e, and 2 more"}, {args: args{n: 3, names: []string{"a", "b", "b", "b", "d"}}, want: "a, b, b, and 2 more"}, //nolint:dupword // Intentional. {args: args{n: 2, names: []string{"a", "c", "e", "b", "d"}}, want: "a, c, and 3 more"}, {args: args{n: 0, names: []string{"a", "c", "e", "b", "d"}}, want: "5"}, {args: args{n: -7, names: []string{"a", "c", "e", "b", "d"}}, want: "5"}, {args: args{n: 1, names: []string{"a", "c", "e", "b", "d"}}, want: "a, and 4 more"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := FirstNAndSomeMore(tt.args.n, tt.args.names); got != tt.want { t.Errorf("FirstNAndSomeMore() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/resource/unstructured/claim/claim.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package claim contains an unstructured composite resource claim. package claim import ( xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/crossplane/crossplane-runtime/v2/pkg/fieldpath" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/reference" ) // An Option modifies an unstructured composite resource claim. type Option func(*Unstructured) // WithGroupVersionKind sets the GroupVersionKind of the unstructured composite // resource claim. func WithGroupVersionKind(gvk schema.GroupVersionKind) Option { return func(c *Unstructured) { c.SetGroupVersionKind(gvk) } } // WithConditions returns an Option that sets the supplied conditions on an // unstructured composite resource claim. func WithConditions(c ...xpv2.Condition) Option { return func(cr *Unstructured) { cr.SetConditions(c...) } } // New returns a new unstructured composite resource claim. func New(opts ...Option) *Unstructured { c := &Unstructured{Unstructured: unstructured.Unstructured{Object: make(map[string]any)}} for _, f := range opts { f(c) } return c } // +k8s:deepcopy-gen=true // +kubebuilder:object:root=true // An Unstructured composite resource claim. type Unstructured struct { unstructured.Unstructured } // GetUnstructured returns the underlying *unstructured.Unstructured. func (c *Unstructured) GetUnstructured() *unstructured.Unstructured { return &c.Unstructured } // GetCompositionSelector of this composite resource claim. func (c *Unstructured) GetCompositionSelector() *metav1.LabelSelector { out := &metav1.LabelSelector{} if err := fieldpath.Pave(c.Object).GetValueInto("spec.compositionSelector", out); err != nil { return nil } return out } // SetCompositionSelector of this composite resource claim. func (c *Unstructured) SetCompositionSelector(sel *metav1.LabelSelector) { _ = fieldpath.Pave(c.Object).SetValue("spec.compositionSelector", sel) } // GetCompositionReference of this composite resource claim. func (c *Unstructured) GetCompositionReference() *corev1.ObjectReference { out := &corev1.ObjectReference{} if err := fieldpath.Pave(c.Object).GetValueInto("spec.compositionRef", out); err != nil { return nil } return out } // SetCompositionReference of this composite resource claim. func (c *Unstructured) SetCompositionReference(ref *corev1.ObjectReference) { _ = fieldpath.Pave(c.Object).SetValue("spec.compositionRef", ref) } // GetCompositionRevisionReference of this resource claim. func (c *Unstructured) GetCompositionRevisionReference() *corev1.LocalObjectReference { out := &corev1.LocalObjectReference{} if err := fieldpath.Pave(c.Object).GetValueInto("spec.compositionRevisionRef", out); err != nil { return nil } return out } // SetCompositionRevisionReference of this resource claim. func (c *Unstructured) SetCompositionRevisionReference(ref *corev1.LocalObjectReference) { _ = fieldpath.Pave(c.Object).SetValue("spec.compositionRevisionRef", ref) } // GetCompositionRevisionSelector of this resource claim. func (c *Unstructured) GetCompositionRevisionSelector() *metav1.LabelSelector { out := &metav1.LabelSelector{} if err := fieldpath.Pave(c.Object).GetValueInto("spec.compositionRevisionSelector", out); err != nil { return nil } return out } // SetCompositionRevisionSelector of this resource claim. func (c *Unstructured) SetCompositionRevisionSelector(ref *metav1.LabelSelector) { _ = fieldpath.Pave(c.Object).SetValue("spec.compositionRevisionSelector", ref) } // SetCompositionUpdatePolicy of this resource claim. func (c *Unstructured) SetCompositionUpdatePolicy(p *xpv2.UpdatePolicy) { _ = fieldpath.Pave(c.Object).SetValue("spec.compositionUpdatePolicy", p) } // GetCompositionUpdatePolicy of this resource claim. func (c *Unstructured) GetCompositionUpdatePolicy() *xpv2.UpdatePolicy { p, err := fieldpath.Pave(c.Object).GetString("spec.compositionUpdatePolicy") if err != nil { return nil } out := xpv2.UpdatePolicy(p) return &out } // SetCompositeDeletePolicy of this resource claim. func (c *Unstructured) SetCompositeDeletePolicy(p *xpv2.CompositeDeletePolicy) { _ = fieldpath.Pave(c.Object).SetValue("spec.compositeDeletePolicy", p) } // GetCompositeDeletePolicy of this resource claim. func (c *Unstructured) GetCompositeDeletePolicy() *xpv2.CompositeDeletePolicy { p, err := fieldpath.Pave(c.Object).GetString("spec.compositeDeletePolicy") if err != nil { return nil } out := xpv2.CompositeDeletePolicy(p) return &out } // GetResourceReference of this composite resource claim. func (c *Unstructured) GetResourceReference() *reference.Composite { out := &reference.Composite{} if err := fieldpath.Pave(c.Object).GetValueInto("spec.resourceRef", out); err != nil { return nil } return out } // SetResourceReference of this composite resource claim. func (c *Unstructured) SetResourceReference(ref *reference.Composite) { _ = fieldpath.Pave(c.Object).SetValue("spec.resourceRef", ref) } // GetReference returns reference to this claim. func (c *Unstructured) GetReference() *reference.Claim { return &reference.Claim{ APIVersion: c.GetAPIVersion(), Kind: c.GetKind(), Name: c.GetName(), Namespace: c.GetNamespace(), } } // GetWriteConnectionSecretToReference of this composite resource claim. func (c *Unstructured) GetWriteConnectionSecretToReference() *xpv2.LocalSecretReference { out := &xpv2.LocalSecretReference{} if err := fieldpath.Pave(c.Object).GetValueInto("spec.writeConnectionSecretToRef", out); err != nil { return nil } return out } // SetWriteConnectionSecretToReference of this composite resource claim. func (c *Unstructured) SetWriteConnectionSecretToReference(ref *xpv2.LocalSecretReference) { _ = fieldpath.Pave(c.Object).SetValue("spec.writeConnectionSecretToRef", ref) } // GetCondition of this composite resource claim. func (c *Unstructured) GetCondition(ct xpv2.ConditionType) xpv2.Condition { conditioned := xpv2.ConditionedStatus{} // The path is directly `status` because conditions are inline. if err := fieldpath.Pave(c.Object).GetValueInto("status", &conditioned); err != nil { return xpv2.Condition{} } return conditioned.GetCondition(ct) } // SetConditions of this composite resource claim. func (c *Unstructured) SetConditions(conditions ...xpv2.Condition) { conditioned := xpv2.ConditionedStatus{} // The path is directly `status` because conditions are inline. _ = fieldpath.Pave(c.Object).GetValueInto("status", &conditioned) conditioned.SetConditions(conditions...) _ = fieldpath.Pave(c.Object).SetValue("status.conditions", conditioned.Conditions) } // GetConnectionDetailsLastPublishedTime of this composite resource claim. func (c *Unstructured) GetConnectionDetailsLastPublishedTime() *metav1.Time { out := &metav1.Time{} if err := fieldpath.Pave(c.Object).GetValueInto("status.connectionDetails.lastPublishedTime", out); err != nil { return nil } return out } // SetConnectionDetailsLastPublishedTime of this composite resource claim. func (c *Unstructured) SetConnectionDetailsLastPublishedTime(t *metav1.Time) { _ = fieldpath.Pave(c.Object).SetValue("status.connectionDetails.lastPublishedTime", t) } // SetObservedGeneration of this composite resource claim. func (c *Unstructured) SetObservedGeneration(generation int64) { status := &xpv2.ObservedStatus{} _ = fieldpath.Pave(c.Object).GetValueInto("status", status) status.SetObservedGeneration(generation) _ = fieldpath.Pave(c.Object).SetValue("status.observedGeneration", status.ObservedGeneration) } // GetObservedGeneration of this composite resource claim. func (c *Unstructured) GetObservedGeneration() int64 { status := &xpv2.ObservedStatus{} _ = fieldpath.Pave(c.Object).GetValueInto("status", status) return status.GetObservedGeneration() } ================================================ FILE: pkg/resource/unstructured/claim/claim_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package claim import ( "encoding/json" "testing" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/reference" ) var _ client.Object = &Unstructured{} func TestWithGroupVersionKind(t *testing.T) { gvk := schema.GroupVersionKind{ Group: "g", Version: "v1", Kind: "k", } cases := map[string]struct { gvk schema.GroupVersionKind want *Unstructured }{ "New": { gvk: gvk, want: &Unstructured{ Unstructured: unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "g/v1", "kind": "k", }, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := New(WithGroupVersionKind(tc.gvk)) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("New(WithGroupVersionKind(...): -want, +got:\n%s", diff) } }) } } func TestConditions(t *testing.T) { cases := map[string]struct { reason string u *Unstructured set []xpv2.Condition get xpv2.ConditionType want xpv2.Condition }{ "NewCondition": { reason: "It should be possible to set a condition of an empty Unstructured.", u: New(), set: []xpv2.Condition{xpv2.Available(), xpv2.ReconcileSuccess()}, get: xpv2.TypeReady, want: xpv2.Available(), }, "ExistingCondition": { reason: "It should be possible to overwrite a condition that is already set.", u: New(WithConditions(xpv2.Creating())), set: []xpv2.Condition{xpv2.Available()}, get: xpv2.TypeReady, want: xpv2.Available(), }, "WeirdStatus": { reason: "It should not be possible to set a condition when status is not an object.", u: &Unstructured{unstructured.Unstructured{Object: map[string]any{ "status": "wat", }}}, set: []xpv2.Condition{xpv2.Available()}, get: xpv2.TypeReady, want: xpv2.Condition{}, }, "WeirdStatusConditions": { reason: "Conditions should be overwritten if they are not an object.", u: &Unstructured{unstructured.Unstructured{Object: map[string]any{ "status": map[string]any{ "conditions": "wat", }, }}}, set: []xpv2.Condition{xpv2.Available()}, get: xpv2.TypeReady, want: xpv2.Available(), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetConditions(tc.set...) got := tc.u.GetCondition(tc.get) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\n%s\nu.GetCondition(%s): -want, +got:\n%s", tc.reason, tc.get, diff) } }) } } func TestCompositionSelector(t *testing.T) { sel := &metav1.LabelSelector{MatchLabels: map[string]string{"cool": "very"}} cases := map[string]struct { u *Unstructured set *metav1.LabelSelector want *metav1.LabelSelector }{ "NewSel": { u: New(), set: sel, want: sel, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionSelector(tc.set) got := tc.u.GetCompositionSelector() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionSelector(): -want, +got:\n%s", diff) } }) } } func TestCompositionReference(t *testing.T) { ref := &corev1.ObjectReference{Namespace: "ns", Name: "cool"} cases := map[string]struct { u *Unstructured set *corev1.ObjectReference want *corev1.ObjectReference }{ "NewRef": { u: New(), set: ref, want: ref, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionReference(tc.set) got := tc.u.GetCompositionReference() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionReference(): -want, +got:\n%s", diff) } }) } } func TestCompositionRevisionReference(t *testing.T) { ref := &corev1.LocalObjectReference{Name: "cool"} cases := map[string]struct { u *Unstructured set *corev1.LocalObjectReference want *corev1.LocalObjectReference }{ "NewRef": { u: New(), set: ref, want: ref, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionRevisionReference(tc.set) got := tc.u.GetCompositionRevisionReference() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionRevisionReference(): -want, +got:\n%s", diff) } }) } } func TestCompositionRevisionSelector(t *testing.T) { sel := &metav1.LabelSelector{MatchLabels: map[string]string{"cool": "very"}} cases := map[string]struct { u *Unstructured set *metav1.LabelSelector want *metav1.LabelSelector }{ "NewRef": { u: New(), set: sel, want: sel, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionRevisionSelector(tc.set) got := tc.u.GetCompositionRevisionSelector() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionRevisionSelector(): -want, +got:\n%s", diff) } }) } } func TestCompositionUpdatePolicy(t *testing.T) { p := xpv2.UpdateManual cases := map[string]struct { u *Unstructured set *xpv2.UpdatePolicy want *xpv2.UpdatePolicy }{ "NewRef": { u: New(), set: &p, want: &p, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionUpdatePolicy(tc.set) got := tc.u.GetCompositionUpdatePolicy() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionUpdatePolicy(): -want, +got:\n%s", diff) } }) } } func TestCompositeDeletePolicy(t *testing.T) { p := xpv2.CompositeDeleteBackground cases := map[string]struct { u *Unstructured set *xpv2.CompositeDeletePolicy want *xpv2.CompositeDeletePolicy }{ "NewRef": { u: New(), set: &p, want: &p, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositeDeletePolicy(tc.set) got := tc.u.GetCompositeDeletePolicy() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositeDeletePolicy(): -want, +got:\n%s", diff) } }) } } func TestResourceReference(t *testing.T) { ref := &reference.Composite{Name: "cool"} cases := map[string]struct { u *Unstructured set *reference.Composite want *reference.Composite }{ "NewRef": { u: New(), set: ref, want: ref, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetResourceReference(tc.set) got := tc.u.GetResourceReference() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetResourceReference(): -want, +got:\n%s", diff) } }) } } func TestClaimReference(t *testing.T) { ref := &reference.Claim{Namespace: "ns", Name: "cool", APIVersion: "foo.com/v1", Kind: "Foo"} u := &Unstructured{} u.SetName(ref.Name) u.SetNamespace(ref.Namespace) u.SetAPIVersion(ref.APIVersion) u.SetKind(ref.Kind) got := u.GetReference() if diff := cmp.Diff(ref, got); diff != "" { t.Errorf("\nu.GetClaimReference(): -want, +got:\n%s", diff) } } func TestWriteConnectionSecretToReference(t *testing.T) { ref := &xpv2.LocalSecretReference{Name: "cool"} cases := map[string]struct { u *Unstructured set *xpv2.LocalSecretReference want *xpv2.LocalSecretReference }{ "NewRef": { u: New(), set: ref, want: ref, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetWriteConnectionSecretToReference(tc.set) got := tc.u.GetWriteConnectionSecretToReference() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetWriteConnectionSecretToReference(): -want, +got:\n%s", diff) } }) } } func TestConnectionDetailsLastPublishedTime(t *testing.T) { now := &metav1.Time{Time: time.Now()} // The timestamp loses a little resolution when round-tripped through JSON // encoding. lores := func(t *metav1.Time) *metav1.Time { out := &metav1.Time{} j, _ := json.Marshal(t) //nolint:errchkjson // No encoding issue in practice. _ = json.Unmarshal(j, out) return out } cases := map[string]struct { u *Unstructured set *metav1.Time want *metav1.Time }{ "NewTime": { u: New(), set: now, want: lores(now), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetConnectionDetailsLastPublishedTime(tc.set) got := tc.u.GetConnectionDetailsLastPublishedTime() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetConnectionDetailsLastPublishedTime(): -want, +got:\n%s", diff) } }) } } func TestObservedGeneration(t *testing.T) { cases := map[string]struct { u *Unstructured want int64 }{ "Set": { u: New(func(u *Unstructured) { u.SetObservedGeneration(123) }), want: 123, }, "NotFound": { u: New(), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := tc.u.GetObservedGeneration() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetObservedGeneration(): -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/resource/unstructured/claim/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by controller-gen. DO NOT EDIT. package claim import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Unstructured) DeepCopyInto(out *Unstructured) { *out = *in in.Unstructured.DeepCopyInto(&out.Unstructured) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Unstructured. func (in *Unstructured) DeepCopy() *Unstructured { if in == nil { return nil } out := new(Unstructured) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Unstructured) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } ================================================ FILE: pkg/resource/unstructured/client.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package unstructured contains utilities unstructured Kubernetes objects. package unstructured import ( "context" "fmt" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" ) // Wrapper returns the underlying *unstructured.Unstructured. type Wrapper interface { GetUnstructured() *unstructured.Unstructured } // ListWrapper allows the *unstructured.UnstructuredList to be accessed. type ListWrapper interface { GetUnstructuredList() *unstructured.UnstructuredList } // NewClient returns a client.Client that will operate on the underlying // *unstructured.Unstructured if the object satisfies the Wrapper or ListWrapper // interfaces. It relies on *unstructured.Unstructured instead of simpler // map[string]any to avoid unnecessary copying. func NewClient(c client.Client) *WrapperClient { return &WrapperClient{kube: c} } // A WrapperClient is a client.Client that will operate on the underlying // *unstructured.Unstructured if the object satisfies the Wrapper or ListWrapper // interfaces. type WrapperClient struct { kube client.Client } // Get retrieves an obj for the given object key from the Kubernetes Cluster. // obj must be a struct pointer so that obj can be updated with the response // returned by the Server. func (c *WrapperClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { if u, ok := obj.(Wrapper); ok { return c.kube.Get(ctx, key, u.GetUnstructured(), opts...) } return c.kube.Get(ctx, key, obj, opts...) } // List retrieves list of objects for a given namespace and list options. On a // successful call, Items field in the list will be populated with the // result returned from the server. func (c *WrapperClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { if u, ok := list.(ListWrapper); ok { return c.kube.List(ctx, u.GetUnstructuredList(), opts...) } return c.kube.List(ctx, list, opts...) } // Create saves the object obj in the Kubernetes cluster. func (c *WrapperClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { if u, ok := obj.(Wrapper); ok { return c.kube.Create(ctx, u.GetUnstructured(), opts...) } return c.kube.Create(ctx, obj, opts...) } // Delete deletes the given obj from Kubernetes cluster. func (c *WrapperClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { if u, ok := obj.(Wrapper); ok { return c.kube.Delete(ctx, u.GetUnstructured(), opts...) } return c.kube.Delete(ctx, obj, opts...) } // Update updates the given obj in the Kubernetes cluster. obj must be a // struct pointer so that obj can be updated with the content returned by the Server. func (c *WrapperClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { if u, ok := obj.(Wrapper); ok { return c.kube.Update(ctx, u.GetUnstructured(), opts...) } return c.kube.Update(ctx, obj, opts...) } // Patch patches the given obj in the Kubernetes cluster. obj must be a // struct pointer so that obj can be updated with the content returned by the Server. func (c *WrapperClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { if u, ok := obj.(Wrapper); ok { return c.kube.Patch(ctx, u.GetUnstructured(), patch, opts...) } return c.kube.Patch(ctx, obj, patch, opts...) } // Apply applies the given apply configuration to the Kubernetes cluster. func (c *WrapperClient) Apply(ctx context.Context, config runtime.ApplyConfiguration, opts ...client.ApplyOption) error { return c.kube.Apply(ctx, config, opts...) } // DeleteAllOf deletes all objects of the given type matching the given options. func (c *WrapperClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { if u, ok := obj.(Wrapper); ok { return c.kube.DeleteAllOf(ctx, u.GetUnstructured(), opts...) } return c.kube.DeleteAllOf(ctx, obj, opts...) } // Status returns a client for the Status subresource. func (c *WrapperClient) Status() client.StatusWriter { return &wrapperStatusClient{ kube: c.kube.Status(), } } // SubResource returns the underlying client's SubResource client, unwrapped. func (c *WrapperClient) SubResource(subResource string) client.SubResourceClient { // TODO(negz): Is there anything to wrap here? return c.kube.SubResource(subResource) } // Scheme returns the scheme this client is using. func (c *WrapperClient) Scheme() *runtime.Scheme { return c.kube.Scheme() } // RESTMapper returns the rest this client is using. func (c *WrapperClient) RESTMapper() meta.RESTMapper { return c.kube.RESTMapper() } // GroupVersionKindFor returns the GVK for the given obj. func (c *WrapperClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { if u, ok := obj.(Wrapper); ok { return c.kube.GroupVersionKindFor(u.GetUnstructured()) } return c.kube.GroupVersionKindFor(obj) } // IsObjectNamespaced checks whether the object is namespaced. func (c *WrapperClient) IsObjectNamespaced(obj runtime.Object) (bool, error) { if u, ok := obj.(Wrapper); ok { return c.kube.IsObjectNamespaced(u.GetUnstructured()) } return c.kube.IsObjectNamespaced(obj) } type wrapperStatusClient struct { kube client.StatusWriter } // Create creates the fields corresponding to the status subresource for the // given obj. obj must be a struct pointer so that obj can be updated // with the content returned by the Server. func (c *wrapperStatusClient) Create(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceCreateOption) error { // TODO(negz): Could subResource be wrapped? if u, ok := obj.(Wrapper); ok { return c.kube.Create(ctx, u.GetUnstructured(), subResource, opts...) } return c.kube.Create(ctx, obj, subResource, opts...) } // Update updates the fields corresponding to the status subresource for the // given obj. obj must be a struct pointer so that obj can be updated // with the content returned by the Server. func (c *wrapperStatusClient) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error { if u, ok := obj.(Wrapper); ok { return c.kube.Update(ctx, u.GetUnstructured(), opts...) } return c.kube.Update(ctx, obj, opts...) } // Patch patches the given object's subresource. obj must be a struct // pointer so that obj can be updated with the content returned by the // Server. func (c *wrapperStatusClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error { if u, ok := obj.(Wrapper); ok { return c.kube.Patch(ctx, u.GetUnstructured(), patch, opts...) } return c.kube.Patch(ctx, obj, patch, opts...) } // Apply applies the given object's subresource. obj must be a struct // pointer so that obj can be updated with the content returned by the // Server. Returns an error if an unstructured object is used due to // how the underlying client works. This method is only added to // satisfy the client.SubResourceWriter interface. func (c *wrapperStatusClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { if u, ok := obj.(Wrapper); ok { return fmt.Errorf("cannot apply status for unstructured %T; use a typed ApplyConfiguration or Patch with ApplyPatchType", u) } return c.kube.Apply(ctx, obj, opts...) } ================================================ FILE: pkg/resource/unstructured/client_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package unstructured import ( "context" "errors" "testing" "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var ( nameWrapped = "wrapped" nameUnwrapped = "unwrapped" errWrapped = errors.New("unexpected Wrapped object") errUnwrapped = errors.New("unexpected Unwrapped object") ) var _ client.Client = &WrapperClient{} type Wrapped struct{ client.Object } func (w *Wrapped) GetUnstructured() *unstructured.Unstructured { return &unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": nameWrapped, }, }} } func NewWrapped() *Wrapped { return &Wrapped{} } type WrappedList struct{ client.ObjectList } func (w *WrappedList) GetUnstructuredList() *unstructured.UnstructuredList { u := NewWrapped().GetUnstructured() return &unstructured.UnstructuredList{Items: []unstructured.Unstructured{*u}} } func NewWrappedList() *WrappedList { return &WrappedList{} } func NewUnwrapped() *unstructured.Unstructured { return &unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": nameUnwrapped, }, }} } func NewUnwrappedList() *unstructured.UnstructuredList { u := NewUnwrapped() return &unstructured.UnstructuredList{Items: []unstructured.Unstructured{*u}} } func TestGet(t *testing.T) { type args struct { ctx context.Context key client.ObjectKey obj client.Object } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrapped()}, }, "Wrapped": { c: &test.MockClient{MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrapped()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.Get(tc.args.ctx, tc.args.key, tc.args.obj) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.Get(...): -want error, +got error:\n %s", diff) } }) } } func TestList(t *testing.T) { type args struct { ctx context.Context obj client.ObjectList } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error { u := &obj.(*unstructured.UnstructuredList).Items[0] if u.GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrappedList()}, }, "Wrapped": { c: &test.MockClient{MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error { u := &obj.(*unstructured.UnstructuredList).Items[0] if u.GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrappedList()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.List(tc.args.ctx, tc.args.obj) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.List(...): -want error, +got error:\n %s", diff) } }) } } func TestCreate(t *testing.T) { type args struct { ctx context.Context obj client.Object } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockCreate: test.NewMockCreateFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrapped()}, }, "Wrapped": { c: &test.MockClient{MockCreate: test.NewMockCreateFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrapped()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.Create(tc.args.ctx, tc.args.obj) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.Create(...): -want error, +got error:\n %s", diff) } }) } } func TestDelete(t *testing.T) { type args struct { ctx context.Context obj client.Object } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockDelete: test.NewMockDeleteFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrapped()}, }, "Wrapped": { c: &test.MockClient{MockDelete: test.NewMockDeleteFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrapped()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.Delete(tc.args.ctx, tc.args.obj) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.Delete(...): -want error, +got error:\n %s", diff) } }) } } func TestUpdate(t *testing.T) { type args struct { ctx context.Context obj client.Object } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrapped()}, }, "Wrapped": { c: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrapped()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.Update(tc.args.ctx, tc.args.obj) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.Update(...): -want error, +got error:\n %s", diff) } }) } } func TestPatch(t *testing.T) { type args struct { ctx context.Context obj client.Object patch client.Patch } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockPatch: test.NewMockPatchFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrapped()}, }, "Wrapped": { c: &test.MockClient{MockPatch: test.NewMockPatchFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrapped()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.Patch(tc.args.ctx, tc.args.obj, tc.args.patch) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.Patch(...): -want error, +got error:\n %s", diff) } }) } } func TestDeleteAllOf(t *testing.T) { type args struct { ctx context.Context obj client.Object } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockDeleteAllOf: test.NewMockDeleteAllOfFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrapped()}, }, "Wrapped": { c: &test.MockClient{MockDeleteAllOf: test.NewMockDeleteAllOfFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrapped()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.DeleteAllOf(tc.args.ctx, tc.args.obj) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.DeleteAllOf(...): -want error, +got error:\n %s", diff) } }) } } func TestStatusCreate(t *testing.T) { type args struct { ctx context.Context obj client.Object sub client.Object } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockStatusCreate: test.NewMockSubResourceCreateFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrapped()}, }, "Wrapped": { c: &test.MockClient{MockStatusCreate: test.NewMockSubResourceCreateFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrapped()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.Status().Create(tc.args.ctx, tc.args.obj, tc.args.sub) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.Status().Create(...): -want error, +got error:\n %s", diff) } }) } } func TestStatusUpdate(t *testing.T) { type args struct { ctx context.Context obj client.Object } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrapped()}, }, "Wrapped": { c: &test.MockClient{MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrapped()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.Status().Update(tc.args.ctx, tc.args.obj) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.Status().Update(...): -want error, +got error:\n %s", diff) } }) } } func TestStatusPatch(t *testing.T) { type args struct { ctx context.Context obj client.Object patch client.Patch } cases := map[string]struct { c client.Client args args want error }{ "Unwrapped": { c: &test.MockClient{MockStatusPatch: test.NewMockSubResourcePatchFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameUnwrapped { return errWrapped } return nil })}, args: args{obj: NewUnwrapped()}, }, "Wrapped": { c: &test.MockClient{MockStatusPatch: test.NewMockSubResourcePatchFn(nil, func(obj client.Object) error { if obj.(metav1.Object).GetName() != nameWrapped { return errUnwrapped } return nil })}, args: args{obj: NewWrapped()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { c := NewClient(tc.c) got := c.Status().Patch(tc.args.ctx, tc.args.obj, tc.args.patch) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("\nc.StatusPatch(...): -want error, +got error:\n %s", diff) } }) } } ================================================ FILE: pkg/resource/unstructured/composed/composed.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package composed contains an unstructured composed resource. package composed import ( xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "github.com/crossplane/crossplane-runtime/v2/pkg/fieldpath" ) // An Option modifies an unstructured composed resource. type Option func(resource *Unstructured) // FromReference returns an Option that propagates the metadata in the supplied // reference to an unstructured composed resource. func FromReference(ref corev1.ObjectReference) Option { return func(cr *Unstructured) { cr.SetGroupVersionKind(ref.GroupVersionKind()) cr.SetName(ref.Name) cr.SetNamespace(ref.Namespace) cr.SetUID(ref.UID) } } // WithConditions returns an Option that sets the supplied conditions on an // unstructured composed resource. func WithConditions(c ...xpv2.Condition) Option { return func(cr *Unstructured) { cr.SetConditions(c...) } } // New returns a new unstructured composed resource. func New(opts ...Option) *Unstructured { cr := &Unstructured{unstructured.Unstructured{Object: make(map[string]any)}} for _, f := range opts { f(cr) } return cr } // +k8s:deepcopy-gen=true // +kubebuilder:object:root=true // An Unstructured composed resource. type Unstructured struct { unstructured.Unstructured } // GetUnstructured returns the underlying *unstructured.Unstructured. func (cr *Unstructured) GetUnstructured() *unstructured.Unstructured { return &cr.Unstructured } // GetCondition of this Composed resource. func (cr *Unstructured) GetCondition(ct xpv2.ConditionType) xpv2.Condition { conditioned := xpv2.ConditionedStatus{} // The path is directly `status` because conditions are inline. if err := fieldpath.Pave(cr.Object).GetValueInto("status", &conditioned); err != nil { return xpv2.Condition{} } return conditioned.GetCondition(ct) } // SetConditions of this Composed resource. func (cr *Unstructured) SetConditions(c ...xpv2.Condition) { conditioned := xpv2.ConditionedStatus{} // The path is directly `status` because conditions are inline. _ = fieldpath.Pave(cr.Object).GetValueInto("status", &conditioned) conditioned.SetConditions(c...) _ = fieldpath.Pave(cr.Object).SetValue("status.conditions", conditioned.Conditions) } // GetWriteConnectionSecretToReference of this Composed resource. func (cr *Unstructured) GetWriteConnectionSecretToReference() *xpv2.SecretReference { out := &xpv2.SecretReference{} if err := fieldpath.Pave(cr.Object).GetValueInto("spec.writeConnectionSecretToRef", out); err != nil { return nil } return out } // SetWriteConnectionSecretToReference of this Composed resource. func (cr *Unstructured) SetWriteConnectionSecretToReference(r *xpv2.SecretReference) { _ = fieldpath.Pave(cr.Object).SetValue("spec.writeConnectionSecretToRef", r) } // OwnedBy returns true if the supplied UID is an owner of the composed. func (cr *Unstructured) OwnedBy(u types.UID) bool { for _, owner := range cr.GetOwnerReferences() { if owner.UID == u { return true } } return false } // RemoveOwnerRef removes the supplied UID from the composed resource's owner. func (cr *Unstructured) RemoveOwnerRef(u types.UID) { refs := cr.GetOwnerReferences() for i := range refs { if refs[i].UID == u { cr.SetOwnerReferences(append(refs[:i], refs[i+1:]...)) return } } } // An ListOption modifies an unstructured list of composed resource. type ListOption func(*UnstructuredList) // FromReferenceToList returns a ListOption that propagates the metadata in the // supplied reference to an unstructured list composed resource. func FromReferenceToList(ref corev1.ObjectReference) ListOption { return func(list *UnstructuredList) { list.SetAPIVersion(ref.APIVersion) list.SetKind(ref.Kind + "List") } } // NewList returns a new unstructured list of composed resources. func NewList(opts ...ListOption) *UnstructuredList { cr := &UnstructuredList{unstructured.UnstructuredList{Object: make(map[string]any)}} for _, f := range opts { f(cr) } return cr } // An UnstructuredList of composed resources. type UnstructuredList struct { unstructured.UnstructuredList } // GetUnstructuredList returns the underlying *unstructured.Unstructured. func (cr *UnstructuredList) GetUnstructuredList() *unstructured.UnstructuredList { return &cr.UnstructuredList } // SetObservedGeneration of this composite resource claim. func (cr *Unstructured) SetObservedGeneration(generation int64) { status := &xpv2.ObservedStatus{} _ = fieldpath.Pave(cr.Object).GetValueInto("status", status) status.SetObservedGeneration(generation) _ = fieldpath.Pave(cr.Object).SetValue("status.observedGeneration", status.ObservedGeneration) } // GetObservedGeneration of this composite resource claim. func (cr *Unstructured) GetObservedGeneration() int64 { status := &xpv2.ObservedStatus{} _ = fieldpath.Pave(cr.Object).GetValueInto("status", status) return status.GetObservedGeneration() } ================================================ FILE: pkg/resource/unstructured/composed/composed_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package composed import ( "testing" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" ) var _ client.Object = &Unstructured{} func TestFromReference(t *testing.T) { ref := corev1.ObjectReference{ APIVersion: "a/v1", Kind: "k", Namespace: "ns", Name: "name", } cases := map[string]struct { ref corev1.ObjectReference want *Unstructured }{ "New": { ref: ref, want: &Unstructured{ Unstructured: unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "a/v1", "kind": "k", "metadata": map[string]any{ "name": "name", "namespace": "ns", }, }, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := New(FromReference(tc.ref)) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("New(FromReference(...): -want, +got:\n%s", diff) } }) } } func TestConditions(t *testing.T) { cases := map[string]struct { reason string u *Unstructured set []xpv2.Condition get xpv2.ConditionType want xpv2.Condition }{ "NewCondition": { reason: "It should be possible to set a condition of an empty Unstructured.", u: New(), set: []xpv2.Condition{xpv2.Available(), xpv2.ReconcileSuccess()}, get: xpv2.TypeReady, want: xpv2.Available(), }, "ExistingCondition": { reason: "It should be possible to overwrite a condition that is already set.", u: New(WithConditions(xpv2.Creating())), set: []xpv2.Condition{xpv2.Available()}, get: xpv2.TypeReady, want: xpv2.Available(), }, "WeirdStatus": { reason: "It should not be possible to set a condition when status is not an object.", u: &Unstructured{unstructured.Unstructured{Object: map[string]any{ "status": "wat", }}}, set: []xpv2.Condition{xpv2.Available()}, get: xpv2.TypeReady, want: xpv2.Condition{}, }, "WeirdStatusConditions": { reason: "Conditions should be overwritten if they are not an object.", u: &Unstructured{unstructured.Unstructured{Object: map[string]any{ "status": map[string]any{ "conditions": "wat", }, }}}, set: []xpv2.Condition{xpv2.Available()}, get: xpv2.TypeReady, want: xpv2.Available(), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetConditions(tc.set...) got := tc.u.GetCondition(tc.get) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\n%s\nu.GetCondition(%s): -want, +got:\n%s", tc.reason, tc.get, diff) } }) } } func TestWriteConnectionSecretToReference(t *testing.T) { ref := &xpv2.SecretReference{Namespace: "ns", Name: "cool"} cases := map[string]struct { u *Unstructured set *xpv2.SecretReference want *xpv2.SecretReference }{ "NewRef": { u: New(), set: ref, want: ref, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetWriteConnectionSecretToReference(tc.set) got := tc.u.GetWriteConnectionSecretToReference() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetWriteConnectionSecretToReference(): -want, +got:\n%s", diff) } }) } } func TestObservedGeneration(t *testing.T) { cases := map[string]struct { u *Unstructured want int64 }{ "Set": { u: New(func(u *Unstructured) { u.SetObservedGeneration(123) }), want: 123, }, "NotFound": { u: New(), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := tc.u.GetObservedGeneration() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetObservedGeneration(): -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/resource/unstructured/composed/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by controller-gen. DO NOT EDIT. package composed import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Unstructured) DeepCopyInto(out *Unstructured) { *out = *in in.Unstructured.DeepCopyInto(&out.Unstructured) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Unstructured. func (in *Unstructured) DeepCopy() *Unstructured { if in == nil { return nil } out := new(Unstructured) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Unstructured) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } ================================================ FILE: pkg/resource/unstructured/composite/composite.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package composite contains an unstructured composite resource. package composite import ( xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/fieldpath" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/reference" ) // Schema specifies the schema version of a composite resource's Crossplane // machinery fields. type Schema int const ( // SchemaModern indicates a modern Namespaced or Cluster scope composite // resource. Modern composite resources nest all Crossplane machinery fields // under spec.crossplane and status.crossplane, and can't be claimed. SchemaModern Schema = iota // SchemaLegacy indicates a LegacyCluster scope composite resource. Legacy // composite resources don't nest Crossplane machinery fields - they're set // directly under spec and status. Legacy composite resources can be claimed. SchemaLegacy ) // An Option modifies an unstructured composite resource. type Option func(*Unstructured) // WithGroupVersionKind sets the GroupVersionKind of the composite resource. func WithGroupVersionKind(gvk schema.GroupVersionKind) Option { return func(c *Unstructured) { c.SetGroupVersionKind(gvk) } } // WithConditions sets the supplied conditions on the composite resource. func WithConditions(c ...xpv2.Condition) Option { return func(cr *Unstructured) { cr.SetConditions(c...) } } // WithSchema sets the schema of the composite resource. func WithSchema(s Schema) Option { return func(c *Unstructured) { c.Schema = s } } // New returns a new unstructured composite resource. func New(opts ...Option) *Unstructured { c := &Unstructured{Unstructured: unstructured.Unstructured{Object: make(map[string]any)}} for _, f := range opts { f(c) } return c } // +k8s:deepcopy-gen=true // +kubebuilder:object:root=true // An Unstructured composite resource. type Unstructured struct { unstructured.Unstructured Schema Schema } // GetUnstructured returns the underlying *unstructured.Unstructured. func (c *Unstructured) GetUnstructured() *unstructured.Unstructured { return &c.Unstructured } // GetCompositionSelector of this composite resource. func (c *Unstructured) GetCompositionSelector() *metav1.LabelSelector { path := "spec.crossplane.compositionSelector" if c.Schema == SchemaLegacy { path = "spec.compositionSelector" } out := &metav1.LabelSelector{} if err := fieldpath.Pave(c.Object).GetValueInto(path, out); err != nil { return nil } return out } // SetCompositionSelector of this composite resource. func (c *Unstructured) SetCompositionSelector(sel *metav1.LabelSelector) { path := "spec.crossplane.compositionSelector" if c.Schema == SchemaLegacy { path = "spec.compositionSelector" } _ = fieldpath.Pave(c.Object).SetValue(path, sel) } // GetCompositionReference of this composite resource. func (c *Unstructured) GetCompositionReference() *corev1.ObjectReference { path := "spec.crossplane.compositionRef" if c.Schema == SchemaLegacy { path = "spec.compositionRef" } out := &corev1.ObjectReference{} if err := fieldpath.Pave(c.Object).GetValueInto(path, out); err != nil { return nil } return out } // SetCompositionReference of this composite resource. func (c *Unstructured) SetCompositionReference(ref *corev1.ObjectReference) { path := "spec.crossplane.compositionRef" if c.Schema == SchemaLegacy { path = "spec.compositionRef" } _ = fieldpath.Pave(c.Object).SetValue(path, ref) } // GetCompositionRevisionReference of this composite resource. func (c *Unstructured) GetCompositionRevisionReference() *corev1.LocalObjectReference { path := "spec.crossplane.compositionRevisionRef" if c.Schema == SchemaLegacy { path = "spec.compositionRevisionRef" } out := &corev1.LocalObjectReference{} if err := fieldpath.Pave(c.Object).GetValueInto(path, out); err != nil { return nil } return out } // SetCompositionRevisionReference of this composite resource. func (c *Unstructured) SetCompositionRevisionReference(ref *corev1.LocalObjectReference) { path := "spec.crossplane.compositionRevisionRef" if c.Schema == SchemaLegacy { path = "spec.compositionRevisionRef" } _ = fieldpath.Pave(c.Object).SetValue(path, ref) } // GetCompositionRevisionSelector of this resource claim. func (c *Unstructured) GetCompositionRevisionSelector() *metav1.LabelSelector { path := "spec.crossplane.compositionRevisionSelector" if c.Schema == SchemaLegacy { path = "spec.compositionRevisionSelector" } out := &metav1.LabelSelector{} if err := fieldpath.Pave(c.Object).GetValueInto(path, out); err != nil { return nil } return out } // SetCompositionRevisionSelector of this resource claim. func (c *Unstructured) SetCompositionRevisionSelector(sel *metav1.LabelSelector) { path := "spec.crossplane.compositionRevisionSelector" if c.Schema == SchemaLegacy { path = "spec.compositionRevisionSelector" } _ = fieldpath.Pave(c.Object).SetValue(path, sel) } // SetCompositionUpdatePolicy of this composite resource. func (c *Unstructured) SetCompositionUpdatePolicy(p *xpv2.UpdatePolicy) { path := "spec.crossplane.compositionUpdatePolicy" if c.Schema == SchemaLegacy { path = "spec.compositionUpdatePolicy" } _ = fieldpath.Pave(c.Object).SetValue(path, p) } // GetCompositionUpdatePolicy of this composite resource. func (c *Unstructured) GetCompositionUpdatePolicy() *xpv2.UpdatePolicy { path := "spec.crossplane.compositionUpdatePolicy" if c.Schema == SchemaLegacy { path = "spec.compositionUpdatePolicy" } p, err := fieldpath.Pave(c.Object).GetString(path) if err != nil { return nil } out := xpv2.UpdatePolicy(p) return &out } // GetClaimReference of this composite resource. func (c *Unstructured) GetClaimReference() *reference.Claim { // Only legacy XRs support claims. if c.Schema != SchemaLegacy { return nil } out := &reference.Claim{} if err := fieldpath.Pave(c.Object).GetValueInto("spec.claimRef", out); err != nil { return nil } return out } // SetClaimReference of this composite resource. func (c *Unstructured) SetClaimReference(ref *reference.Claim) { // Only legacy XRs support claims. if c.Schema != SchemaLegacy { return } _ = fieldpath.Pave(c.Object).SetValue("spec.claimRef", ref) } // GetResourceReferences of this composite resource. func (c *Unstructured) GetResourceReferences() []corev1.ObjectReference { path := "spec.crossplane.resourceRefs" if c.Schema == SchemaLegacy { path = "spec.resourceRefs" } out := &[]corev1.ObjectReference{} _ = fieldpath.Pave(c.Object).GetValueInto(path, out) return *out } // SetResourceReferences of this composite resource. func (c *Unstructured) SetResourceReferences(refs []corev1.ObjectReference) { path := "spec.crossplane.resourceRefs" if c.Schema == SchemaLegacy { path = "spec.resourceRefs" } empty := corev1.ObjectReference{} filtered := make([]corev1.ObjectReference, 0, len(refs)) for _, ref := range refs { // TODO(negz): Ask muvaf to explain what this is working around. :) // TODO(muvaf): temporary workaround. if ref.String() == empty.String() { continue } filtered = append(filtered, ref) } _ = fieldpath.Pave(c.Object).SetValue(path, filtered) } // GetReference returns reference to this composite. func (c *Unstructured) GetReference() *reference.Composite { ref := &reference.Composite{ APIVersion: c.GetAPIVersion(), Kind: c.GetKind(), Name: c.GetName(), } if c.GetNamespace() != "" { ref.Namespace = ptr.To(c.GetNamespace()) } return ref } // TODO(negz): Ideally we'd use LocalSecretReference for namespaced XRs. As is // we'll return a SecretReference with an empty namespace if the XR doesn't // actually have a spec.crossplane.writeConnectionSecretToRef.namespace field. // GetWriteConnectionSecretToReference of this composite resource. func (c *Unstructured) GetWriteConnectionSecretToReference() *xpv2.SecretReference { // Only legacy XRs support connection secrets. if c.Schema != SchemaLegacy { return nil } out := &xpv2.SecretReference{} if err := fieldpath.Pave(c.Object).GetValueInto("spec.writeConnectionSecretToRef", out); err != nil { return nil } return out } // SetWriteConnectionSecretToReference of this composite resource. func (c *Unstructured) SetWriteConnectionSecretToReference(ref *xpv2.SecretReference) { // Only legacy XRs support connection secrets. if c.Schema != SchemaLegacy { return } _ = fieldpath.Pave(c.Object).SetValue("spec.writeConnectionSecretToRef", ref) } // GetCondition of this composite resource. func (c *Unstructured) GetCondition(ct xpv2.ConditionType) xpv2.Condition { conditioned := xpv2.ConditionedStatus{} // The path is directly `status` because conditions are inline. if err := fieldpath.Pave(c.Object).GetValueInto("status", &conditioned); err != nil { return xpv2.Condition{} } return conditioned.GetCondition(ct) } // SetConditions of this composite resource. func (c *Unstructured) SetConditions(conditions ...xpv2.Condition) { conditioned := xpv2.ConditionedStatus{} // The path is directly `status` because conditions are inline. _ = fieldpath.Pave(c.Object).GetValueInto("status", &conditioned) conditioned.SetConditions(conditions...) _ = fieldpath.Pave(c.Object).SetValue("status.conditions", conditioned.Conditions) } // GetConditions of this composite resource. func (c *Unstructured) GetConditions() []xpv2.Condition { conditioned := xpv2.ConditionedStatus{} // The path is directly `status` because conditions are inline. _ = fieldpath.Pave(c.Object).GetValueInto("status", &conditioned) return conditioned.Conditions } // GetConnectionDetailsLastPublishedTime of this composite resource. func (c *Unstructured) GetConnectionDetailsLastPublishedTime() *metav1.Time { // Only legacy XRs support connection details. if c.Schema != SchemaLegacy { return nil } out := &metav1.Time{} if err := fieldpath.Pave(c.Object).GetValueInto("status.connectionDetails.lastPublishedTime", out); err != nil { return nil } return out } // SetConnectionDetailsLastPublishedTime of this composite resource. func (c *Unstructured) SetConnectionDetailsLastPublishedTime(t *metav1.Time) { // Only legacy XRs support connection details. if c.Schema != SchemaLegacy { return } _ = fieldpath.Pave(c.Object).SetValue("status.connectionDetails.lastPublishedTime", t) } // SetObservedGeneration of this composite resource claim. func (c *Unstructured) SetObservedGeneration(generation int64) { status := &xpv2.ObservedStatus{} _ = fieldpath.Pave(c.Object).GetValueInto("status", status) status.SetObservedGeneration(generation) _ = fieldpath.Pave(c.Object).SetValue("status.observedGeneration", status.ObservedGeneration) } // GetObservedGeneration of this composite resource claim. func (c *Unstructured) GetObservedGeneration() int64 { status := &xpv2.ObservedStatus{} _ = fieldpath.Pave(c.Object).GetValueInto("status", status) return status.GetObservedGeneration() } // SetLastHandledReconcileAt of this composite resource. func (c *Unstructured) SetLastHandledReconcileAt(token string) { _ = fieldpath.Pave(c.Object).SetValue("status.lastHandledReconcileAt", token) } // GetLastHandledReconcileAt of this composite resource. func (c *Unstructured) GetLastHandledReconcileAt() string { v, err := fieldpath.Pave(c.Object).GetString("status.lastHandledReconcileAt") if err != nil { return "" } return v } // SetClaimConditionTypes of this composite resource. You cannot set system // condition types such as Ready, Synced or Healthy as claim conditions. func (c *Unstructured) SetClaimConditionTypes(in ...xpv2.ConditionType) error { // Only legacy XRs support claims. if c.Schema != SchemaLegacy { return nil } ts := c.GetClaimConditionTypes() m := make(map[xpv2.ConditionType]bool, len(ts)) for _, t := range ts { m[t] = true } for _, t := range in { if xpv2.IsSystemConditionType(t) { return errors.Errorf("cannot set system condition %s as a claim condition", t) } if m[t] { continue } m[t] = true ts = append(ts, t) } _ = fieldpath.Pave(c.Object).SetValue("status.claimConditionTypes", ts) return nil } // GetClaimConditionTypes of this composite resource. func (c *Unstructured) GetClaimConditionTypes() []xpv2.ConditionType { // Only legacy XRs support claims. if c.Schema != SchemaLegacy { return nil } cs := []xpv2.ConditionType{} _ = fieldpath.Pave(c.Object).GetValueInto("status.claimConditionTypes", &cs) return cs } ================================================ FILE: pkg/resource/unstructured/composite/composite_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package composite import ( "encoding/json" "errors" "testing" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/reference" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var _ client.Object = &Unstructured{} func TestWithGroupVersionKind(t *testing.T) { gvk := schema.GroupVersionKind{ Group: "g", Version: "v1", Kind: "k", } cases := map[string]struct { gvk schema.GroupVersionKind want *Unstructured }{ "New": { gvk: gvk, want: &Unstructured{ Unstructured: unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "g/v1", "kind": "k", }, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := New(WithGroupVersionKind(tc.gvk)) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("New(WithGroupVersionKind(...): -want, +got:\n%s", diff) } }) } } func TestConditions(t *testing.T) { cases := map[string]struct { reason string u *Unstructured set []xpv2.Condition get xpv2.ConditionType want xpv2.Condition wantAll []xpv2.Condition }{ "NewCondition": { reason: "It should be possible to set a condition of an empty Unstructured.", u: New(), set: []xpv2.Condition{xpv2.Available(), xpv2.ReconcileSuccess()}, get: xpv2.TypeReady, want: xpv2.Available(), wantAll: []xpv2.Condition{xpv2.Available(), xpv2.ReconcileSuccess()}, }, "ExistingCondition": { reason: "It should be possible to overwrite a condition that is already set.", u: New(WithConditions(xpv2.Creating())), set: []xpv2.Condition{xpv2.Available()}, get: xpv2.TypeReady, want: xpv2.Available(), wantAll: []xpv2.Condition{xpv2.Available()}, }, "WeirdStatus": { reason: "It should not be possible to set a condition when status is not an object.", u: &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{ "status": "wat", }}}, set: []xpv2.Condition{xpv2.Available()}, get: xpv2.TypeReady, want: xpv2.Condition{}, wantAll: nil, }, "WeirdStatusConditions": { reason: "Conditions should be overwritten if they are not an object.", u: &Unstructured{Unstructured: unstructured.Unstructured{Object: map[string]any{ "status": map[string]any{ "conditions": "wat", }, }}}, set: []xpv2.Condition{xpv2.Available()}, get: xpv2.TypeReady, want: xpv2.Available(), wantAll: []xpv2.Condition{xpv2.Available()}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetConditions(tc.set...) got := tc.u.GetCondition(tc.get) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\n%s\nu.GetCondition(%s): -want, +got:\n%s", tc.reason, tc.get, diff) } gotAll := tc.u.GetConditions() if diff := cmp.Diff(tc.wantAll, gotAll); diff != "" { t.Errorf("\n%s\nu.GetConditions(): -want, +got:\n%s", tc.reason, diff) } }) } } func TestClaimConditionTypes(t *testing.T) { cases := map[string]struct { reason string u *Unstructured set []xpv2.ConditionType want []xpv2.ConditionType wantErr error }{ "CannotSetSystemConditionTypes": { reason: "Claim conditions API should fail to set conditions if a system condition is detected.", u: New(WithSchema(SchemaLegacy)), set: []xpv2.ConditionType{ xpv2.ConditionType("DatabaseReady"), xpv2.ConditionType("NetworkReady"), // system condition xpv2.ConditionType("Ready"), }, want: []xpv2.ConditionType{}, wantErr: errors.New("cannot set system condition Ready as a claim condition"), }, "SetSingleCustomConditionType": { reason: "Claim condition API should work with a single custom condition type.", u: New(WithSchema(SchemaLegacy)), set: []xpv2.ConditionType{xpv2.ConditionType("DatabaseReady")}, want: []xpv2.ConditionType{xpv2.ConditionType("DatabaseReady")}, }, "SetMultipleCustomConditionTypes": { reason: "Claim condition API should work with multiple custom condition types.", u: New(WithSchema(SchemaLegacy)), set: []xpv2.ConditionType{xpv2.ConditionType("DatabaseReady"), xpv2.ConditionType("NetworkReady")}, want: []xpv2.ConditionType{xpv2.ConditionType("DatabaseReady"), xpv2.ConditionType("NetworkReady")}, }, "SetMultipleOfTheSameCustomConditionTypes": { reason: "Claim condition API not add more than one of the same condition.", u: New(WithSchema(SchemaLegacy)), set: []xpv2.ConditionType{xpv2.ConditionType("DatabaseReady"), xpv2.ConditionType("DatabaseReady")}, want: []xpv2.ConditionType{xpv2.ConditionType("DatabaseReady")}, }, "WeirdStatus": { reason: "It should not be possible to set a condition when status is not an object.", u: &Unstructured{ Unstructured: unstructured.Unstructured{Object: map[string]any{ "status": "wat", }}, Schema: SchemaLegacy, }, set: []xpv2.ConditionType{xpv2.ConditionType("DatabaseReady")}, want: []xpv2.ConditionType{}, }, "WeirdStatusClaimConditionTypes": { reason: "Claim conditions should be overwritten if they are not an object.", u: &Unstructured{ Unstructured: unstructured.Unstructured{Object: map[string]any{ "status": map[string]any{ "claimConditionTypes": "wat", }, }}, Schema: SchemaLegacy, }, set: []xpv2.ConditionType{xpv2.ConditionType("DatabaseReady")}, want: []xpv2.ConditionType{xpv2.ConditionType("DatabaseReady")}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { gotErr := tc.u.SetClaimConditionTypes(tc.set...) if diff := cmp.Diff(tc.wantErr, gotErr, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nu.SetClaimConditionTypes(): -want, +got:\n%s", tc.reason, diff) } got := tc.u.GetClaimConditionTypes() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\n%s\nu.GetClaimConditionTypes(): -want, +got:\n%s", tc.reason, diff) } }) } } func TestCompositionSelector(t *testing.T) { sel := &metav1.LabelSelector{MatchLabels: map[string]string{"cool": "very"}} cases := map[string]struct { u *Unstructured set *metav1.LabelSelector want *metav1.LabelSelector }{ "NewSel": { u: New(), set: sel, want: sel, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionSelector(tc.set) got := tc.u.GetCompositionSelector() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionSelector(): -want, +got:\n%s", diff) } }) } } func TestCompositionReference(t *testing.T) { ref := &corev1.ObjectReference{Namespace: "ns", Name: "cool"} cases := map[string]struct { u *Unstructured set *corev1.ObjectReference want *corev1.ObjectReference }{ "NewRef": { u: New(), set: ref, want: ref, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionReference(tc.set) got := tc.u.GetCompositionReference() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionReference(): -want, +got:\n%s", diff) } }) } } func TestCompositionRevisionReference(t *testing.T) { ref := &corev1.LocalObjectReference{Name: "cool"} cases := map[string]struct { u *Unstructured set *corev1.LocalObjectReference want *corev1.LocalObjectReference }{ "NewRef": { u: New(), set: ref, want: ref, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionRevisionReference(tc.set) got := tc.u.GetCompositionRevisionReference() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionRevisionReference(): -want, +got:\n%s", diff) } }) } } func TestCompositionRevisionSelector(t *testing.T) { sel := &metav1.LabelSelector{MatchLabels: map[string]string{"cool": "very"}} cases := map[string]struct { u *Unstructured set *metav1.LabelSelector want *metav1.LabelSelector }{ "NewRef": { u: New(), set: sel, want: sel, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionRevisionSelector(tc.set) got := tc.u.GetCompositionRevisionSelector() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionRevisionSelector(): -want, +got:\n%s", diff) } }) } } func TestCompositionUpdatePolicy(t *testing.T) { p := xpv2.UpdateManual cases := map[string]struct { u *Unstructured set *xpv2.UpdatePolicy want *xpv2.UpdatePolicy }{ "NewRef": { u: New(), set: &p, want: &p, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetCompositionUpdatePolicy(tc.set) got := tc.u.GetCompositionUpdatePolicy() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetCompositionUpdatePolicy(): -want, +got:\n%s", diff) } }) } } func TestClaimReference(t *testing.T) { ref := &reference.Claim{Namespace: "ns", Name: "cool", APIVersion: "acme.com/v1", Kind: "Foo"} cases := map[string]struct { u *Unstructured set *reference.Claim want *reference.Claim }{ "NewRef": { u: New(WithSchema(SchemaLegacy)), set: ref, want: ref, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetClaimReference(tc.set) got := tc.u.GetClaimReference() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetClaimReference(): -want, +got:\n%s", diff) } }) } } func TestResourceReferences(t *testing.T) { ref := corev1.ObjectReference{Namespace: "ns", Name: "cool"} cases := map[string]struct { u *Unstructured set []corev1.ObjectReference want []corev1.ObjectReference }{ "NewRef": { u: New(), set: []corev1.ObjectReference{ref}, want: []corev1.ObjectReference{ref}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetResourceReferences(tc.set) got := tc.u.GetResourceReferences() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetResourceReferences(): -want, +got:\n%s", diff) } }) } } func TestWriteConnectionSecretToReference(t *testing.T) { ref := &xpv2.SecretReference{Namespace: "ns", Name: "cool"} cases := map[string]struct { u *Unstructured set *xpv2.SecretReference want *xpv2.SecretReference }{ "NewRef": { u: New(WithSchema(SchemaLegacy)), set: ref, want: ref, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetWriteConnectionSecretToReference(tc.set) got := tc.u.GetWriteConnectionSecretToReference() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetWriteConnectionSecretToReference(): -want, +got:\n%s", diff) } }) } } func TestConnectionDetailsLastPublishedTime(t *testing.T) { now := &metav1.Time{Time: time.Now()} // The timestamp loses a little resolution when round-tripped through JSON // encoding. lores := func(t *metav1.Time) *metav1.Time { out := &metav1.Time{} j, _ := json.Marshal(t) //nolint:errchkjson // No encoding error in practice. _ = json.Unmarshal(j, out) return out } cases := map[string]struct { u *Unstructured set *metav1.Time want *metav1.Time }{ "NewTimeLegacy": { u: New(WithSchema(SchemaLegacy)), set: now, want: lores(now), }, "NewTimeModern": { u: New(WithSchema(SchemaModern)), set: now, want: nil, // modern schema doesn't support connection details }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { tc.u.SetConnectionDetailsLastPublishedTime(tc.set) got := tc.u.GetConnectionDetailsLastPublishedTime() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetConnectionDetailsLastPublishedTime(): -want, +got:\n%s", diff) } }) } } func TestObservedGeneration(t *testing.T) { cases := map[string]struct { u *Unstructured want int64 }{ "Set": { u: New(func(u *Unstructured) { u.SetObservedGeneration(123) }), want: 123, }, "NotFound": { u: New(), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := tc.u.GetObservedGeneration() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetObservedGeneration(): -want, +got:\n%s", diff) } }) } } func TestLastHandledReconcileAt(t *testing.T) { cases := map[string]struct { u *Unstructured want string }{ "Set": { u: New(func(u *Unstructured) { u.SetLastHandledReconcileAt("2024-01-15T10:30:00Z") }), want: "2024-01-15T10:30:00Z", }, "NotFound": { u: New(), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := tc.u.GetLastHandledReconcileAt() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\nu.GetLastHandledReconcileAt(): -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/resource/unstructured/composite/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by controller-gen. DO NOT EDIT. package composite import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Unstructured) DeepCopyInto(out *Unstructured) { *out = *in in.Unstructured.DeepCopyInto(&out.Unstructured) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Unstructured. func (in *Unstructured) DeepCopy() *Unstructured { if in == nil { return nil } out := new(Unstructured) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Unstructured) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } ================================================ FILE: pkg/resource/unstructured/generate.go ================================================ //go:build generate /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // NOTE(negz): See the below link for details on what is happening here. // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module // Generate deepcopy methodsets //go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../../../hack/boilerplate.go.txt paths=./... package unstructured import ( _ "sigs.k8s.io/controller-tools/cmd/controller-gen" //nolint:typecheck ) ================================================ FILE: pkg/resource/unstructured/reference/reference.go ================================================ /* Copyright 2024 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package reference contains references to resources. package reference import ( "k8s.io/apimachinery/pkg/runtime/schema" ) // A Claim is a reference to a claim. type Claim struct { // APIVersion of the referenced claim. APIVersion string `json:"apiVersion"` // Kind of the referenced claim. Kind string `json:"kind"` // Name of the referenced claim. Name string `json:"name"` // Namespace of the referenced claim. Namespace string `json:"namespace"` } // A Composite is a reference to a composite. type Composite struct { // APIVersion of the referenced composite. APIVersion string `json:"apiVersion"` // Kind of the referenced composite. Kind string `json:"kind"` // Name of the referenced composite. Name string `json:"name"` // Namespace of the referenced composite. Namespace *string `json:"namespace,omitempty"` } // GroupVersionKind returns the GroupVersionKind of the claim reference. func (c *Claim) GroupVersionKind() schema.GroupVersionKind { return schema.FromAPIVersionAndKind(c.APIVersion, c.Kind) } // GroupVersionKind returns the GroupVersionKind of the composite reference. func (c *Composite) GroupVersionKind() schema.GroupVersionKind { return schema.FromAPIVersionAndKind(c.APIVersion, c.Kind) } ================================================ FILE: pkg/statemetrics/mr_state_metrics.go ================================================ /* Copyright 2024 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package statemetrics import ( "context" "strings" "time" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/prometheus/client_golang/prometheus" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/crossplane-runtime/v2/pkg/resource" ) // MRStateMetrics holds Prometheus metrics for managed resources. type MRStateMetrics struct { Exists *prometheus.GaugeVec Ready *prometheus.GaugeVec Synced *prometheus.GaugeVec } // NewMRStateMetrics returns a new MRStateMetrics. func NewMRStateMetrics() *MRStateMetrics { return &MRStateMetrics{ Exists: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Subsystem: subSystem, Name: "managed_resource_exists", Help: "The number of managed resources that exist", }, []string{"gvk"}), Ready: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Subsystem: subSystem, Name: "managed_resource_ready", Help: "The number of managed resources in Ready=True state", }, []string{"gvk"}), Synced: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Subsystem: subSystem, Name: "managed_resource_synced", Help: "The number of managed resources in Synced=True state", }, []string{"gvk"}), } } // Describe sends the super-set of all possible descriptors of metrics // collected by this Collector to the provided channel and returns once // the last descriptor has been sent. func (r *MRStateMetrics) Describe(ch chan<- *prometheus.Desc) { r.Exists.Describe(ch) r.Ready.Describe(ch) r.Synced.Describe(ch) } // Collect is called by the Prometheus registry when collecting // metrics. The implementation sends each collected metric via the // provided channel and returns once the last metric has been sent. func (r *MRStateMetrics) Collect(ch chan<- prometheus.Metric) { r.Exists.Collect(ch) r.Ready.Collect(ch) r.Synced.Collect(ch) } // A MRStateRecorder records the state of managed resources. type MRStateRecorder struct { client client.Client log logging.Logger interval time.Duration managedList resource.ManagedList metrics *MRStateMetrics } // NewMRStateRecorder returns a new MRStateRecorder which records the state of managed resources. func NewMRStateRecorder(c client.Client, log logging.Logger, metrics *MRStateMetrics, managedList resource.ManagedList, interval time.Duration) *MRStateRecorder { return &MRStateRecorder{ client: c, log: log, metrics: metrics, managedList: managedList, interval: interval, } } // Record records the state of managed resources. func (r *MRStateRecorder) Record(ctx context.Context) error { if err := r.client.List(ctx, r.managedList); err != nil { return errors.Wrap(err, "failed to list managed resources") } labels, err := r.getLabels() if err != nil { return errors.Wrap(err, "failed to get labels") } mrs := r.managedList.GetItems() r.metrics.Exists.With(labels).Set(float64(len(mrs))) var numReady, numSynced float64 = 0, 0 for _, o := range mrs { if o.GetCondition(xpv2.TypeReady).Status == corev1.ConditionTrue { numReady++ } if o.GetCondition(xpv2.TypeSynced).Status == corev1.ConditionTrue { numSynced++ } } r.metrics.Ready.With(labels).Set(numReady) r.metrics.Synced.With(labels).Set(numSynced) return nil } // Start records state of managed resources with given interval. func (r *MRStateRecorder) Start(ctx context.Context) error { ticker := time.NewTicker(r.interval) for { select { case <-ticker.C: if err := r.Record(ctx); err != nil { return err } case <-ctx.Done(): ticker.Stop() return nil } } } func (r *MRStateRecorder) getLabels() (prometheus.Labels, error) { gvk, err := apiutil.GVKForObject(r.managedList, r.client.Scheme()) if err != nil { return nil, err } // Remove "List" to get object kind. res := strings.Replace(gvk.String(), "List", "", 1) return prometheus.Labels{"gvk": res}, nil } ================================================ FILE: pkg/statemetrics/state_recorder.go ================================================ /* Copyright 2024 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package statemetrics contains utilities for recording Crossplane resource state metrics. package statemetrics import ( "context" "k8s.io/apimachinery/pkg/runtime/schema" ) const subSystem = "crossplane" // A StateRecorder records the state of given GroupVersionKind. type StateRecorder interface { Record(ctx context.Context, gvk schema.GroupVersionKind) Start(ctx context.Context) error } // A NopStateRecorder does nothing. type NopStateRecorder struct{} // NewNopStateRecorder returns a NopStateRecorder that does nothing. func NewNopStateRecorder() *NopStateRecorder { return &NopStateRecorder{} } // Record does nothing. func (r *NopStateRecorder) Record(_ context.Context, _ schema.GroupVersionKind) {} // Start does nothing. func (r *NopStateRecorder) Start(_ context.Context) error { return nil } ================================================ FILE: pkg/test/cmp.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package test import ( "reflect" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) // EquateErrors returns true if the supplied errors are of the same type and // produce identical strings. This mirrors the error comparison behaviour of // https://github.com/go-test/deep, which most Crossplane tests targeted before // we switched to go-cmp. // // This differs from cmpopts.EquateErrors, which does not test for error strings // and instead returns whether one error 'is' (in the errors.Is sense) the // other. func EquateErrors() cmp.Option { return cmp.Comparer(func(a, b error) bool { if a == nil || b == nil { return a == nil && b == nil } av := reflect.ValueOf(a) bv := reflect.ValueOf(b) if av.Type() != bv.Type() { return false } return a.Error() == b.Error() }) } // EquateConditions sorts any slices of Condition before comparing them. func EquateConditions() cmp.Option { return cmpopts.SortSlices(func(i, j xpv2.Condition) bool { return i.Type < j.Type }) } ================================================ FILE: pkg/test/doc.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package test implements utilities that can be used to test Kubernetes // controllers that reconcile Crossplane resources. package test ================================================ FILE: pkg/test/fake.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package test import ( "context" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" ) var _ client.Client = &MockClient{} // A MockGetFn is used to mock client.Client's Get implementation. type MockGetFn func(ctx context.Context, key client.ObjectKey, obj client.Object) error // A MockListFn is used to mock client.Client's List implementation. type MockListFn func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error // A MockCreateFn is used to mock client.Client's Create implementation. type MockCreateFn func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error // A MockDeleteFn is used to mock client.Client's Delete implementation. type MockDeleteFn func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error // A MockDeleteAllOfFn is used to mock client.Client's Delete implementation. type MockDeleteAllOfFn func(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error // A MockUpdateFn is used to mock client.Client's Update implementation. type MockUpdateFn func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error // A MockPatchFn is used to mock client.Client's Patch implementation. type MockPatchFn func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error // A MockApplyFn is used to mock client.Client's Apply implementation. type MockApplyFn func(ctx context.Context, config runtime.ApplyConfiguration, opts ...client.ApplyOption) error // A MockSubResourceGetFn is used to mock client.SubResourceClient's get implementation. type MockSubResourceGetFn func(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceGetOption) error // A MockSubResourceCreateFn is used to mock client.SubResourceClient's create implementation. type MockSubResourceCreateFn func(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceCreateOption) error // A MockSubResourceUpdateFn is used to mock client.SubResourceClient's update implementation. type MockSubResourceUpdateFn func(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error // A MockSubResourcePatchFn is used to mock client.SubResourceClient's patch implementation. type MockSubResourcePatchFn func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error // A MockSubResourceApplyFn is used to mock client.SubResourceClient's apply implementation. type MockSubResourceApplyFn func(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error // A MockSchemeFn is used to mock client.Client's Scheme implementation. type MockSchemeFn func() *runtime.Scheme // A MockGroupVersionKindForFn is used to mock client.Client's GroupVersionKindFor implementation. type MockGroupVersionKindForFn func(runtime.Object) (schema.GroupVersionKind, error) // A MockIsObjectNamespacedFn is used to mock client.Client's IsObjectNamespaced implementation. type MockIsObjectNamespacedFn func(runtime.Object) (bool, error) // An ObjectFn operates on the supplied Object. You might use an ObjectFn to // test or update the contents of an Object. type ObjectFn func(obj client.Object) error // ApplyFn operates on the supplied ApplyConfiguration. You might use an ApplyFn to // test or update the contents of an ApplyConfiguration object. type ApplyFn func(obj runtime.ApplyConfiguration) error // An RuntimeObjectFn operates on the supplied Object. You might use an RuntimeObjectFn to // test or update the contents of an runtime.Object. type RuntimeObjectFn func(obj runtime.Object) error // An ObjectListFn operates on the supplied ObjectList. You might use an // ObjectListFn to test or update the contents of an ObjectList. type ObjectListFn func(obj client.ObjectList) error // NewMockGetFn returns a MockGetFn that returns the supplied error. func NewMockGetFn(err error, ofn ...ObjectFn) MockGetFn { return func(_ context.Context, _ client.ObjectKey, obj client.Object) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockListFn returns a MockListFn that returns the supplied error. func NewMockListFn(err error, ofn ...ObjectListFn) MockListFn { return func(_ context.Context, obj client.ObjectList, _ ...client.ListOption) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockCreateFn returns a MockCreateFn that returns the supplied error. func NewMockCreateFn(err error, ofn ...ObjectFn) MockCreateFn { return func(_ context.Context, obj client.Object, _ ...client.CreateOption) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockDeleteFn returns a MockDeleteFn that returns the supplied error. func NewMockDeleteFn(err error, ofn ...ObjectFn) MockDeleteFn { return func(_ context.Context, obj client.Object, _ ...client.DeleteOption) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockDeleteAllOfFn returns a MockDeleteAllOfFn that returns the supplied error. func NewMockDeleteAllOfFn(err error, ofn ...ObjectFn) MockDeleteAllOfFn { return func(_ context.Context, obj client.Object, _ ...client.DeleteAllOfOption) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockUpdateFn returns a MockUpdateFn that returns the supplied error. func NewMockUpdateFn(err error, ofn ...ObjectFn) MockUpdateFn { return func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockPatchFn returns a MockPatchFn that returns the supplied error. func NewMockPatchFn(err error, ofn ...ObjectFn) MockPatchFn { return func(_ context.Context, obj client.Object, _ client.Patch, _ ...client.PatchOption) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockApplyFn returns a MockApplyFn that returns the supplied error. func NewMockApplyFn(err error, afn ...ApplyFn) MockApplyFn { return func(_ context.Context, obj runtime.ApplyConfiguration, _ ...client.ApplyOption) error { for _, fn := range afn { if fnErr := fn(obj); fnErr != nil { return fnErr } } return err } } // NewMockSubResourceCreateFn returns a MockSubResourceCreateFn that returns the supplied error. func NewMockSubResourceCreateFn(err error, ofn ...ObjectFn) MockSubResourceCreateFn { return func(_ context.Context, obj, _ client.Object, _ ...client.SubResourceCreateOption) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockSubResourceUpdateFn returns a MockSubResourceUpdateFn that returns the supplied error. func NewMockSubResourceUpdateFn(err error, ofn ...ObjectFn) MockSubResourceUpdateFn { return func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockSubResourcePatchFn returns a MockSubResourcePatchFn that returns the supplied error. func NewMockSubResourcePatchFn(err error, ofn ...ObjectFn) MockSubResourcePatchFn { return func(_ context.Context, obj client.Object, _ client.Patch, _ ...client.SubResourcePatchOption) error { for _, fn := range ofn { if err := fn(obj); err != nil { return err } } return err } } // NewMockSchemeFn returns a MockSchemeFn that returns the scheme. func NewMockSchemeFn(scheme *runtime.Scheme) MockSchemeFn { return func() *runtime.Scheme { return scheme } } // NewMockGroupVersionKindForFn returns a MockGroupVersionKindForFn that returns the supplied GVK and error. func NewMockGroupVersionKindForFn(err error, gvk schema.GroupVersionKind, rofn ...RuntimeObjectFn) MockGroupVersionKindForFn { return func(obj runtime.Object) (schema.GroupVersionKind, error) { for _, fn := range rofn { if err := fn(obj); err != nil { return gvk, err } } return gvk, err } } // NewMockIsObjectNamespacedFn returns a MockGroupVersionKindForFn that returns the supplied GVK and error. func NewMockIsObjectNamespacedFn(err error, isNamespaced bool, rofn ...RuntimeObjectFn) MockIsObjectNamespacedFn { return func(obj runtime.Object) (bool, error) { for _, fn := range rofn { if err := fn(obj); err != nil { return isNamespaced, err } } return isNamespaced, err } } // MockClient implements controller-runtime's Client interface, allowing each // method to be overridden for testing. The controller-runtime provides a fake // client, but it is has surprising side effects (e.g. silently calling // os.Exit(1)) and does not allow us control over the errors it returns. type MockClient struct { MockGet MockGetFn MockList MockListFn MockCreate MockCreateFn MockDelete MockDeleteFn MockDeleteAllOf MockDeleteAllOfFn MockUpdate MockUpdateFn MockPatch MockPatchFn MockApply MockApplyFn MockStatusCreate MockSubResourceCreateFn MockStatusUpdate MockSubResourceUpdateFn MockStatusPatch MockSubResourcePatchFn MockSubResourceGet MockSubResourceGetFn MockSubResourceCreate MockSubResourceCreateFn MockSubResourceUpdate MockSubResourceUpdateFn MockSubResourcePatch MockSubResourcePatchFn MockScheme MockSchemeFn MockGroupVersionKindFor MockGroupVersionKindForFn MockIsObjectNamespaced MockIsObjectNamespacedFn } // NewMockClient returns a MockClient that does nothing when its methods are // called. func NewMockClient() *MockClient { return &MockClient{ MockGet: NewMockGetFn(nil), MockList: NewMockListFn(nil), MockCreate: NewMockCreateFn(nil), MockDelete: NewMockDeleteFn(nil), MockDeleteAllOf: NewMockDeleteAllOfFn(nil), MockUpdate: NewMockUpdateFn(nil), MockPatch: NewMockPatchFn(nil), MockApply: NewMockApplyFn(nil), MockStatusUpdate: NewMockSubResourceUpdateFn(nil), MockStatusPatch: NewMockSubResourcePatchFn(nil), MockScheme: NewMockSchemeFn(nil), MockGroupVersionKindFor: NewMockGroupVersionKindForFn(nil, schema.GroupVersionKind{}), MockIsObjectNamespaced: NewMockIsObjectNamespacedFn(nil, false), } } // Get calls MockClient's MockGet function. func (c *MockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { return c.MockGet(ctx, key, obj) } // List calls MockClient's MockList function. func (c *MockClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { return c.MockList(ctx, list, opts...) } // Create calls MockClient's MockCreate function. func (c *MockClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { return c.MockCreate(ctx, obj, opts...) } // Delete calls MockClient's MockDelete function. func (c *MockClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { return c.MockDelete(ctx, obj, opts...) } // DeleteAllOf calls MockClient's DeleteAllOf function. func (c *MockClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { return c.MockDeleteAllOf(ctx, obj, opts...) } // Update calls MockClient's MockUpdate function. func (c *MockClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { return c.MockUpdate(ctx, obj, opts...) } // Patch calls MockClient's MockPatch function. func (c *MockClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { return c.MockPatch(ctx, obj, patch, opts...) } // Apply calls MockClient's MockApply function. func (c *MockClient) Apply(ctx context.Context, config runtime.ApplyConfiguration, opts ...client.ApplyOption) error { return c.MockApply(ctx, config, opts...) } // Status returns status writer for status sub-resource. func (c *MockClient) Status() client.SubResourceWriter { return &MockSubResourceClient{ MockCreate: c.MockStatusCreate, MockUpdate: c.MockStatusUpdate, MockPatch: c.MockStatusPatch, } } // SubResource is unimplemented. It panics if called. func (c *MockClient) SubResource(_ string) client.SubResourceClient { return &MockSubResourceClient{ MockGet: c.MockSubResourceGet, MockCreate: c.MockSubResourceCreate, MockUpdate: c.MockSubResourceUpdate, MockPatch: c.MockSubResourcePatch, } } // RESTMapper returns the REST mapper. func (c *MockClient) RESTMapper() meta.RESTMapper { return nil } // Scheme calls MockClient's MockScheme function. func (c *MockClient) Scheme() *runtime.Scheme { return c.MockScheme() } // GroupVersionKindFor calls MockClient's MockGroupVersionKindFor function. func (c *MockClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { return c.MockGroupVersionKindFor(obj) } // IsObjectNamespaced calls MockClient's MockIsObjectNamespaced function. func (c *MockClient) IsObjectNamespaced(obj runtime.Object) (bool, error) { return c.MockIsObjectNamespaced(obj) } // MockSubResourceClient provides mock functionality for status sub-resource. type MockSubResourceClient struct { MockGet MockSubResourceGetFn MockCreate MockSubResourceCreateFn MockUpdate MockSubResourceUpdateFn MockPatch MockSubResourcePatchFn MockApply MockSubResourceApplyFn } // Get a sub-resource. func (m *MockSubResourceClient) Get(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceGetOption) error { return m.MockGet(ctx, obj, subResource, opts...) } // Create a sub-resource. func (m *MockSubResourceClient) Create(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceCreateOption) error { return m.MockCreate(ctx, obj, subResource, opts...) } // Update a sub-resource. func (m *MockSubResourceClient) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error { return m.MockUpdate(ctx, obj, opts...) } // Patch a sub-resource. func (m *MockSubResourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error { return m.MockPatch(ctx, obj, patch, opts...) } // Apply a sub-resource. func (m *MockSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { return m.MockApply(ctx, obj, opts...) } ================================================ FILE: pkg/test/retry.go ================================================ /* Copyright 2019 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package test import ( "time" "k8s.io/apimachinery/pkg/util/wait" ) // DefaultRetry is the recommended retry parameters for unit testing scenarios // where a condition is being tested multiple times before it is expected to // succeed. // //nolint:gochecknoglobals // We treat this as a constant. var DefaultRetry = wait.Backoff{ Steps: 500, Duration: 10 * time.Millisecond, Factor: 1.0, Jitter: 0.1, } ================================================ FILE: pkg/version/fake/mocks.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package fake contains semantic version mocks. package fake import ( "github.com/Masterminds/semver/v3" "github.com/crossplane/crossplane-runtime/v2/pkg/version" ) var _ version.Operations = &MockVersioner{} // MockVersioner provides mock version operations. type MockVersioner struct { MockGetVersionString func() string MockGetSemVer func() (*semver.Version, error) MockInConstraints func() (bool, error) } // NewMockGetVersionStringFn creates new MockGetVersionString function for MockVersioner. func NewMockGetVersionStringFn(s string) func() string { return func() string { return s } } // NewMockGetSemVerFn creates new MockGetSemver function for MockVersioner. func NewMockGetSemVerFn(s *semver.Version, err error) func() (*semver.Version, error) { return func() (*semver.Version, error) { return s, err } } // NewMockInConstraintsFn creates new MockInConstraintsString function for MockVersioner. func NewMockInConstraintsFn(b bool, err error) func() (bool, error) { return func() (bool, error) { return b, err } } // GetVersionString calls the underlying MockGetVersionString. func (m *MockVersioner) GetVersionString() string { return m.MockGetVersionString() } // GetSemVer calls the underlying MockGetSemVer. func (m *MockVersioner) GetSemVer() (*semver.Version, error) { return m.MockGetSemVer() } // InConstraints calls the underlying MockInConstraints. func (m *MockVersioner) InConstraints(_ string) (bool, error) { return m.MockInConstraints() } ================================================ FILE: pkg/version/version.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package version contains utilities for working with semantic versions. package version import ( "github.com/Masterminds/semver/v3" ) var version string // Operations provides semantic version operations. type Operations interface { GetVersionString() string GetSemVer() (*semver.Version, error) InConstraints(c string) (bool, error) } // Versioner provides semantic version operations. type Versioner struct { version string } // New creates a new versioner. func New() *Versioner { return &Versioner{ version: version, } } // GetVersionString returns the current Crossplane version as string. func (v *Versioner) GetVersionString() string { return v.version } // GetSemVer returns the current Crossplane version as a semantic version. func (v *Versioner) GetSemVer() (*semver.Version, error) { return semver.NewVersion(v.version) } // InConstraints is a helper function that checks if the current Crossplane // version is in the semantic version constraints. func (v *Versioner) InConstraints(c string) (bool, error) { ver, err := v.GetSemVer() if err != nil { return false, err } constraint, err := semver.NewConstraint(c) if err != nil { return false, err } return constraint.Check(ver), nil } ================================================ FILE: pkg/version/version_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package version import ( "errors" "testing" "github.com/google/go-cmp/cmp" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestInRange(t *testing.T) { type args struct { version string r string } type want struct { is bool err error } cases := map[string]struct { reason string args args want want }{ "ValidInRange": { reason: "Should return true when a valid semantic version is in a valid range.", args: args{ version: "v0.13.0", r: ">0.12.0", }, want: want{ is: true, }, }, "ValidNotInRange": { reason: "Should return false when a valid semantic version is not in a valid range.", args: args{ version: "v0.13.0", r: ">0.13.0", }, want: want{ is: false, }, }, "InvalidVersion": { reason: "Should return error when version is invalid.", args: args{ version: "v0a.13.0", }, want: want{ err: errors.New("invalid semantic version"), }, }, "InvalidRange": { reason: "Should return error when range is invalid.", args: args{ version: "v0.13.0", r: ">a2", }, want: want{ err: errors.New("improper constraint: >a2"), }, }, "ValidSpaceSeparatedRange": { reason: "Should return true if version is within a valid space separated ranged constraint", args: args{ version: "v2.13.0", r: ">=v2.0.0 =v2.0.0,=v2.0.0 >v5a.0.0", }, want: want{ err: errors.New("improper constraint: >=v2.0.0 >v5a.0.0"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { version = tc.args.version is, err := New().InConstraints(tc.args.r) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nInRange(...): -want err, +got err:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.is, is, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nInRange(...): -want, +got:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/webhook/mutator.go ================================================ /* Copyright 2022 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package webhook contains utilities for building Kubernetes webhooks. package webhook import ( "context" "k8s.io/apimachinery/pkg/runtime" ) // WithMutationFns allows you to initiate the mutator with given list of mutator // functions. func WithMutationFns(fns ...MutateFn) MutatorOption { return func(m *Mutator) { m.MutationChain = fns } } // MutatorOption configures given Mutator. type MutatorOption func(*Mutator) // MutateFn is a single mutating function that can be used by Mutator. type MutateFn func(ctx context.Context, obj runtime.Object) error // NewMutator returns a new instance of Mutator that can be used as CustomDefaulter. func NewMutator(opts ...MutatorOption) *Mutator { m := &Mutator{ MutationChain: []MutateFn{}, } for _, f := range opts { f(m) } return m } // Mutator satisfies CustomDefaulter interface with an ordered MutateFn list. type Mutator struct { MutationChain []MutateFn } // Default executes the MutatorFns in given order. Its name might sound misleading // since defaulting seems to be the first use case used by controller-runtime // but MutatorFns can make any changes on given resource. func (m *Mutator) Default(ctx context.Context, obj runtime.Object) error { for _, f := range m.MutationChain { if err := f(ctx, obj); err != nil { return err } } return nil } ================================================ FILE: pkg/webhook/mutator_test.go ================================================ /* Copyright 2022 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package webhook import ( "context" "testing" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var ( // Mutator has to satisfy CustomDefaulter interface so that it can be used by // controller-runtime Manager. _ webhook.CustomDefaulter = &Mutator{} //nolint:staticcheck // Testing deprecated interface for backwards compatibility. _ admission.Defaulter[runtime.Object] = &Mutator{} ) func TestDefault(t *testing.T) { type args struct { obj runtime.Object fns []MutateFn } type want struct { err error } cases := map[string]struct { reason string args want }{ "Success": { reason: "Functions without errors should be executed successfully", args: args{ fns: []MutateFn{ func(_ context.Context, _ runtime.Object) error { return nil }, }, }, }, "Failure": { reason: "Functions with errors should return with error", args: args{ fns: []MutateFn{ func(_ context.Context, _ runtime.Object) error { return errBoom }, }, }, want: want{ err: errBoom, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { v := NewMutator(WithMutationFns(tc.fns...)) err := v.Default(context.TODO(), tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nDefault(...): -want, +got\n%s\n", tc.reason, diff) } }) } } ================================================ FILE: pkg/webhook/validator.go ================================================ /* Copyright 2022 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package webhook import ( "context" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) // WithValidateCreationFns initializes the Validator with given set of creation // validation functions. func WithValidateCreationFns(fns ...ValidateCreateFn) ValidatorOption { return func(v *Validator) { v.CreationChain = fns } } // WithValidateUpdateFns initializes the Validator with given set of update // validation functions. func WithValidateUpdateFns(fns ...ValidateUpdateFn) ValidatorOption { return func(v *Validator) { v.UpdateChain = fns } } // WithValidateDeletionFns initializes the Validator with given set of deletion // validation functions. func WithValidateDeletionFns(fns ...ValidateDeleteFn) ValidatorOption { return func(v *Validator) { v.DeletionChain = fns } } // ValidatorOption allows you to configure given Validator. type ValidatorOption func(*Validator) // ValidateCreateFn is function type for creation validation. type ValidateCreateFn func(ctx context.Context, obj runtime.Object) (admission.Warnings, error) // ValidateUpdateFn is function type for update validation. type ValidateUpdateFn func(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) // ValidateDeleteFn is function type for deletion validation. type ValidateDeleteFn func(ctx context.Context, obj runtime.Object) (admission.Warnings, error) // NewValidator returns a new Validator with no-op defaults. func NewValidator(opts ...ValidatorOption) *Validator { vc := &Validator{ CreationChain: []ValidateCreateFn{}, UpdateChain: []ValidateUpdateFn{}, DeletionChain: []ValidateDeleteFn{}, } for _, f := range opts { f(vc) } return vc } // Validator runs the given validation chains in order. type Validator struct { CreationChain []ValidateCreateFn UpdateChain []ValidateUpdateFn DeletionChain []ValidateDeleteFn } // ValidateCreate runs functions in creation chain in order. func (vc *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { warnings := []string{} for _, f := range vc.CreationChain { warns, err := f(ctx, obj) if err != nil { return append(warnings, warns...), err } warnings = append(warnings, warns...) } return warnings, nil } // ValidateUpdate runs functions in update chain in order. func (vc *Validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { warnings := []string{} for _, f := range vc.UpdateChain { warns, err := f(ctx, oldObj, newObj) if err != nil { return append(warnings, warns...), err } warnings = append(warnings, warns...) } return warnings, nil } // ValidateDelete runs functions in deletion chain in order. func (vc *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { warnings := []string{} for _, f := range vc.DeletionChain { warns, err := f(ctx, obj) if err != nil { return append(warnings, warns...), err } warnings = append(warnings, warns...) } return warnings, nil } ================================================ FILE: pkg/webhook/validator_test.go ================================================ /* Copyright 2022 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package webhook import ( "context" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var ( // Validator has to satisfy CustomValidator interface so that it can be used by // controller-runtime Manager. _ webhook.CustomValidator = &Validator{} //nolint:staticcheck // Testing deprecated interface for backwards compatibility. _ admission.Validator[runtime.Object] = &Validator{} ) var ( errBoom = errors.New("boom") warnings = []string{"warning"} ) func TestValidateCreate(t *testing.T) { type args struct { obj runtime.Object fns []ValidateCreateFn } type want struct { err error warnings admission.Warnings } cases := map[string]struct { reason string args want }{ "Success": { reason: "Functions without errors and warnings should be executed successfully", args: args{ fns: []ValidateCreateFn{ func(_ context.Context, _ runtime.Object) (admission.Warnings, error) { return nil, nil }, }, }, }, "SuccessWithWarnings": { reason: "Functions with warnings but without errors should be executed successfully", args: args{ fns: []ValidateCreateFn{ func(_ context.Context, _ runtime.Object) (admission.Warnings, error) { return warnings, nil }, }, }, want: want{ warnings: warnings, }, }, "Failure": { reason: "Functions with errors and without warnings should return with error", args: args{ fns: []ValidateCreateFn{ func(_ context.Context, _ runtime.Object) (admission.Warnings, error) { return nil, errBoom }, }, }, want: want{ err: errBoom, }, }, "FailureWithWarnings": { reason: "Functions with errors and warnings should return with error", args: args{ fns: []ValidateCreateFn{ func(_ context.Context, _ runtime.Object) (admission.Warnings, error) { return warnings, errBoom }, }, }, want: want{ warnings: warnings, err: errBoom, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { v := NewValidator(WithValidateCreationFns(tc.fns...)) warn, err := v.ValidateCreate(context.TODO(), tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nValidateCreate(...): -want error, +got error\n%s\n", tc.reason, diff) } if diff := cmp.Diff(tc.warnings, warn, cmpopts.EquateEmpty()); diff != "" { t.Errorf("\n%s\nValidateCreate(...): -want warnings, +got warnings\n%s\n", tc.reason, diff) } }) } } func TestValidateUpdate(t *testing.T) { type args struct { oldObj runtime.Object newObj runtime.Object fns []ValidateUpdateFn } type want struct { err error warnings admission.Warnings } cases := map[string]struct { reason string args want }{ "Success": { reason: "Functions without errors should be executed successfully", args: args{ fns: []ValidateUpdateFn{ func(_ context.Context, _, _ runtime.Object) (admission.Warnings, error) { return nil, nil }, }, }, }, "SuccessWithWarnings": { reason: "Functions without errors but with warnings should be executed successfully with warnings", args: args{ fns: []ValidateUpdateFn{ func(_ context.Context, _, _ runtime.Object) (admission.Warnings, error) { return warnings, nil }, }, }, want: want{ warnings: warnings, }, }, "Failure": { reason: "Functions with errors should return with error", args: args{ fns: []ValidateUpdateFn{ func(_ context.Context, _, _ runtime.Object) (admission.Warnings, error) { return nil, errBoom }, }, }, want: want{ err: errBoom, }, }, "FailureWithWarnings": { reason: "Functions with errors and warnings should return with error and warning", args: args{ fns: []ValidateUpdateFn{ func(_ context.Context, _, _ runtime.Object) (admission.Warnings, error) { return warnings, errBoom }, }, }, want: want{ err: errBoom, warnings: warnings, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { v := NewValidator(WithValidateUpdateFns(tc.fns...)) warn, err := v.ValidateUpdate(context.TODO(), tc.oldObj, tc.newObj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nValidateUpdate(...): -want error, +got error\n%s\n", tc.reason, diff) } if diff := cmp.Diff(tc.warnings, warn, cmpopts.EquateEmpty()); diff != "" { t.Errorf("\n%s\nValidateUpdate(...): -want warnings, +got warnings\n%s\n", tc.reason, diff) } }) } } func TestValidateDelete(t *testing.T) { type args struct { obj runtime.Object fns []ValidateDeleteFn } type want struct { err error warnings admission.Warnings } cases := map[string]struct { reason string args want }{ "Success": { reason: "Functions without errors should be executed successfully", args: args{ fns: []ValidateDeleteFn{ func(_ context.Context, _ runtime.Object) (admission.Warnings, error) { return nil, nil }, }, }, }, "SuccessWithWarnings": { reason: "Functions without errors but with warnings should be executed successfully", args: args{ fns: []ValidateDeleteFn{ func(_ context.Context, _ runtime.Object) (admission.Warnings, error) { return warnings, nil }, }, }, want: want{ warnings: warnings, }, }, "Failure": { reason: "Functions with errors should return with error", args: args{ fns: []ValidateDeleteFn{ func(_ context.Context, _ runtime.Object) (admission.Warnings, error) { return nil, errBoom }, }, }, want: want{ err: errBoom, }, }, "FailureWithWarnings": { reason: "Functions with errors and warnings should return with error and warnings", args: args{ fns: []ValidateDeleteFn{ func(_ context.Context, _ runtime.Object) (admission.Warnings, error) { return warnings, errBoom }, }, }, want: want{ err: errBoom, warnings: warnings, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { v := NewValidator(WithValidateDeletionFns(tc.fns...)) warn, err := v.ValidateDelete(context.TODO(), tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nValidateDelete(...): -want error, +got error\n%s\n", tc.reason, diff) } if diff := cmp.Diff(tc.warnings, warn, cmpopts.EquateEmpty()); diff != "" { t.Errorf("\n%s\nValidateDelete(...): -want warnings, +got warnings\n%s\n", tc.reason, diff) } }) } } ================================================ FILE: pkg/xcrd/composite.go ================================================ /* Copyright 2021 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xcrd import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" ) // Annotation keys. const ( // AnnotationKeyCompositionResourceName is the name of the composite resource as described from a composition. AnnotationKeyCompositionResourceName = "crossplane.io/composition-resource-name" ) // SetCompositionResourceName sets the name of the composition template used to // reconcile a composed resource as an annotation. func SetCompositionResourceName(o metav1.Object, n string) { meta.AddAnnotations(o, map[string]string{AnnotationKeyCompositionResourceName: n}) } // GetCompositionResourceName gets the name of the composition template used to // reconcile a composed resource from its annotations. func GetCompositionResourceName(o metav1.Object) string { return o.GetAnnotations()[AnnotationKeyCompositionResourceName] } ================================================ FILE: pkg/xcrd/crd.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package xcrd generates CustomResourceDefinitions from Crossplane definitions. // // v1.JSONSchemaProps is incompatible with controller-tools (as of 0.2.4) // because it is missing JSON tags and uses float64, which is a disallowed type. // We thus copy the entire struct as CRDSpecTemplate. See the below issue: // https://github.com/kubernetes-sigs/controller-tools/issues/291 package xcrd import ( "encoding/json" "maps" v1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1" extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" ) // Category names for generated claim and composite CRDs. const ( CategoryClaim = "claim" CategoryComposite = "composite" ) const ( errFmtGenCrd = "cannot generate CRD for %q %q" errParseValidation = "cannot parse validation schema" errInvalidClaimNames = "invalid resource claim names" errMissingClaimNames = "missing names" errFmtConflictingClaimName = "%q conflicts with composite resource name" errCustomResourceValidationNil = "custom resource validation cannot be nil" ) // ForCompositeResource derives the CustomResourceDefinition for a composite // resource from the supplied CompositeResourceDefinition. func ForCompositeResource(xrd *v1.CompositeResourceDefinition) (*extv1.CustomResourceDefinition, error) { crd := &extv1.CustomResourceDefinition{ Spec: extv1.CustomResourceDefinitionSpec{ Group: xrd.Spec.Group, Names: xrd.Spec.Names, Versions: make([]extv1.CustomResourceDefinitionVersion, len(xrd.Spec.Versions)), Conversion: xrd.Spec.Conversion, }, } crd.SetName(xrd.GetName()) setCrdMetadata(crd, xrd) crd.SetOwnerReferences([]metav1.OwnerReference{meta.AsController( meta.TypedReferenceTo(xrd, v1.CompositeResourceDefinitionGroupVersionKind), )}) scope := ptr.Deref(xrd.Spec.Scope, v1.CompositeResourceScopeLegacyCluster) switch scope { case v1.CompositeResourceScopeNamespaced: crd.Spec.Scope = extv1.NamespaceScoped case v1.CompositeResourceScopeCluster: crd.Spec.Scope = extv1.ClusterScoped case v1.CompositeResourceScopeLegacyCluster: crd.Spec.Scope = extv1.ClusterScoped } crd.Spec.Names.Categories = append(crd.Spec.Names.Categories, CategoryComposite) // The composite name is used as a label value, so we must ensure it is not // longer. const maxCompositeNameLength = 63 for i, vr := range xrd.Spec.Versions { crdv, err := genCrdVersion(vr, maxCompositeNameLength) if err != nil { return nil, errors.Wrapf(err, errFmtGenCrd, "Composite Resource", xrd.Name) } crdv.AdditionalPrinterColumns = append(crdv.AdditionalPrinterColumns, CompositeResourcePrinterColumns(scope)...) props := CompositeResourceSpecProps(scope, xrd.Spec.DefaultCompositionUpdatePolicy) maps.Copy(crdv.Schema.OpenAPIV3Schema.Properties["spec"].Properties, props) props = CompositeResourceStatusProps(scope) maps.Copy(crdv.Schema.OpenAPIV3Schema.Properties["status"].Properties, props) crd.Spec.Versions[i] = *crdv } return crd, nil } // ForCompositeResourceClaim derives the CustomResourceDefinition for a // composite resource claim from the supplied CompositeResourceDefinition. func ForCompositeResourceClaim(xrd *v1.CompositeResourceDefinition) (*extv1.CustomResourceDefinition, error) { if err := validateClaimNames(xrd); err != nil { return nil, errors.Wrap(err, errInvalidClaimNames) } crd := &extv1.CustomResourceDefinition{ Spec: extv1.CustomResourceDefinitionSpec{ Scope: extv1.NamespaceScoped, Group: xrd.Spec.Group, Names: *xrd.Spec.ClaimNames, Versions: make([]extv1.CustomResourceDefinitionVersion, len(xrd.Spec.Versions)), Conversion: xrd.Spec.Conversion, }, } crd.SetName(xrd.Spec.ClaimNames.Plural + "." + xrd.Spec.Group) setCrdMetadata(crd, xrd) crd.SetOwnerReferences([]metav1.OwnerReference{meta.AsController( meta.TypedReferenceTo(xrd, v1.CompositeResourceDefinitionGroupVersionKind), )}) crd.Spec.Names.Categories = append(crd.Spec.Names.Categories, CategoryClaim) // 63 because the names are used as label values. We don't put 63-6 // (generateName suffix length) here because the name generator shortens // the base to 57 automatically before appending the suffix. const maxClaimNameLength = 63 for i, vr := range xrd.Spec.Versions { crdv, err := genCrdVersion(vr, maxClaimNameLength) if err != nil { return nil, errors.Wrapf(err, errFmtGenCrd, "Composite Resource Claim", xrd.Name) } crdv.AdditionalPrinterColumns = append(crdv.AdditionalPrinterColumns, CompositeResourceClaimPrinterColumns()...) props := CompositeResourceClaimSpecProps(xrd.Spec.DefaultCompositeDeletePolicy) maps.Copy(crdv.Schema.OpenAPIV3Schema.Properties["spec"].Properties, props) // TODO(negz): This means claims will have status.claimConditionTypes. // I think that's a bug - only XRs should have that field. props = CompositeResourceStatusProps(v1.CompositeResourceScopeLegacyCluster) maps.Copy(crdv.Schema.OpenAPIV3Schema.Properties["status"].Properties, props) crd.Spec.Versions[i] = *crdv } return crd, nil } func genCrdVersion(vr v1.CompositeResourceDefinitionVersion, maxNameLength int64) (*extv1.CustomResourceDefinitionVersion, error) { crdv := extv1.CustomResourceDefinitionVersion{ Name: vr.Name, Served: vr.Served, Storage: vr.Referenceable, Deprecated: ptr.Deref(vr.Deprecated, false), DeprecationWarning: vr.DeprecationWarning, AdditionalPrinterColumns: vr.AdditionalPrinterColumns, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: BaseProps(), }, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, } s, err := parseSchema(vr.Schema) if err != nil { return nil, errors.Wrapf(err, errParseValidation) } if s == nil { return nil, errors.New(errCustomResourceValidationNil) } crdv.Schema.OpenAPIV3Schema.Description = s.Description crdv.Schema.OpenAPIV3Schema.XValidations = s.XValidations maxLength := maxNameLength if old := s.Properties["metadata"].Properties["name"].MaxLength; old != nil && *old < maxLength { maxLength = *old } xName := crdv.Schema.OpenAPIV3Schema.Properties["metadata"].Properties["name"] xName.MaxLength = ptr.To(maxLength) xName.Type = "string" xMetaData := crdv.Schema.OpenAPIV3Schema.Properties["metadata"] xMetaData.Properties = map[string]extv1.JSONSchemaProps{"name": xName} crdv.Schema.OpenAPIV3Schema.Properties["metadata"] = xMetaData xSpec := s.Properties["spec"] cSpec := crdv.Schema.OpenAPIV3Schema.Properties["spec"] cSpec.Required = append(cSpec.Required, xSpec.Required...) cSpec.XPreserveUnknownFields = xSpec.XPreserveUnknownFields cSpec.XValidations = append(cSpec.XValidations, xSpec.XValidations...) cSpec.OneOf = append(cSpec.OneOf, xSpec.OneOf...) cSpec.Description = xSpec.Description maps.Copy(cSpec.Properties, xSpec.Properties) crdv.Schema.OpenAPIV3Schema.Properties["spec"] = cSpec xStatus := s.Properties["status"] cStatus := crdv.Schema.OpenAPIV3Schema.Properties["status"] cStatus.Required = xStatus.Required cStatus.XValidations = xStatus.XValidations cStatus.Description = xStatus.Description cStatus.OneOf = xStatus.OneOf maps.Copy(cStatus.Properties, xStatus.Properties) crdv.Schema.OpenAPIV3Schema.Properties["status"] = cStatus if vr.Subresources != nil && vr.Subresources.Scale != nil { crdv.Subresources.Scale = vr.Subresources.Scale } return &crdv, nil } func validateClaimNames(d *v1.CompositeResourceDefinition) error { if d.Spec.ClaimNames == nil { return errors.New(errMissingClaimNames) } if n := d.Spec.ClaimNames.Kind; n == d.Spec.Names.Kind { return errors.Errorf(errFmtConflictingClaimName, n) } if n := d.Spec.ClaimNames.Plural; n == d.Spec.Names.Plural { return errors.Errorf(errFmtConflictingClaimName, n) } if n := d.Spec.ClaimNames.Singular; n != "" && n == d.Spec.Names.Singular { return errors.Errorf(errFmtConflictingClaimName, n) } if n := d.Spec.ClaimNames.ListKind; n != "" && n == d.Spec.Names.ListKind { return errors.Errorf(errFmtConflictingClaimName, n) } return nil } func parseSchema(v *v1.CompositeResourceValidation) (*extv1.JSONSchemaProps, error) { if v == nil { return nil, nil } s := &extv1.JSONSchemaProps{} if err := json.Unmarshal(v.OpenAPIV3Schema.Raw, s); err != nil { return nil, errors.Wrap(err, errParseValidation) } return s, nil } // setCrdMetadata sets the labels and annotations on the CRD. func setCrdMetadata(crd *extv1.CustomResourceDefinition, xrd *v1.CompositeResourceDefinition) *extv1.CustomResourceDefinition { crd.SetLabels(xrd.GetLabels()) if xrd.Spec.Metadata != nil { if xrd.Spec.Metadata.Labels != nil { inheritedLabels := crd.GetLabels() if inheritedLabels == nil { inheritedLabels = map[string]string{} } maps.Copy(inheritedLabels, xrd.Spec.Metadata.Labels) crd.SetLabels(inheritedLabels) } if xrd.Spec.Metadata.Annotations != nil { crd.SetAnnotations(xrd.Spec.Metadata.Annotations) } } return crd } // IsEstablished is a helper function to check whether api-server is ready // to accept the instances of registered CRD. func IsEstablished(s extv1.CustomResourceDefinitionStatus) bool { for _, c := range s.Conditions { if c.Type == extv1.Established { return c.Status == extv1.ConditionTrue } } return false } ================================================ FILE: pkg/xcrd/crd_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xcrd import ( "fmt" "strings" "testing" v1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" "github.com/google/go-cmp/cmp" extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var ( name = "coolcomposites.example.org" labels = map[string]string{"cool": "very"} annotations = map[string]string{"example.org/cool": "very"} group = "example.org" version = "v1" kind = "CoolComposite" listKind = "CoolCompositeList" singular = "coolcomposite" plural = "coolcomposites" d = &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, Annotations: annotations, UID: types.UID("you-you-eye-dee"), }, Spec: v1.CompositeResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, }, Versions: []v1.CompositeResourceDefinitionVersion{{ Name: version, Referenceable: true, Served: true, }}, }, } schema = ` { "required": [ "spec" ], "properties": { "spec": { "description": "Specification of the resource.", "required": [ "storageGB", "engineVersion" ], "properties": { "engineVersion": { "enum": [ "5.6", "5.7" ], "type": "string" }, "storageGB": { "type": "integer", "description": "Pretend this is useful." }, "someField": { "type": "string", "description": "Pretend this is useful." }, "someOtherField": { "type": "string", "description": "Pretend this is useful." } }, "x-kubernetes-validations": [ { "message": "Cannot change engine version", "rule": "self.engineVersion == oldSelf.engineVersion" } ], "type": "object", "oneOf": [ { "required": ["someField"] }, { "required": ["someOtherField"] } ] }, "status": { "properties": { "phase": { "type": "string" }, "something": { "type": "string" } }, "x-kubernetes-validations": [ { "message": "Phase is required once set", "rule": "!has(oldSelf.phase) || has(self.phase)" } ], "oneOf": [ { "required": ["phase"] }, { "required": ["something"] } ], "type": "object", "description": "Status of the resource." } }, "type": "object", "description": "What the resource is for." }` ) func TestIsEstablished(t *testing.T) { cases := map[string]struct { s extv1.CustomResourceDefinitionStatus want bool }{ "IsEstablished": { s: extv1.CustomResourceDefinitionStatus{ Conditions: []extv1.CustomResourceDefinitionCondition{{ Type: extv1.Established, Status: extv1.ConditionTrue, }}, }, want: true, }, "IsNotEstablished": { s: extv1.CustomResourceDefinitionStatus{}, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := IsEstablished(tc.s) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("IsEstablished(...): -want, +got:\n%s", diff) } }) } } func TestForCompositeResource(t *testing.T) { defaultCompositionUpdatePolicy := xpv2.UpdatePolicy("Automatic") type args struct { xrd *v1.CompositeResourceDefinition v *v1.CompositeResourceValidation } type want struct { c *extv1.CustomResourceDefinition err error } cases := map[string]struct { reason string args args want want }{ "Namespaced": { reason: "A CRD should be generated from a modern CompositeResourceDefinitionVersion of a namespaced XR.", args: args{ xrd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, Annotations: annotations, UID: types.UID("you-you-eye-dee"), }, Spec: v1.CompositeResourceDefinitionSpec{ Scope: ptr.To(v1.CompositeResourceScopeNamespaced), Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, }, Versions: []v1.CompositeResourceDefinitionVersion{{ Name: version, Referenceable: true, Served: true, }}, }, }, v: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(schema)}, }, }, want: want{ c: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, Categories: []string{CategoryComposite}, }, Scope: extv1.NamespaceScoped, Versions: []extv1.CustomResourceDefinitionVersion{{ Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.crossplane.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.crossplane.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Description: "What the resource is for.", Required: []string{"spec"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "someField": {Type: "string", Description: "Pretend this is useful."}, "someOtherField": {Type: "string", Description: "Pretend this is useful."}, "crossplane": { Type: "object", Description: "Configures how Crossplane will reconcile this composite resource", Properties: map[string]extv1.JSONSchemaProps{ "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, XListType: ptr.To("atomic"), }, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"someField"}}, {Required: []string{"someOtherField"}}, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, "something": {Type: "string"}, "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"phase"}}, {Required: []string{"something"}}, }, }, }, }, }, }}, }, }, }, }, "Legacy": { reason: "A CRD should be generated from a legacy CompositeResourceDefinitionVersion.", args: args{ v: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(schema)}, }, }, want: want{ c: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, Categories: []string{CategoryComposite}, }, Scope: extv1.ClusterScoped, Versions: []extv1.CustomResourceDefinitionVersion{{ Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Description: "What the resource is for.", Required: []string{"spec"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", Properties: map[string]extv1.JSONSchemaProps{ // From CRDSpecTemplate.Validation "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "someField": {Type: "string", Description: "Pretend this is useful."}, "someOtherField": {Type: "string", Description: "Pretend this is useful."}, // From CompositeResourceSpecProps() "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "claimRef": { Type: "object", Required: []string{"apiVersion", "kind", "namespace", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "namespace": {Type: "string"}, "name": {Type: "string"}, }, }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "namespace": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, XListType: ptr.To("atomic"), }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name", "namespace"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, "namespace": {Type: "string"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"someField"}}, {Required: []string{"someOtherField"}}, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, "something": {Type: "string"}, // From CompositeResourceStatusProps() "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, "claimConditionTypes": { Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, }, "connectionDetails": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"phase"}}, {Required: []string{"something"}}, }, }, }, }, }, }}, }, }, }, }, "DefaultCompositionUpdatePolicyIsSet": { reason: "A CRD should be generated from a CompositeResourceDefinitionVersion.", args: args{ xrd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, Annotations: annotations, UID: types.UID("you-you-eye-dee"), }, Spec: v1.CompositeResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, }, Versions: []v1.CompositeResourceDefinitionVersion{{ Name: version, Referenceable: true, Served: true, }}, DefaultCompositionUpdatePolicy: &defaultCompositionUpdatePolicy, }, }, v: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(schema)}, }, }, want: want{ c: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, Categories: []string{CategoryComposite}, }, Scope: extv1.ClusterScoped, Versions: []extv1.CustomResourceDefinitionVersion{{ Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Description: "What the resource is for.", Required: []string{"spec"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", Properties: map[string]extv1.JSONSchemaProps{ // From CRDSpecTemplate.Validation "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "someField": {Type: "string", Description: "Pretend this is useful."}, "someOtherField": {Type: "string", Description: "Pretend this is useful."}, // From CompositeResourceSpecProps() "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Default: &extv1.JSON{Raw: fmt.Appendf(nil, "\"%s\"", defaultCompositionUpdatePolicy)}, Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "claimRef": { Type: "object", Required: []string{"apiVersion", "kind", "namespace", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "namespace": {Type: "string"}, "name": {Type: "string"}, }, }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "namespace": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, XListType: ptr.To("atomic"), }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name", "namespace"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, "namespace": {Type: "string"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"someField"}}, {Required: []string{"someOtherField"}}, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, "something": {Type: "string"}, // From CompositeResourceStatusProps() "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, "claimConditionTypes": { Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, }, "connectionDetails": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"phase"}}, {Required: []string{"something"}}, }, }, }, }, }, }}, }, }, }, }, "EmptyOpenAPIV3Schema": { reason: "A CRD should be generated from a CompositeResourceDefinitionVersion when schema is empty.", args: args{ v: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(`{}`)}, }, }, want: want{ c: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, Categories: []string{CategoryComposite}, }, Scope: extv1.ClusterScoped, Versions: []extv1.CustomResourceDefinitionVersion{{ Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Description: "", Required: []string{"spec"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Description: "", Properties: map[string]extv1.JSONSchemaProps{ // From CompositeResourceSpecProps() "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "claimRef": { Type: "object", Required: []string{"apiVersion", "kind", "namespace", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "namespace": {Type: "string"}, "name": {Type: "string"}, }, }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "namespace": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, XListType: ptr.To("atomic"), }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name", "namespace"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, "namespace": {Type: "string"}, }, }, }, }, "status": { Type: "object", Description: "", Properties: map[string]extv1.JSONSchemaProps{ // From CompositeResourceStatusProps() "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, "claimConditionTypes": { Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, }, "connectionDetails": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, }, }, }, }, }, }, }}, }, }, }, }, "RestrictingNameLength": { reason: "A CRD should be generated from a CompositeResourceDefinitionVersion.", args: args{ v: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(strings.Replace(schema, `"spec":`, `"metadata":{"type":"object","properties":{"name":{"type":"string","maxLength":10}}},"spec":`, 1))}, }, }, want: want{ c: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, Categories: []string{CategoryComposite}, }, Scope: extv1.ClusterScoped, Versions: []extv1.CustomResourceDefinitionVersion{{ Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Description: "What the resource is for.", Required: []string{"spec"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](10), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", Properties: map[string]extv1.JSONSchemaProps{ // From CRDSpecTemplate.Validation "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "someField": {Type: "string", Description: "Pretend this is useful."}, "someOtherField": {Type: "string", Description: "Pretend this is useful."}, // From CompositeResourceSpecProps() "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "claimRef": { Type: "object", Required: []string{"apiVersion", "kind", "namespace", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "namespace": {Type: "string"}, "name": {Type: "string"}, }, }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "namespace": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, XListType: ptr.To("atomic"), }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name", "namespace"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, "namespace": {Type: "string"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"someField"}}, {Required: []string{"someOtherField"}}, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, "something": {Type: "string"}, // From CompositeResourceStatusProps() "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, "claimConditionTypes": { Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, }, "connectionDetails": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"phase"}}, {Required: []string{"something"}}, }, }, }, }, }, }}, }, }, }, }, "WeaklyRestrictingNameLength": { reason: "A CRD should be generated from a CompositeResourceDefinitionVersion.", args: args{ v: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(strings.Replace(schema, `"spec":`, `"metadata":{"type":"object","properties":{"name":{"type":"string","maxLength":100}}},"spec":`, 1))}, }, }, want: want{ c: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, Categories: []string{CategoryComposite}, }, Scope: extv1.ClusterScoped, Versions: []extv1.CustomResourceDefinitionVersion{{ Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Description: "What the resource is for.", Required: []string{"spec"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", Properties: map[string]extv1.JSONSchemaProps{ // From CRDSpecTemplate.Validation "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "someField": {Type: "string", Description: "Pretend this is useful."}, "someOtherField": {Type: "string", Description: "Pretend this is useful."}, // From CompositeResourceSpecProps() "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "claimRef": { Type: "object", Required: []string{"apiVersion", "kind", "namespace", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "namespace": {Type: "string"}, "name": {Type: "string"}, }, }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "namespace": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, XListType: ptr.To("atomic"), }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name", "namespace"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, "namespace": {Type: "string"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"someField"}}, {Required: []string{"someOtherField"}}, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, "something": {Type: "string"}, // From CompositeResourceStatusProps() "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, "claimConditionTypes": { Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, }, "connectionDetails": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"phase"}}, {Required: []string{"something"}}, }, }, }, }, }, }}, }, }, }, }, "NilCompositeResourceValidation": { reason: "Error should be returned if composite resource validation is nil.", args: args{ v: nil, }, want: want{ err: errors.Wrap(errors.New(errCustomResourceValidationNil), fmt.Sprintf(errFmtGenCrd, "Composite Resource", name)), c: nil, }, }, "PreserveUnknownFieldsInSpec": { reason: "A CRD should set PreserveUnknownFields based on the XRD PreserveUnknownFields.", args: args{ v: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(strings.Replace(schema, `"spec": {`, `"spec": { "x-kubernetes-preserve-unknown-fields": true,`, 1))}, }, }, want: want{ c: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, Categories: []string{CategoryComposite}, }, Scope: extv1.ClusterScoped, Versions: []extv1.CustomResourceDefinitionVersion{{ Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Description: "What the resource is for.", Required: []string{"spec"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", XPreserveUnknownFields: ptr.To(true), Properties: map[string]extv1.JSONSchemaProps{ // From CRDSpecTemplate.Validation "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "someField": {Type: "string", Description: "Pretend this is useful."}, "someOtherField": {Type: "string", Description: "Pretend this is useful."}, // From CompositeResourceSpecProps() "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "claimRef": { Type: "object", Required: []string{"apiVersion", "kind", "namespace", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "namespace": {Type: "string"}, "name": {Type: "string"}, }, }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "namespace": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, XListType: ptr.To("atomic"), }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name", "namespace"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, "namespace": {Type: "string"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"someField"}}, {Required: []string{"someOtherField"}}, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, "something": {Type: "string"}, // From CompositeResourceStatusProps() "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, "claimConditionTypes": { Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, }, "connectionDetails": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"phase"}}, {Required: []string{"something"}}, }, }, }, }, }, }}, }, }, }, }, "PreserveTopLevelXValidations": { reason: "A CRD should be generated with top-level x-kubernetes-validations outside of spec.", args: args{ xrd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, Annotations: annotations, UID: types.UID("you-you-eye-dee"), }, Spec: v1.CompositeResourceDefinitionSpec{ Scope: ptr.To(v1.CompositeResourceScopeNamespaced), Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, }, Versions: []v1.CompositeResourceDefinitionVersion{{ Name: version, Referenceable: true, Served: true, }}, }, }, v: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(strings.Replace(schema, `"properties":`, `"x-kubernetes-validations":[{"rule":"self.metadata.name == ('database-' + self.spec.engineVersion)","message":"metadata.name must be database-spec.engineVersion"}],"properties":`, 1))}, }, }, want: want{ c: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, Categories: []string{CategoryComposite}, }, Scope: extv1.NamespaceScoped, Versions: []extv1.CustomResourceDefinitionVersion{{ Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.crossplane.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.crossplane.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Description: "What the resource is for.", Required: []string{"spec"}, XValidations: extv1.ValidationRules{ { Rule: "self.metadata.name == ('database-' + self.spec.engineVersion)", Message: "metadata.name must be database-spec.engineVersion", }, }, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "someField": {Type: "string", Description: "Pretend this is useful."}, "someOtherField": {Type: "string", Description: "Pretend this is useful."}, "crossplane": { Type: "object", Description: "Configures how Crossplane will reconcile this composite resource", Properties: map[string]extv1.JSONSchemaProps{ "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, XListType: ptr.To("atomic"), }, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"someField"}}, {Required: []string{"someOtherField"}}, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, "something": {Type: "string"}, "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"phase"}}, {Required: []string{"something"}}, }, }, }, }, }, }}, }, }, }, }, "SubresourcesScaleIsSet": { reason: "A CRD should set Scale subresource based on the XRD subresources.", args: args{ xrd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, Annotations: annotations, UID: types.UID("you-you-eye-dee"), }, Spec: v1.CompositeResourceDefinitionSpec{ Scope: ptr.To(v1.CompositeResourceScopeNamespaced), Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, }, Versions: []v1.CompositeResourceDefinitionVersion{{ Name: version, Referenceable: true, Served: true, Subresources: &v1.CompositeResourceDefinitionVersionSubresources{ Scale: &extv1.CustomResourceSubresourceScale{ SpecReplicasPath: "spec.replicas", StatusReplicasPath: "status.replicas", LabelSelectorPath: ptr.To("status.labelSelector"), }, }, }}, }, }, v: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(schema)}, }, }, want: want{ c: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, Categories: []string{CategoryComposite}, }, Scope: extv1.NamespaceScoped, Versions: []extv1.CustomResourceDefinitionVersion{{ Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, Scale: &extv1.CustomResourceSubresourceScale{ SpecReplicasPath: "spec.replicas", StatusReplicasPath: "status.replicas", LabelSelectorPath: ptr.To("status.labelSelector"), }, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.crossplane.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.crossplane.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Description: "What the resource is for.", Required: []string{"spec"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "someField": {Type: "string", Description: "Pretend this is useful."}, "someOtherField": {Type: "string", Description: "Pretend this is useful."}, "crossplane": { Type: "object", Description: "Configures how Crossplane will reconcile this composite resource", Properties: map[string]extv1.JSONSchemaProps{ "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, XListType: ptr.To("atomic"), }, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"someField"}}, {Required: []string{"someOtherField"}}, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, "something": {Type: "string"}, "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, OneOf: []extv1.JSONSchemaProps{ {Required: []string{"phase"}}, {Required: []string{"something"}}, }, }, }, }, }, }}, }, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { // TODO(negz): This is surprising - refactor it. We should always // pass the xrd as an argument, not default it here. Same with the // version. var xrd *v1.CompositeResourceDefinition if tc.args.xrd != nil { xrd = tc.args.xrd } else { xrd = d } xrd.Spec.Versions[0].Schema = tc.args.v got, err := ForCompositeResource(xrd) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nForCompositeResource(...): -want err, +got err:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.c, got, test.EquateErrors()); diff != "" { t.Errorf("ForCompositeResource(...): -want, +got:\n%s", diff) } }) } } func TestValidateClaimNames(t *testing.T) { cases := map[string]struct { d *v1.CompositeResourceDefinition want error }{ "MissingClaimNames": { d: &v1.CompositeResourceDefinition{}, want: errors.New(errMissingClaimNames), }, "KindConflict": { d: &v1.CompositeResourceDefinition{ Spec: v1.CompositeResourceDefinitionSpec{ ClaimNames: &extv1.CustomResourceDefinitionNames{ Kind: "a", ListKind: "a", Singular: "a", Plural: "a", }, Names: extv1.CustomResourceDefinitionNames{ Kind: "a", ListKind: "b", Singular: "b", Plural: "b", }, }, }, want: errors.Errorf(errFmtConflictingClaimName, "a"), }, "ListKindConflict": { d: &v1.CompositeResourceDefinition{ Spec: v1.CompositeResourceDefinitionSpec{ ClaimNames: &extv1.CustomResourceDefinitionNames{ Kind: "a", ListKind: "a", Singular: "a", Plural: "a", }, Names: extv1.CustomResourceDefinitionNames{ Kind: "b", ListKind: "a", Singular: "b", Plural: "b", }, }, }, want: errors.Errorf(errFmtConflictingClaimName, "a"), }, "SingularConflict": { d: &v1.CompositeResourceDefinition{ Spec: v1.CompositeResourceDefinitionSpec{ ClaimNames: &extv1.CustomResourceDefinitionNames{ Kind: "a", ListKind: "a", Singular: "a", Plural: "a", }, Names: extv1.CustomResourceDefinitionNames{ Kind: "b", ListKind: "b", Singular: "a", Plural: "b", }, }, }, want: errors.Errorf(errFmtConflictingClaimName, "a"), }, "PluralConflict": { d: &v1.CompositeResourceDefinition{ Spec: v1.CompositeResourceDefinitionSpec{ ClaimNames: &extv1.CustomResourceDefinitionNames{ Kind: "a", ListKind: "a", Singular: "a", Plural: "a", }, Names: extv1.CustomResourceDefinitionNames{ Kind: "b", ListKind: "b", Singular: "b", Plural: "a", Categories: []string{CategoryClaim}, }, }, }, want: errors.Errorf(errFmtConflictingClaimName, "a"), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := validateClaimNames(tc.d) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("validateClaimNames(...): -want, +got:\n%s", diff) } }) } } func TestForCompositeResourceClaim(t *testing.T) { name := "coolcomposites.example.org" labels := map[string]string{"cool": "very"} annotations := map[string]string{"example.org/cool": "very"} group := "example.org" version := "v1" kind := "CoolComposite" listKind := "CoolCompositeList" singular := "coolcomposite" plural := "coolcomposites" claimKind := "CoolClaim" claimListKind := "CoolClaimList" claimSingular := "coolclaim" claimPlural := "coolclaims" defaultPolicy := xpv2.CompositeDeletePolicy("Background") schema := ` { "properties": { "spec": { "description": "Specification of the resource.", "required": [ "storageGB", "engineVersion" ], "properties": { "engineVersion": { "enum": [ "5.6", "5.7" ], "type": "string" }, "storageGB": { "type": "integer", "description": "Pretend this is useful." } }, "x-kubernetes-validations": [ { "message": "Cannot change engine version", "rule": "self.engineVersion == oldSelf.engineVersion" } ], "type": "object" }, "status": { "properties": { "phase": { "type": "string" } }, "x-kubernetes-validations": [ { "message": "Phase is required once set", "rule": "!has(oldSelf.phase) || has(self.phase)" } ], "type": "object", "description": "Status of the resource." } }, "type": "object", "description": "Description of the resource." }` cases := map[string]struct { reason string crd *v1.CompositeResourceDefinition want *extv1.CustomResourceDefinition }{ "CompositeDeletionPolicyUnspecified": { reason: "If default composite deletion unspecified on XRD, set no default value on claim's spec.compositeDeletionPolicy", crd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, Annotations: annotations, UID: types.UID("you-you-eye-dee"), }, Spec: v1.CompositeResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, }, ClaimNames: &extv1.CustomResourceDefinitionNames{ Plural: claimPlural, Singular: claimSingular, Kind: claimKind, ListKind: claimListKind, }, Versions: []v1.CompositeResourceDefinitionVersion{{ Name: version, Referenceable: true, Served: true, Schema: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(schema)}, }, }}, }, }, want: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: claimPlural + "." + group, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: claimPlural, Singular: claimSingular, Kind: claimKind, ListKind: claimListKind, Categories: []string{CategoryClaim}, }, Scope: extv1.NamespaceScoped, Versions: []extv1.CustomResourceDefinitionVersion{ { Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "CONNECTION-SECRET", Type: "string", JSONPath: ".spec.writeConnectionSecretToRef.name", }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"spec"}, Description: "Description of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", Properties: map[string]extv1.JSONSchemaProps{ // From CRDSpecTemplate.Validation "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "compositeDeletePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Background"`)}, {Raw: []byte(`"Foreground"`)}, }, }, // From CompositeResourceClaimSpecProps() "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "resourceRef": { Type: "object", Required: []string{"apiVersion", "kind", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "name": {Type: "string"}, }, }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, // From CompositeResourceStatusProps() "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, "claimConditionTypes": { Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, }, "connectionDetails": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, }, }, }, }, }, }, }, }, }, "CompositeDeletionPolicySetToDefault": { reason: "Propagate default composite deletion set on XRD as the default value on claim's spec.compositeDeletionPolicy", crd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, Annotations: annotations, UID: types.UID("you-you-eye-dee"), }, Spec: v1.CompositeResourceDefinitionSpec{ Group: group, DefaultCompositeDeletePolicy: &defaultPolicy, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, }, ClaimNames: &extv1.CustomResourceDefinitionNames{ Plural: claimPlural, Singular: claimSingular, Kind: claimKind, ListKind: claimListKind, }, Versions: []v1.CompositeResourceDefinitionVersion{{ Name: version, Referenceable: true, Served: true, Schema: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(schema)}, }, }}, }, }, want: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: claimPlural + "." + group, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: claimPlural, Singular: claimSingular, Kind: claimKind, ListKind: claimListKind, Categories: []string{CategoryClaim}, }, Scope: extv1.NamespaceScoped, Versions: []extv1.CustomResourceDefinitionVersion{ { Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "CONNECTION-SECRET", Type: "string", JSONPath: ".spec.writeConnectionSecretToRef.name", }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"spec"}, Description: "Description of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Required: []string{"storageGB", "engineVersion"}, Description: "Specification of the resource.", Properties: map[string]extv1.JSONSchemaProps{ // From CRDSpecTemplate.Validation "storageGB": {Type: "integer", Description: "Pretend this is useful."}, "engineVersion": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"5.6"`)}, {Raw: []byte(`"5.7"`)}, }, }, "compositeDeletePolicy": { Type: "string", Default: &extv1.JSON{Raw: fmt.Appendf(nil, "\"%s\"", defaultPolicy)}, Enum: []extv1.JSON{ {Raw: []byte(`"Background"`)}, {Raw: []byte(`"Foreground"`)}, }, }, // From CompositeResourceClaimSpecProps() "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "resourceRef": { Type: "object", Required: []string{"apiVersion", "kind", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "name": {Type: "string"}, }, }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Cannot change engine version", Rule: "self.engineVersion == oldSelf.engineVersion", }, }, }, "status": { Type: "object", Description: "Status of the resource.", Properties: map[string]extv1.JSONSchemaProps{ "phase": {Type: "string"}, // From CompositeResourceStatusProps() "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, "claimConditionTypes": { Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, }, "connectionDetails": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, }, }, XValidations: extv1.ValidationRules{ { Message: "Phase is required once set", Rule: "!has(oldSelf.phase) || has(self.phase)", }, }, }, }, }, }, }, }, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := ForCompositeResourceClaim(tc.crd) if err != nil { t.Fatalf("ForCompositeResourceClaim(...): %s", err) } if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("ForCompositeResourceClaim(...): -want, +got:\n%s", diff) } }) } } func TestForCompositeResourceClaimEmptyXrd(t *testing.T) { name := "coolcomposites.example.org" labels := map[string]string{"cool": "very"} annotations := map[string]string{"example.org/cool": "very"} group := "example.org" version := "v1" kind := "CoolComposite" listKind := "CoolCompositeList" singular := "coolcomposite" plural := "coolcomposites" claimKind := "CoolClaim" claimListKind := "CoolClaimList" claimSingular := "coolclaim" claimPlural := "coolclaims" schema := "{}" d := &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, Annotations: annotations, UID: types.UID("you-you-eye-dee"), }, Spec: v1.CompositeResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: kind, ListKind: listKind, }, ClaimNames: &extv1.CustomResourceDefinitionNames{ Plural: claimPlural, Singular: claimSingular, Kind: claimKind, ListKind: claimListKind, }, Versions: []v1.CompositeResourceDefinitionVersion{{ Name: version, Referenceable: true, Served: true, Schema: &v1.CompositeResourceValidation{ OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(schema)}, }, }}, }, } want := &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: claimPlural + "." + group, Labels: labels, OwnerReferences: []metav1.OwnerReference{ meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)), }, }, Spec: extv1.CustomResourceDefinitionSpec{ Group: group, Names: extv1.CustomResourceDefinitionNames{ Plural: claimPlural, Singular: claimSingular, Kind: claimKind, ListKind: claimListKind, Categories: []string{CategoryClaim}, }, Scope: extv1.NamespaceScoped, Versions: []extv1.CustomResourceDefinitionVersion{ { Name: version, Served: true, Storage: true, Subresources: &extv1.CustomResourceSubresources{ Status: &extv1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "CONNECTION-SECRET", Type: "string", JSONPath: ".spec.writeConnectionSecretToRef.name", }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, }, Schema: &extv1.CustomResourceValidation{ OpenAPIV3Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"spec"}, Description: "", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "name": { Type: "string", MaxLength: ptr.To[int64](63), }, }, }, "spec": { Type: "object", Description: "", Properties: map[string]extv1.JSONSchemaProps{ "compositeDeletePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Background"`)}, {Raw: []byte(`"Foreground"`)}, }, }, // From CompositeResourceClaimSpecProps() "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "resourceRef": { Type: "object", Required: []string{"apiVersion", "kind", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "name": {Type: "string"}, }, }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, }, }, "status": { Type: "object", Description: "", Properties: map[string]extv1.JSONSchemaProps{ // From CompositeResourceStatusProps() "conditions": { Description: "Conditions of the resource.", Type: "array", XListType: ptr.To("map"), XListMapKeys: []string{"type"}, Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, "claimConditionTypes": { Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, }, "connectionDetails": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, }, }, }, }, }, }, }, }, }, } got, err := ForCompositeResourceClaim(d) if err != nil { t.Fatalf("ForCompositeResourceClaim(...): %s", err) } if diff := cmp.Diff(want, got); diff != "" { t.Errorf("ForCompositeResourceClaim(...): -want, +got:\n%s", diff) } } func TestSetCrdMetadata(t *testing.T) { type args struct { crd *extv1.CustomResourceDefinition xrd *v1.CompositeResourceDefinition } tests := map[string]struct { reason string args args want *extv1.CustomResourceDefinition }{ "SetAnnotations": { reason: "Should set CRD annotations only from XRD spec", args: args{ crd: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, xrd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Annotations: map[string]string{ "example.com/some-xrd-annotation": "not-propagated", }, }, Spec: v1.CompositeResourceDefinitionSpec{Metadata: &v1.CompositeResourceDefinitionSpecMetadata{ Annotations: map[string]string{ "cert-manager.io/inject-ca-from": "example1-ns/webhook1-certificate", }, }}, }, }, want: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Annotations: map[string]string{ "cert-manager.io/inject-ca-from": "example1-ns/webhook1-certificate", }, }, }, }, "SetLabelsFromXRDSpec": { reason: "Should set CRD labels from XRD spec", args: args{ crd: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, xrd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: v1.CompositeResourceDefinitionSpec{Metadata: &v1.CompositeResourceDefinitionSpecMetadata{ Labels: map[string]string{ "example.com/some-crd-label": "value1", "example.com/some-additional-crd-label": "value2", }, }}, }, }, want: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Labels: map[string]string{ "example.com/some-crd-label": "value1", "example.com/some-additional-crd-label": "value2", }, }, }, }, "AppendLabelsFromXRDSpec": { reason: "Should set CRD labels by appending labels from the XRD spec to the ones of the XRD itself", args: args{ crd: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, xrd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Labels: map[string]string{ "example.com/some-xrd-label": "value1", "example.com/some-additional-xrd-label": "value2", }, }, Spec: v1.CompositeResourceDefinitionSpec{Metadata: &v1.CompositeResourceDefinitionSpecMetadata{ Labels: map[string]string{ "example.com/some-crd-label": "value3", "example.com/some-additional-crd-label": "value4", }, }}, }, }, want: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Labels: map[string]string{ "example.com/some-xrd-label": "value1", "example.com/some-additional-xrd-label": "value2", "example.com/some-crd-label": "value3", "example.com/some-additional-crd-label": "value4", }, }, }, }, "SetLabelsAndAnnotations": { reason: "Should set CRD labels and annotations from XRD spec and XRD itself", args: args{ crd: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, xrd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Annotations: map[string]string{ "example.com/some-xrd-annotation": "not-propagated", "example.com/some-additional-xrd-label-annotation": "not-propagated", }, Labels: map[string]string{ "example.com/some-xrd-label": "value1", "example.com/some-additional-xrd-label": "value2", }, }, Spec: v1.CompositeResourceDefinitionSpec{Metadata: &v1.CompositeResourceDefinitionSpecMetadata{ Annotations: map[string]string{ "example.com/some-crd-annotation": "value1", "example.com/some-additional-crd-label-annotation": "value2", }, Labels: map[string]string{ "example.com/some-crd-label": "value3", "example.com/some-additional-crd-label": "value4", }, }}, }, }, want: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Annotations: map[string]string{ "example.com/some-crd-annotation": "value1", "example.com/some-additional-crd-label-annotation": "value2", }, Labels: map[string]string{ "example.com/some-xrd-label": "value1", "example.com/some-additional-xrd-label": "value2", "example.com/some-crd-label": "value3", "example.com/some-additional-crd-label": "value4", }, }, }, }, "NoLabelsAndAnnotations": { reason: "Should do nothing if no annotations or labels are set in XRD spec or XRD itself", args: args{ crd: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, xrd: &v1.CompositeResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, }, want: &extv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { got := setCrdMetadata(tt.args.crd, tt.args.xrd) if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("\n%s\nsetCrdMetadata(...): -want, +got:\n%s", tt.reason, diff) } }) } } ================================================ FILE: pkg/xcrd/fuzz_test.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xcrd import ( "testing" fuzz "github.com/AdaLogics/go-fuzz-headers" v1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1" ) func FuzzForCompositeResourceXcrd(f *testing.F) { f.Fuzz(func(_ *testing.T, data []byte) { ff := fuzz.NewConsumer(data) xrd := &v1.CompositeResourceDefinition{} err := ff.GenerateStruct(xrd) if err != nil { return } _, _ = ForCompositeResource(xrd) }) } func FuzzForCompositeResourceClaim(f *testing.F) { f.Fuzz(func(_ *testing.T, data []byte) { ff := fuzz.NewConsumer(data) xrd := &v1.CompositeResourceDefinition{} err := ff.GenerateStruct(xrd) if err != nil { return } _, _ = ForCompositeResourceClaim(xrd) }) } ================================================ FILE: pkg/xcrd/schemas.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xcrd import ( "fmt" v1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1" xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/utils/ptr" ) // Label keys. const ( LabelKeyNamePrefixForComposed = "crossplane.io/composite" LabelKeyClaimName = "crossplane.io/claim-name" LabelKeyClaimNamespace = "crossplane.io/claim-namespace" ) // CompositionRevisionRef should be propagated dynamically. const CompositionRevisionRef = "compositionRevisionRef" // PropagateSpecProps is the list of XRC spec properties to propagate // when translating an XRC into an XR. var PropagateSpecProps = []string{"compositionRef", "compositionSelector", "compositionUpdatePolicy", "compositionRevisionSelector"} //nolint:gochecknoglobals // We treat this as a constant. // TODO(negz): Add descriptions to schema fields. // BaseProps is a partial OpenAPIV3Schema for the spec fields that Crossplane // expects to be present for all CRDs that it creates. func BaseProps() *extv1.JSONSchemaProps { return &extv1.JSONSchemaProps{ Type: "object", Required: []string{"spec"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": { Type: "string", }, "kind": { Type: "string", }, "metadata": { // NOTE(muvaf): api-server takes care of validating // metadata. Type: "object", }, "spec": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{}, }, "status": { Type: "object", Properties: map[string]extv1.JSONSchemaProps{}, }, }, } } // CompositeResourceSpecProps is a partial OpenAPIV3Schema for the spec fields // that Crossplane expects to be present for all defined composite resources. func CompositeResourceSpecProps(s v1.CompositeResourceScope, defaultPol *xpv2.UpdatePolicy) map[string]extv1.JSONSchemaProps { props := map[string]extv1.JSONSchemaProps{ "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, Default: func() *extv1.JSON { if defaultPol == nil { return nil } return &extv1.JSON{Raw: fmt.Appendf(nil, "\"%s\"", *defaultPol)} }(), }, "resourceRefs": { Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "namespace": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, // Controllers should replace the entire resourceRefs array. XListType: ptr.To("atomic"), }, } // Namespaced XRs don't get to reference composed resources in other // namespaces. if s == v1.CompositeResourceScopeNamespaced { props["resourceRefs"] = extv1.JSONSchemaProps{ Type: "array", Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "name": {Type: "string"}, "kind": {Type: "string"}, }, Required: []string{"apiVersion", "kind"}, }, }, // Controllers should replace the entire resourceRefs array. XListType: ptr.To("atomic"), } } // Legacy XRs have their Crossplane machinery fields directly under spec. // They also support referencing a claim, and writing a secret. if s == v1.CompositeResourceScopeLegacyCluster { props["claimRef"] = extv1.JSONSchemaProps{ Type: "object", Required: []string{"apiVersion", "kind", "namespace", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "namespace": {Type: "string"}, "name": {Type: "string"}, }, } props["writeConnectionSecretToRef"] = extv1.JSONSchemaProps{ Type: "object", Required: []string{"name", "namespace"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, "namespace": {Type: "string"}, }, } return props } // Modern XRs nest their Crossplane machinery fields under spec.crossplane. return map[string]extv1.JSONSchemaProps{ "crossplane": { Type: "object", Description: "Configures how Crossplane will reconcile this composite resource", Properties: props, }, } } // CompositeResourceClaimSpecProps is a partial OpenAPIV3Schema for the spec // fields that Crossplane expects to be present for all published infrastructure // resources. func CompositeResourceClaimSpecProps(defaultPol *xpv2.CompositeDeletePolicy) map[string]extv1.JSONSchemaProps { return map[string]extv1.JSONSchemaProps{ "compositionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionRevisionRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, "compositionRevisionSelector": { Type: "object", Required: []string{"matchLabels"}, Properties: map[string]extv1.JSONSchemaProps{ "matchLabels": { Type: "object", AdditionalProperties: &extv1.JSONSchemaPropsOrBool{ Allows: true, Schema: &extv1.JSONSchemaProps{Type: "string"}, }, }, }, }, "compositionUpdatePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Automatic"`)}, {Raw: []byte(`"Manual"`)}, }, }, "compositeDeletePolicy": { Type: "string", Enum: []extv1.JSON{ {Raw: []byte(`"Background"`)}, {Raw: []byte(`"Foreground"`)}, }, Default: func() *extv1.JSON { if defaultPol == nil { return nil } return &extv1.JSON{Raw: fmt.Appendf(nil, "\"%s\"", *defaultPol)} }(), }, "resourceRef": { Type: "object", Required: []string{"apiVersion", "kind", "name"}, Properties: map[string]extv1.JSONSchemaProps{ "apiVersion": {Type: "string"}, "kind": {Type: "string"}, "name": {Type: "string"}, }, }, "writeConnectionSecretToRef": { Type: "object", Required: []string{"name"}, Properties: map[string]extv1.JSONSchemaProps{ "name": {Type: "string"}, }, }, } } // CompositeResourceStatusProps is a partial OpenAPIV3Schema for the status // fields that Crossplane expects to be present for all composite resources. func CompositeResourceStatusProps(s v1.CompositeResourceScope) map[string]extv1.JSONSchemaProps { props := map[string]extv1.JSONSchemaProps{ "conditions": { Description: "Conditions of the resource.", Type: "array", XListMapKeys: []string{ "type", }, XListType: ptr.To("map"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "object", Required: []string{"lastTransitionTime", "reason", "status", "type"}, Properties: map[string]extv1.JSONSchemaProps{ "lastTransitionTime": {Type: "string", Format: "date-time"}, "message": {Type: "string"}, "reason": {Type: "string"}, "status": {Type: "string"}, "type": {Type: "string"}, "observedGeneration": {Type: "integer", Format: "int64"}, }, }, }, }, } switch s { case v1.CompositeResourceScopeNamespaced, v1.CompositeResourceScopeCluster: // Modern XRs don't have connection details or support claims, so // there's nothing else to put in the status for them case v1.CompositeResourceScopeLegacyCluster: // Legacy XRs don't use status.crossplane, and support claims. props["connectionDetails"] = extv1.JSONSchemaProps{ Type: "object", Properties: map[string]extv1.JSONSchemaProps{ "lastPublishedTime": {Type: "string", Format: "date-time"}, }, } props["claimConditionTypes"] = extv1.JSONSchemaProps{ Type: "array", XListType: ptr.To("set"), Items: &extv1.JSONSchemaPropsOrArray{ Schema: &extv1.JSONSchemaProps{ Type: "string", }, }, } } return props } // CompositeResourcePrinterColumns returns the set of default printer columns // that should exist in all generated composite resource CRDs. func CompositeResourcePrinterColumns(s v1.CompositeResourceScope) []extv1.CustomResourceColumnDefinition { cols := []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "COMPOSITION", Type: "string", JSONPath: ".spec.crossplane.compositionRef.name", }, { Name: "COMPOSITIONREVISION", Type: "string", JSONPath: ".spec.crossplane.compositionRevisionRef.name", Priority: 1, }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, } if s == v1.CompositeResourceScopeLegacyCluster { for i := range cols { if cols[i].Name == "COMPOSITION" { cols[i].JSONPath = ".spec.compositionRef.name" } if cols[i].Name == "COMPOSITIONREVISION" { cols[i].JSONPath = ".spec.compositionRevisionRef.name" } } } return cols } // CompositeResourceClaimPrinterColumns returns the set of default printer // columns that should exist in all generated composite resource claim CRDs. func CompositeResourceClaimPrinterColumns() []extv1.CustomResourceColumnDefinition { return []extv1.CustomResourceColumnDefinition{ { Name: "SYNCED", Type: "string", JSONPath: ".status.conditions[?(@.type=='Synced')].status", }, { Name: "READY", Type: "string", JSONPath: ".status.conditions[?(@.type=='Ready')].status", }, { Name: "CONNECTION-SECRET", Type: "string", JSONPath: ".spec.writeConnectionSecretToRef.name", }, { Name: "AGE", Type: "date", JSONPath: ".metadata.creationTimestamp", }, } } // GetPropFields returns the fields from a map of schema properties. func GetPropFields(props map[string]extv1.JSONSchemaProps) []string { propFields := make([]string, len(props)) i := 0 for k := range props { propFields[i] = k i++ } return propFields } ================================================ FILE: pkg/xpkg/build.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "bytes" "context" "io" "os" "strings" pkgmetav1 "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1" "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1beta1" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser/examples" ) const ( errParserPackage = "failed to parse package" errParserExample = "failed to parse examples" errLintPackage = "failed to lint package" errInitBackend = "failed to initialize package parsing backend" errTarFromStream = "failed to build tarball from stream" errLayerFromTar = "failed to convert tarball to image layer" errDigestInvalid = "failed to get digest from image layer" errBuildImage = "failed to build image from layers" errConfigFile = "failed to get config file from image" errMutateConfig = "failed to mutate config for image" errBuildObjectScheme = "failed to build scheme for package encoder" ) // annotatedTeeReadCloser is a copy of io.TeeReader that implements // parser.AnnotatedReadCloser. It returns a Reader that writes to w what it // reads from r. All reads from r performed through it are matched with // corresponding writes to w. There is no internal buffering - the write must // complete before the read completes. Any error encountered while writing is // reported as a read error. If the underlying reader is a // parser.AnnotatedReadCloser the tee reader will invoke its Annotate function. // Otherwise it will return nil. Closing is always a no-op. func annotatedTeeReadCloser(r io.Reader, w io.Writer) *teeReader { return &teeReader{r, w} } type teeReader struct { r io.Reader w io.Writer } func (t *teeReader) Read(p []byte) (n int, err error) { n, err = t.r.Read(p) if n > 0 { if n, err := t.w.Write(p[:n]); err != nil { return n, err } } return } func (t *teeReader) Close() error { return nil } func (t *teeReader) Annotate() any { anno, ok := t.r.(parser.AnnotatedReadCloser) if !ok { return nil } return anno.Annotate() } // Builder defines an xpkg Builder. type Builder struct { packageSource parser.Backend exampleSource parser.Backend packageParser parser.Parser examplesParser *examples.Parser } // New returns a new Builder. func New(packageSource, exampleSource parser.Backend, packageParser parser.Parser, examplesParser *examples.Parser) *Builder { return &Builder{ packageSource: packageSource, exampleSource: exampleSource, packageParser: packageParser, examplesParser: examplesParser, } } type buildOpts struct { base v1.Image } // A BuildOpt modifies how a package is built. type BuildOpt func(*buildOpts) // WithBase sets the base image of the package. func WithBase(img v1.Image) BuildOpt { return func(o *buildOpts) { o.base = img } } // Build compiles a Crossplane package from an on-disk package. func (b *Builder) Build(ctx context.Context, opts ...BuildOpt) (v1.Image, runtime.Object, error) { bOpts := &buildOpts{ base: empty.Image, } for _, o := range opts { o(bOpts) } // assume examples exist examplesExist := true // Get package YAML stream. pkgReader, err := b.packageSource.Init(ctx) if err != nil { return nil, nil, errors.Wrap(err, errInitBackend) } defer func() { _ = pkgReader.Close() }() // Get examples YAML stream. exReader, err := b.exampleSource.Init(ctx) if err != nil && !os.IsNotExist(err) { return nil, nil, errors.Wrap(err, errInitBackend) } defer func() { _ = exReader.Close() }() // examples/ doesn't exist if os.IsNotExist(err) { examplesExist = false } pkg, err := b.packageParser.Parse(ctx, pkgReader) if err != nil { return nil, nil, errors.Wrap(err, errParserPackage) } metas := pkg.GetMeta() if len(metas) != 1 { return nil, nil, errors.New(errNotExactlyOneMeta) } // TODO(hasheddan): make linter selection logic configurable. meta := metas[0] var linter parser.Linter switch meta.GetObjectKind().GroupVersionKind().Kind { case pkgmetav1.ConfigurationKind: linter = NewConfigurationLinter() case v1beta1.FunctionKind: linter = NewFunctionLinter() case pkgmetav1.ProviderKind: linter = NewProviderLinter() } if err := linter.Lint(pkg); err != nil { return nil, nil, errors.Wrap(err, errLintPackage) } layers := make([]v1.Layer, 0) cfgFile, err := bOpts.base.ConfigFile() if err != nil { return nil, nil, errors.Wrap(err, errConfigFile) } cfg := cfgFile.Config if cfg.Labels == nil { cfg.Labels = make(map[string]string) } pkgBytes, err := encode(pkg) if err != nil { return nil, nil, errors.Wrap(err, errConfigFile) } pkgLayer, err := Layer(pkgBytes, StreamFile, PackageAnnotation, int64(pkgBytes.Len()), StreamFileMode, &cfg) if err != nil { return nil, nil, err } layers = append(layers, pkgLayer) // examples exist, create the layer if examplesExist { exBuf := new(bytes.Buffer) if _, err = b.examplesParser.Parse(ctx, annotatedTeeReadCloser(exReader, exBuf)); err != nil { return nil, nil, errors.Wrap(err, errParserExample) } exLayer, err := Layer(exBuf, XpkgExamplesFile, ExamplesAnnotation, int64(exBuf.Len()), StreamFileMode, &cfg) if err != nil { return nil, nil, err } layers = append(layers, exLayer) } for _, l := range layers { bOpts.base, err = mutate.AppendLayers(bOpts.base, l) if err != nil { return nil, nil, errors.Wrap(err, errBuildImage) } } bOpts.base, err = mutate.Config(bOpts.base, cfg) if err != nil { return nil, nil, errors.Wrap(err, errMutateConfig) } return bOpts.base, meta, nil } // encode encodes a package as a YAML stream. Does not check meta existence // or quantity i.e. it should be linted first to ensure that it is valid. func encode(pkg parser.Lintable) (*bytes.Buffer, error) { pkgBuf := new(bytes.Buffer) objScheme, err := BuildObjectScheme() if err != nil { return nil, errors.New(errBuildObjectScheme) } do := json.NewSerializerWithOptions(json.DefaultMetaFactory, objScheme, objScheme, json.SerializerOptions{Yaml: true}) pkgBuf.WriteString("---\n") if err = do.Encode(pkg.GetMeta()[0], pkgBuf); err != nil { return nil, errors.Wrap(err, errBuildObjectScheme) } pkgBuf.WriteString("---\n") for _, o := range pkg.GetObjects() { if err = do.Encode(o, pkgBuf); err != nil { return nil, errors.Wrap(err, errBuildObjectScheme) } pkgBuf.WriteString("---\n") } return pkgBuf, nil } // SkipContains supplies a FilterFn that skips paths that contain the give pattern. func SkipContains(pattern string) parser.FilterFn { return func(path string, _ os.FileInfo) (bool, error) { return strings.Contains(path, pattern), nil } } ================================================ FILE: pkg/xpkg/build_test.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "archive/tar" "context" "io" "os" "sort" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/spf13/afero" "github.com/spf13/afero/tarfs" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser/examples" ) var ( testCRD []byte testMeta []byte testEx1 []byte testEx2 []byte testEx3 []byte testEx4 []byte _ parser.Backend = &MockBackend{} ) func init() { testCRD, _ = afero.ReadFile(afero.NewOsFs(), "testdata/providerconfigs.helm.crossplane.io.yaml") testMeta, _ = afero.ReadFile(afero.NewOsFs(), "testdata/provider_meta.yaml") testEx1, _ = afero.ReadFile(afero.NewOsFs(), "testdata/examples/ec2/instance.yaml") testEx2, _ = afero.ReadFile(afero.NewOsFs(), "testdata/examples/ec2/internetgateway.yaml") testEx3, _ = afero.ReadFile(afero.NewOsFs(), "testdata/examples/ecr/repository.yaml") testEx4, _ = afero.ReadFile(afero.NewOsFs(), "testdata/examples/provider.yaml") } type MockBackend struct { MockInit func() (io.ReadCloser, error) } func NewMockInitFn(r io.ReadCloser, err error) func() (io.ReadCloser, error) { return func() (io.ReadCloser, error) { return r, err } } func (m *MockBackend) Init(_ context.Context, _ ...parser.BackendOption) (io.ReadCloser, error) { return m.MockInit() } var _ parser.Parser = &MockParser{} type MockParser struct { MockParse func() (*parser.Package, error) } func NewMockParseFn(pkg *parser.Package, err error) func() (*parser.Package, error) { return func() (*parser.Package, error) { return pkg, err } } func (m *MockParser) Parse(context.Context, io.ReadCloser) (*parser.Package, error) { return m.MockParse() } func TestBuild(t *testing.T) { errBoom := errors.New("boom") type args struct { be parser.Backend ex parser.Backend p parser.Parser e *examples.Parser } cases := map[string]struct { reason string args args want error }{ "ErrInitBackend": { reason: "Should return an error if we fail to initialize backend.", args: args{ be: &MockBackend{ MockInit: NewMockInitFn(nil, errBoom), }, }, want: errors.Wrap(errBoom, errInitBackend), }, "ErrParse": { reason: "Should return an error if we fail to parse package.", args: args{ be: parser.NewEchoBackend(""), ex: parser.NewEchoBackend(""), p: &MockParser{ MockParse: NewMockParseFn(nil, errBoom), }, }, want: errors.Wrap(errBoom, errParserPackage), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { builder := New(tc.args.be, tc.args.ex, tc.args.p, tc.args.e) _, _, err := builder.Build(context.TODO()) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nBuild(...): -want err, +got err:\n%s", tc.reason, diff) } }) } } func TestBuildExamples(t *testing.T) { pkgp, _ := yamlParser() defaultFilters := []parser.FilterFn{ parser.SkipDirs(), parser.SkipNotYAML(), parser.SkipEmpty(), } type withFsFn func() afero.Fs type args struct { rootDir string examplesDir string fs withFsFn } type want struct { pkgExists bool exExists bool labels []string err error } cases := map[string]struct { reason string args args want want }{ "SuccessNoExamples": { args: args{ rootDir: "/ws", examplesDir: "/ws/examples", fs: func() afero.Fs { fs := afero.NewMemMapFs() _ = fs.Mkdir("/ws", os.ModePerm) _ = fs.Mkdir("/ws/crds", os.ModePerm) _ = afero.WriteFile(fs, "/ws/crossplane.yaml", testMeta, os.ModePerm) _ = afero.WriteFile(fs, "/ws/crds/crd.yaml", testCRD, os.ModePerm) return fs }, }, want: want{ pkgExists: true, labels: []string{ PackageAnnotation, }, }, }, "SuccessExamplesAtRoot": { args: args{ rootDir: "/ws", examplesDir: "/ws/examples", fs: func() afero.Fs { fs := afero.NewMemMapFs() _ = fs.Mkdir("/ws", os.ModePerm) _ = afero.WriteFile(fs, "/ws/crossplane.yaml", testMeta, os.ModePerm) _ = afero.WriteFile(fs, "/ws/crds/crd.yaml", testCRD, os.ModePerm) _ = afero.WriteFile(fs, "/ws/examples/ec2/instance.yaml", testEx1, os.ModePerm) _ = afero.WriteFile(fs, "/ws/examples/ec2/internetgateway.yaml", testEx2, os.ModePerm) _ = afero.WriteFile(fs, "/ws/examples/ecr/repository.yaml", testEx3, os.ModePerm) _ = afero.WriteFile(fs, "/ws/examples/provider.yaml", testEx4, os.ModePerm) return fs }, }, want: want{ pkgExists: true, exExists: true, labels: []string{ PackageAnnotation, ExamplesAnnotation, }, }, }, "SuccessExamplesAtCustomDir": { args: args{ rootDir: "/ws", examplesDir: "/other_directory/examples", fs: func() afero.Fs { fs := afero.NewMemMapFs() _ = fs.Mkdir("/ws", os.ModePerm) _ = fs.Mkdir("/other_directory", os.ModePerm) _ = afero.WriteFile(fs, "/ws/crossplane.yaml", testMeta, os.ModePerm) _ = afero.WriteFile(fs, "/ws/crds/crd.yaml", testCRD, os.ModePerm) _ = afero.WriteFile(fs, "/other_directory/examples/ec2/instance.yaml", testEx1, os.ModePerm) _ = afero.WriteFile(fs, "/other_directory/examples/ec2/internetgateway.yaml", testEx2, os.ModePerm) _ = afero.WriteFile(fs, "/other_directory/examples/ecr/repository.yaml", testEx3, os.ModePerm) _ = afero.WriteFile(fs, "/other_directory/examples/provider.yaml", testEx4, os.ModePerm) return fs }, }, want: want{ pkgExists: true, exExists: true, labels: []string{ PackageAnnotation, ExamplesAnnotation, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { pkgBe := parser.NewFsBackend( tc.args.fs(), parser.FsDir(tc.args.rootDir), parser.FsFilters([]parser.FilterFn{ parser.SkipDirs(), parser.SkipNotYAML(), parser.SkipEmpty(), SkipContains("examples/"), // don't try to parse the examples in the package }...), ) pkgEx := parser.NewFsBackend( tc.args.fs(), parser.FsDir(tc.args.examplesDir), parser.FsFilters(defaultFilters...), ) builder := New(pkgBe, pkgEx, pkgp, examples.New()) img, _, err := builder.Build(context.TODO()) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nBuildExamples(...): -want err, +got err:\n%s", tc.reason, diff) } // validate the xpkg img has the correct annotations, etc contents, err := readImg(img) // sort the contents slice for test comparison sort.Strings(contents.labels) if diff := cmp.Diff(tc.want.pkgExists, len(contents.pkgBytes) != 0); diff != "" { t.Errorf("\n%s\nBuildExamples(...): -want err, +got err:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.exExists, len(contents.exBytes) != 0); diff != "" { t.Errorf("\n%s\nBuildExamples(...): -want err, +got err:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.labels, contents.labels, cmpopts.SortSlices(func(i, j int) bool { return contents.labels[i] < contents.labels[j] })); diff != "" { t.Errorf("\n%s\nBuildExamples(...): -want err, +got err:\n%s", tc.reason, diff) } if diff := cmp.Diff(nil, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nBuildExamples(...): -want err, +got err:\n%s", tc.reason, diff) } }) } } type xpkgContents struct { labels []string pkgBytes []byte exBytes []byte } func readImg(i v1.Image) (xpkgContents, error) { contents := xpkgContents{ labels: make([]string, 0), } reader := mutate.Extract(i) fs := tarfs.New(tar.NewReader(reader)) pkgYaml, err := fs.Open(StreamFile) if err != nil { return contents, err } pkgBytes, err := io.ReadAll(pkgYaml) if err != nil { return contents, err } contents.pkgBytes = pkgBytes exYaml, err := fs.Open(XpkgExamplesFile) if err != nil && !os.IsNotExist(err) { return contents, err } if exYaml != nil { exBytes, err := io.ReadAll(exYaml) if err != nil { return contents, err } contents.exBytes = exBytes } labels, err := allLabels(i) if err != nil { return contents, err } contents.labels = labels return contents, nil } func allLabels(i partial.WithConfigFile) ([]string, error) { labels := []string{} cfgFile, err := i.ConfigFile() if err != nil { return labels, err } cfg := cfgFile.Config for _, label := range cfg.Labels { labels = append(labels, label) } return labels, nil } // This is equivalent to yaml.New. Duplicated here to avoid an import cycle. func yamlParser() (*parser.PackageParser, error) { metaScheme, err := BuildMetaScheme() if err != nil { panic(err) } objScheme, err := BuildObjectScheme() if err != nil { panic(err) } return parser.New(metaScheme, objScheme), nil } ================================================ FILE: pkg/xpkg/cache.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "compress/gzip" "io" "os" "sync" "github.com/spf13/afero" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) const ( errGetNopCache = "cannot get content from a NopCache" ) const cacheContentExt = ".gz" // A PackageCache caches package content. type PackageCache interface { Has(id string) bool Get(id string) (io.ReadCloser, error) Store(id string, content io.ReadCloser) error Delete(id string) error } // FsPackageCache stores and retrieves package content in a filesystem-backed // cache in a thread-safe manner. type FsPackageCache struct { dir string fs afero.Fs mu sync.RWMutex } // NewFsPackageCache creates a new FsPackageCache. func NewFsPackageCache(dir string, fs afero.Fs) *FsPackageCache { return &FsPackageCache{ dir: dir, fs: fs, } } // Has indicates whether an item with the given id is in the cache. func (c *FsPackageCache) Has(id string) bool { if fi, err := c.fs.Stat(BuildPath(c.dir, id, cacheContentExt)); err == nil && !fi.IsDir() { return true } return false } // Get retrieves package contents from the cache. func (c *FsPackageCache) Get(id string) (io.ReadCloser, error) { c.mu.RLock() defer c.mu.RUnlock() f, err := c.fs.Open(BuildPath(c.dir, id, cacheContentExt)) if err != nil { return nil, err } return GzipReadCloser(f) } // Store saves the package contents to the cache. func (c *FsPackageCache) Store(id string, content io.ReadCloser) error { c.mu.Lock() defer c.mu.Unlock() cf, err := c.fs.Create(BuildPath(c.dir, id, cacheContentExt)) if err != nil { return err } defer cf.Close() //nolint:errcheck // Error is checked in the happy path. w, err := gzip.NewWriterLevel(cf, gzip.BestSpeed) if err != nil { return err } _, err = io.Copy(w, content) if err != nil { return err } // NOTE(hasheddan): gzip writer must be closed to ensure all data is flushed // to file. if err := w.Close(); err != nil { return err } return cf.Close() } // Delete removes package contents from the cache. func (c *FsPackageCache) Delete(id string) error { c.mu.Lock() defer c.mu.Unlock() err := c.fs.Remove(BuildPath(c.dir, id, cacheContentExt)) if os.IsNotExist(err) { return nil } return err } // NopCache is a cache implementation that does not store anything and always // returns an error on get. type NopCache struct{} // NewNopCache creates a new NopCache. func NewNopCache() *NopCache { return &NopCache{} } // Has indicates whether content is in the NopCache. func (c *NopCache) Has(string) bool { return false } // Get retrieves content from the NopCache. func (c *NopCache) Get(string) (io.ReadCloser, error) { return nil, errors.New(errGetNopCache) } // Store saves content to the NopCache. func (c *NopCache) Store(string, io.ReadCloser) error { return nil } // Delete removes content from the NopCache. func (c *NopCache) Delete(string) error { return nil } ================================================ FILE: pkg/xpkg/cache_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "bytes" "compress/gzip" "io" "os" "syscall" "testing" "github.com/google/go-cmp/cmp" "github.com/spf13/afero" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var _ PackageCache = &FsPackageCache{} func TestHas(t *testing.T) { fs := afero.NewMemMapFs() cf, _ := fs.Create("/cache/exists.gz") _ = fs.Mkdir("/cache/some-dir.gz", os.ModeDir) defer cf.Close() type args struct { cache PackageCache id string } cases := map[string]struct { reason string args args want bool }{ "Success": { reason: "Should not return an error if package exists at path.", args: args{ cache: NewFsPackageCache("/cache", fs), id: "exists", }, want: true, }, "ErrNotExist": { reason: "Should return error if package does not exist at path.", args: args{ cache: NewFsPackageCache("/cache", fs), id: "not-exist", }, want: false, }, "ErrIsDir": { reason: "Should return error if path is a directory.", args: args{ cache: NewFsPackageCache("/cache", fs), id: "some-dir.gz", }, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { h := tc.args.cache.Has(tc.args.id) if diff := cmp.Diff(tc.want, h); diff != "" { t.Errorf("\n%s\nHas(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestGet(t *testing.T) { fs := afero.NewMemMapFs() cf, _ := fs.Create("/cache/exists.gz") // NOTE(hasheddan): valid gzip header. cf.Write([]byte{31, 139, 8, 0, 0, 0, 0, 0, 0, 0}) cf, _ = fs.Create("/cache/not-gzip.gz") cf.WriteString("some content") defer cf.Close() type args struct { cache PackageCache id string } cases := map[string]struct { reason string args args want error }{ "Success": { reason: "Should not return an error if package exists at path.", args: args{ cache: NewFsPackageCache("/cache", fs), id: "exists", }, }, "ErrNotGzip": { reason: "Should return error if package does not exist at path.", args: args{ cache: NewFsPackageCache("/cache", fs), id: "not-gzip", }, want: gzip.ErrHeader, }, "ErrNotExist": { reason: "Should return error if package does not exist at path.", args: args{ cache: NewFsPackageCache("/cache", fs), id: "not-exist", }, want: &os.PathError{Op: "open", Path: "/cache/not-exist.gz", Err: afero.ErrFileNotFound}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { _, err := tc.args.cache.Get(tc.args.id) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nGet(...): -want err, +got err:\n%s", tc.reason, diff) } }) } } func TestStore(t *testing.T) { fs := afero.NewMemMapFs() type args struct { cache PackageCache id string } cases := map[string]struct { reason string args args want error }{ "Success": { reason: "Should not return an error if package is created at path.", args: args{ cache: NewFsPackageCache("/cache", fs), id: "exists-1234567", }, }, "ErrFailedCreate": { reason: "Should return an error if file creation fails.", args: args{ cache: NewFsPackageCache("/cache", afero.NewReadOnlyFs(fs)), id: "exists-1234567", }, want: syscall.EPERM, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := tc.args.cache.Store(tc.args.id, io.NopCloser(new(bytes.Buffer))) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nStore(...): -want err, +got err:\n%s", tc.reason, diff) } }) } } func TestDelete(t *testing.T) { fs := afero.NewMemMapFs() _, _ = fs.Create("/cache/exists.xpkg") type args struct { cache PackageCache id string } cases := map[string]struct { reason string args args want error }{ "Success": { reason: "Should not return an error if package is deleted at path.", args: args{ cache: NewFsPackageCache("/cache", fs), id: "exists", }, }, "SuccessNotExist": { reason: "Should not return an error if package does not exist.", args: args{ cache: NewFsPackageCache("/cache", fs), id: "not-exist", }, }, "ErrFailedDelete": { reason: "Should return an error if file deletion fails.", args: args{ cache: NewFsPackageCache("/cache", afero.NewReadOnlyFs(fs)), id: "exists-1234567", }, want: syscall.EPERM, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := tc.args.cache.Delete(tc.args.id) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nStore(...): -want err, +got err:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/xpkg/client.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "archive/tar" "context" "io" "path/filepath" "sort" "strings" "github.com/Masterminds/semver/v3" pkgmetav1 "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1" ociname "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" corev1 "k8s.io/api/core/v1" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/signature" ) // Client is a client for fetching and parsing Crossplane packages. type Client interface { // Get fetches and parses a complete package from the given reference. // The ref parameter is a package reference (e.g., // "registry.io/org/package:v1.0.0" or "registry.io/org/package@sha256:..."). // // Caching and ImageConfig path rewriting are handled transparently. Get(ctx context.Context, ref string, opts ...GetOption) (*Package, error) // ListVersions returns available versions for a package source. // The source parameter is the package path without tag/digest // (e.g., "registry.io/org/package"). // // Honors ImageConfig path rewriting when listing versions. ListVersions(ctx context.Context, source string, opts ...GetOption) ([]string, error) } // ImageConfig represents an ImageConfig that was applied during package fetch. type ImageConfig struct { Name string Reason ImageConfigReason } // ImageConfigReason describes why an ImageConfig was applied. type ImageConfigReason string const ( // ImageConfigReasonRewrite indicates the ImageConfig rewrote the image // path. ImageConfigReasonRewrite ImageConfigReason = "RewriteImage" // ImageConfigReasonSetPullSecret indicates the ImageConfig provided a // pull secret. ImageConfigReasonSetPullSecret ImageConfigReason = "SetImagePullSecret" // ImageConfigReasonVerify indicates the ImageConfig provided signature // verification config. ImageConfigReasonVerify ImageConfigReason = "VerifyImage" ) // SupportedImageConfigs returns the ImageConfigReasons that Client may return. // Callers tracking applied ImageConfigs should clear all of these before // setting the ones returned by Client.Get, to handle the case where a // previously-applied ImageConfig no longer matches. func SupportedImageConfigs() []ImageConfigReason { return []ImageConfigReason{ ImageConfigReasonRewrite, ImageConfigReasonSetPullSecret, ImageConfigReasonVerify, } } // maxPackageSize is the maximum size of a package.yaml file that can be parsed. // This limit prevents denial of service attacks via maliciously large packages. const maxPackageSize = 200 << 20 // 200 MB // Package represents a successfully fetched package with all its content. type Package struct { *parser.Package // Digest is the immutable content identifier (sha256 from OCI image). Digest string // Version is the package version, either a semver tag (v1.0.0) or digest // (sha256:abc123). This is extracted from the original reference used to // fetch the package. Version string // Source is the package source without tag/digest, normalized. // This is the ORIGINAL source before any ImageConfig rewriting. Source string // ResolvedVersion is the package version after ImageConfig rewriting. // May differ from Version if an ImageConfig rewrote the tag/digest. ResolvedVersion string // ResolvedSource is the source after ImageConfig path rewriting. // May be the same as Source if no rewriting occurred. ResolvedSource string // AppliedImageConfigs tracks which ImageConfigs were applied during fetch. AppliedImageConfigs []ImageConfig } // DigestHex returns the hex string of the digest without the algorithm prefix. // Returns empty string if the digest cannot be parsed. func (p *Package) DigestHex() string { hash, err := v1.NewHash(p.Digest) if err != nil { return "" } return hash.Hex } // GetMeta returns the package metadata object. // Returns nil if the package doesn't contain exactly one metadata object. func (p *Package) GetMeta() pkgmetav1.Pkg { meta := p.Package.GetMeta() if len(meta) != 1 { return nil } pkg, _ := TryConvertToPkg(meta[0], &pkgmetav1.Provider{}, &pkgmetav1.Configuration{}, &pkgmetav1.Function{}) return pkg } // GetDependencies returns the package dependencies from metadata. // Returns nil if metadata cannot be extracted. func (p *Package) GetDependencies() []pkgmetav1.Dependency { meta := p.GetMeta() if meta == nil { return nil } return meta.GetDependencies() } // Ref returns the full original package reference (Source + Version). func (p *Package) Ref() string { return BuildPackageRef(p.Source, p.Version) } // ResolvedRef returns the full resolved package reference after ImageConfig // rewriting (ResolvedSource + ResolvedVersion). func (p *Package) ResolvedRef() string { return BuildPackageRef(p.ResolvedSource, p.ResolvedVersion) } // BuildPackageRef combines a source and version into a full package reference. // Uses "@" for digests (version contains ":") and ":" for tags. func BuildPackageRef(source, version string) string { if strings.Contains(version, ":") { return source + "@" + version } return source + ":" + version } // GetOption configures per-request package fetching behavior. type GetOption func(*GetConfig) // WithPullSecrets specifies secrets for authenticating to private registries. // These are combined with any pull secrets from ImageConfig. func WithPullSecrets(secrets ...string) GetOption { return func(c *GetConfig) { c.pullSecrets = secrets } } // WithPullPolicy specifies when to fetch from the registry vs use cache. // Default is IfNotPresent. func WithPullPolicy(policy corev1.PullPolicy) GetOption { return func(c *GetConfig) { c.pullPolicy = policy } } // GetConfig configures the client's Get method. type GetConfig struct { pullSecrets []string pullPolicy corev1.PullPolicy } // CachedClient implements Client with caching support. type CachedClient struct { fetcher Fetcher parser parser.Parser cache PackageCache config ConfigStore validator signature.Validator } // NewCachedClient creates a new package client. func NewCachedClient(f Fetcher, p parser.Parser, c PackageCache, s ConfigStore, v signature.Validator) *CachedClient { return &CachedClient{ fetcher: f, parser: p, cache: c, config: s, validator: v, } } // Get fetches and parses a complete package. func (c *CachedClient) Get(ctx context.Context, ref string, opts ...GetOption) (*Package, error) { cfg := &GetConfig{ pullPolicy: corev1.PullIfNotPresent, } for _, opt := range opts { opt(cfg) } originalRef := ref resolvedRef := ref var applied []ImageConfig name, rewritten, err := c.config.RewritePath(ctx, ref) if err != nil { return nil, errors.Wrap(err, "cannot get image rewrite config") } if rewritten != "" { resolvedRef = rewritten applied = append(applied, ImageConfig{Name: name, Reason: ImageConfigReasonRewrite}) } parsedOriginalRef, err := ociname.ParseReference(originalRef) if err != nil { return nil, errors.Wrapf(err, "cannot parse package reference %s", originalRef) } parsedResolvedRef, err := ociname.ParseReference(resolvedRef) if err != nil { return nil, errors.Wrapf(err, "cannot parse resolved package reference %s", resolvedRef) } secrets := cfg.pullSecrets name, secret, err := c.config.PullSecretFor(ctx, resolvedRef) if err != nil { return nil, errors.Wrap(err, "cannot get image pull secret config") } if secret != "" { secrets = append(secrets, secret) applied = append(applied, ImageConfig{Name: name, Reason: ImageConfigReasonSetPullSecret}) } var digest string if d, ok := parsedResolvedRef.(ociname.Digest); ok { digest = d.Identifier() } else { desc, err := c.fetcher.Head(ctx, parsedResolvedRef, secrets...) if err != nil { return nil, errors.Wrapf(err, "cannot resolve %s to digest", parsedResolvedRef.String()) } digest = desc.Digest.String() } cacheKey := FriendlyID(ParsePackageSourceFromReference(parsedOriginalRef), digest) if cfg.pullPolicy != corev1.PullAlways { rc, err := c.cache.Get(cacheKey) if err == nil { pkg, err := c.parser.Parse(ctx, struct { io.Reader io.Closer }{ Reader: io.LimitReader(rc, maxPackageSize), Closer: rc, }) rc.Close() //nolint:errcheck // Only open for reading. if err == nil { return &Package{ Package: pkg, Digest: digest, Version: parsedOriginalRef.Identifier(), Source: ParsePackageSourceFromReference(parsedOriginalRef), ResolvedVersion: parsedResolvedRef.Identifier(), ResolvedSource: ParsePackageSourceFromReference(parsedResolvedRef), AppliedImageConfigs: applied, }, nil } } } if cfg.pullPolicy == corev1.PullNever { return nil, errors.New("package not in cache and pull policy is Never") } // Verification only happens if we don't get a cache hit. This means we // won't verify packages if verification is enabled _after_ the package // was fetched and cached. // // We cache gzipped package.yaml files from the OCI image. We can't // cryptographically guarantee the cached files are the ones from the // verified package - e.g. if someone manually populated the cache. So // we're making a trade-off here. The only way to guarantee we're using // a verified OCI image on every reconcile would be to disable caching, // or cache the entire OCI image. name, vc, err := c.config.ImageVerificationConfigFor(ctx, resolvedRef) if err != nil { return nil, errors.Wrap(err, "cannot get image verification config") } if vc != nil { ref := parsedResolvedRef.Context().Digest(digest) if err := c.validator.Validate(ctx, ref, vc, secrets...); err != nil { return nil, errors.Wrap(err, "signature verification failed") } applied = append(applied, ImageConfig{Name: name, Reason: ImageConfigReasonVerify}) } img, err := c.fetcher.Fetch(ctx, parsedResolvedRef, secrets...) if err != nil { return nil, errors.Wrapf(err, "cannot fetch package %s", resolvedRef) } rc, err := ExtractPackageYAML(img) if err != nil { return nil, errors.Wrapf(err, "cannot extract package content from %s", resolvedRef) } pipeR, pipeW := io.Pipe() teeRC := TeeReadCloser(rc, pipeW) defer teeRC.Close() //nolint:errcheck // Would only error if we called pipeW.CloseWithError() go func() { defer pipeR.Close() //nolint:errcheck // Only open for reading. _ = c.cache.Store(cacheKey, pipeR) }() pkg, err := c.parser.Parse(ctx, struct { io.Reader io.Closer }{ Reader: io.LimitReader(teeRC, maxPackageSize), Closer: teeRC, }) if err != nil { return nil, errors.Wrapf(err, "cannot parse package %s", resolvedRef) } return &Package{ Package: pkg, Digest: digest, Version: parsedOriginalRef.Identifier(), Source: ParsePackageSourceFromReference(parsedOriginalRef), ResolvedVersion: parsedResolvedRef.Identifier(), ResolvedSource: ParsePackageSourceFromReference(parsedResolvedRef), AppliedImageConfigs: applied, }, nil } // ListVersions returns available versions for a package source. func (c *CachedClient) ListVersions(ctx context.Context, source string, opts ...GetOption) ([]string, error) { cfg := &GetConfig{ pullPolicy: corev1.PullIfNotPresent, } for _, opt := range opts { opt(cfg) } resolvedSource := source _, rewritten, err := c.config.RewritePath(ctx, source) if err != nil { return nil, errors.Wrap(err, "cannot get image rewrite config") } if rewritten != "" { resolvedSource = rewritten } ref, err := ociname.ParseReference(resolvedSource) if err != nil { return nil, errors.Wrapf(err, "cannot parse package source %s", resolvedSource) } secrets := cfg.pullSecrets _, secret, err := c.config.PullSecretFor(ctx, resolvedSource) if err != nil { return nil, errors.Wrap(err, "cannot get image pull secret config") } if secret != "" { secrets = append(secrets, secret) } tags, err := c.fetcher.Tags(ctx, ref, secrets...) if err != nil { return nil, errors.Wrapf(err, "cannot list tags for %s", resolvedSource) } return FilterAndSortVersions(tags), nil } // ExtractPackageYAML extracts the package.yaml file from an OCI image. // It looks for the annotated package layer (io.crossplane.xpkg: base) and // falls back to the flattened filesystem from all layers if no annotation // is found, per the xpkg specification. func ExtractPackageYAML(img v1.Image) (io.ReadCloser, error) { manifest, err := img.Manifest() if err != nil { return nil, errors.Wrap(err, "cannot get image manifest") } var tarc io.ReadCloser for _, l := range manifest.Layers { if l.Annotations[AnnotationKey] != PackageAnnotation { continue } layer, err := img.LayerByDigest(l.Digest) if err != nil { return nil, errors.Wrap(err, "cannot get annotated layer") } tarc, err = layer.Uncompressed() if err != nil { return nil, errors.Wrap(err, "cannot uncompress layer") } break // Only one annotated layer expected per xpkg spec. } if tarc == nil { tarc = mutate.Extract(img) } t := tar.NewReader(tarc) for { h, err := t.Next() if err == io.EOF { return nil, errors.New("package.yaml not found in package") } if err != nil { return nil, errors.Wrap(err, "cannot read package contents") } if filepath.Base(h.Name) == StreamFile { break } } return JoinedReadCloser(t, tarc), nil } // FilterAndSortVersions filters tags to valid semver versions and sorts them // in ascending order (oldest first). func FilterAndSortVersions(tags []string) []string { versions := make([]*semver.Version, 0, len(tags)) for _, tag := range tags { v, err := semver.NewVersion(tag) if err != nil { continue } versions = append(versions, v) } sort.Slice(versions, func(i, j int) bool { return versions[i].LessThan(versions[j]) }) result := make([]string, len(versions)) for i, v := range versions { result[i] = v.Original() } return result } ================================================ FILE: pkg/xpkg/client_test.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "archive/tar" "bytes" "context" "io" "strings" "testing" "github.com/crossplane/crossplane/apis/v2/pkg/v1beta1" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" corev1 "k8s.io/api/core/v1" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser" ) const ( testDigest = "sha256:abc123def456789012345678901234567890123456789012345678901234abcd" testSource = "xpkg.crossplane.io/crossplane-contrib/provider-aws" testTag = "v1.0.0" ) var _ Fetcher = &MockFetcher{} type MockFetcher struct { MockFetch func(context.Context, name.Reference, ...string) (v1.Image, error) MockHead func(context.Context, name.Reference, ...string) (*v1.Descriptor, error) MockTags func(context.Context, name.Reference, ...string) ([]string, error) } func (m *MockFetcher) Fetch(ctx context.Context, ref name.Reference, secrets ...string) (v1.Image, error) { return m.MockFetch(ctx, ref, secrets...) } func (m *MockFetcher) Head(ctx context.Context, ref name.Reference, secrets ...string) (*v1.Descriptor, error) { return m.MockHead(ctx, ref, secrets...) } func (m *MockFetcher) Tags(ctx context.Context, ref name.Reference, secrets ...string) ([]string, error) { return m.MockTags(ctx, ref, secrets...) } var _ PackageCache = &MockCache{} type MockCache struct { MockGet func(string) (io.ReadCloser, error) MockStore func(string, io.ReadCloser) error MockDelete func(string) error MockHas func(string) bool } func (m *MockCache) Get(key string) (io.ReadCloser, error) { return m.MockGet(key) } func (m *MockCache) Store(key string, rc io.ReadCloser) error { return m.MockStore(key, rc) } func (m *MockCache) Delete(key string) error { return m.MockDelete(key) } func (m *MockCache) Has(key string) bool { return m.MockHas(key) } var _ ConfigStore = &MockConfigStore{} type MockConfigStore struct { MockRewritePath func(context.Context, string) (string, string, error) MockPullSecretFor func(context.Context, string) (string, string, error) MockImageVerificationConfigFor func(context.Context, string) (string, *v1beta1.ImageVerification, error) MockRuntimeConfigFor func(context.Context, string) (string, *v1beta1.ImageRuntime, error) } func (m *MockConfigStore) RewritePath(ctx context.Context, ref string) (string, string, error) { return m.MockRewritePath(ctx, ref) } func (m *MockConfigStore) PullSecretFor(ctx context.Context, ref string) (string, string, error) { return m.MockPullSecretFor(ctx, ref) } func (m *MockConfigStore) ImageVerificationConfigFor(ctx context.Context, ref string) (string, *v1beta1.ImageVerification, error) { return m.MockImageVerificationConfigFor(ctx, ref) } func (m *MockConfigStore) RuntimeConfigFor(ctx context.Context, ref string) (string, *v1beta1.ImageRuntime, error) { return m.MockRuntimeConfigFor(ctx, ref) } type MockValidator struct { MockValidate func(context.Context, name.Reference, *v1beta1.ImageVerification, ...string) error } func (m *MockValidator) Validate(ctx context.Context, ref name.Reference, config *v1beta1.ImageVerification, pullSecrets ...string) error { return m.MockValidate(ctx, ref, config, pullSecrets...) } type MockImage struct { v1.Image MockManifest func() (*v1.Manifest, error) MockLayerByDigest func(v1.Hash) (v1.Layer, error) MockLayers func() ([]v1.Layer, error) } func (m *MockImage) Manifest() (*v1.Manifest, error) { return m.MockManifest() } func (m *MockImage) LayerByDigest(h v1.Hash) (v1.Layer, error) { return m.MockLayerByDigest(h) } func (m *MockImage) Layers() ([]v1.Layer, error) { if m.MockLayers != nil { return m.MockLayers() } return nil, nil } type MockLayer struct { v1.Layer content string } func NewMockLayer(content string) *MockLayer { return &MockLayer{content: content} } func (m *MockLayer) Uncompressed() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader(m.content)), nil } func CreateTarWithPackageYAML(packageYAML string) string { var buf bytes.Buffer tw := tar.NewWriter(&buf) tw.WriteHeader(&tar.Header{ Name: StreamFile, Mode: 0o644, Size: int64(len(packageYAML)), }) tw.Write([]byte(packageYAML)) tw.Close() return buf.String() } func NewTestParser(t *testing.T) parser.Parser { t.Helper() meta, err := BuildMetaScheme() if err != nil { t.Fatalf("failed to build meta scheme: %v", err) } obj, err := BuildObjectScheme() if err != nil { t.Fatalf("failed to build object scheme: %v", err) } return parser.New(meta, obj) } func NewTestPackage(t *testing.T, metaJSON string, objectsJSON ...string) *parser.Package { t.Helper() p := NewTestParser(t) var allJSON strings.Builder allJSON.WriteString("---\n") allJSON.WriteString(metaJSON) for _, objJSON := range objectsJSON { allJSON.WriteString("\n---\n") allJSON.WriteString(objJSON) } pkg, err := p.Parse(context.Background(), io.NopCloser(strings.NewReader(allJSON.String()))) if err != nil { t.Fatalf("failed to parse test package: %v", err) } return pkg } func PackageComparer() cmp.Option { return cmp.Comparer(func(a, b *parser.Package) bool { if a == nil && b == nil { return true } if a == nil || b == nil { return false } if !cmp.Equal(a.GetMeta(), b.GetMeta()) { return false } return cmp.Equal(a.GetObjects(), b.GetObjects()) }) } func TestClientGet(t *testing.T) { providerMeta := `{"apiVersion":"meta.pkg.crossplane.io/v1","kind":"Provider","metadata":{"name":"provider-aws"}}` tarContent := CreateTarWithPackageYAML(providerMeta) type args struct { ref string opts []GetOption } type want struct { pkg *Package err error } cases := map[string]struct { reason string client *CachedClient args args want want }{ "SuccessWithTag": { reason: "Should successfully fetch and parse a package with a tag reference", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { return NewMockLayer(tarContent), nil }, }, nil }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, MockStore: func(_ string, rc io.ReadCloser) error { _, _ = io.Copy(io.Discard, rc) return nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ pkg: &Package{ Package: NewTestPackage(t, providerMeta), Digest: testDigest, Version: testTag, Source: testSource, ResolvedVersion: testTag, ResolvedSource: testSource, }, }, }, "SuccessWithDigest": { reason: "Should successfully fetch a package with a digest reference without calling Head", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return nil, errors.New("Head should not be called for digest refs") }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { return NewMockLayer(tarContent), nil }, }, nil }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, MockStore: func(_ string, rc io.ReadCloser) error { _, _ = io.Copy(io.Discard, rc) return nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + "@" + testDigest, }, want: want{ pkg: &Package{ Package: NewTestPackage(t, providerMeta), Digest: testDigest, Version: testDigest, Source: testSource, ResolvedVersion: testDigest, ResolvedSource: testSource, }, }, }, "SuccessFromCache": { reason: "Should return cached package without fetching from registry", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return nil, errors.New("Fetch should not be called when cached") }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return io.NopCloser(strings.NewReader(providerMeta)), nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + ":" + testTag, opts: []GetOption{WithPullPolicy(corev1.PullIfNotPresent)}, }, want: want{ pkg: &Package{ Package: NewTestPackage(t, providerMeta), Digest: testDigest, Version: testTag, Source: testSource, ResolvedVersion: testTag, ResolvedSource: testSource, }, }, }, "SuccessWithImageConfigRewrite": { reason: "Should use rewritten path from ImageConfig and track which config was applied", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { return NewMockLayer(tarContent), nil }, }, nil }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, MockStore: func(_ string, rc io.ReadCloser) error { _, _ = io.Copy(io.Discard, rc) return nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "mirror-config", "private-registry.io/mirror/provider-aws:v1.0.0", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ pkg: &Package{ Package: NewTestPackage(t, providerMeta), Digest: testDigest, Version: testTag, Source: testSource, ResolvedVersion: testTag, ResolvedSource: "private-registry.io/mirror/provider-aws", AppliedImageConfigs: []ImageConfig{ {Name: "mirror-config", Reason: ImageConfigReasonRewrite}, }, }, }, }, "SuccessWithImageConfigRewriteAndPullSecret": { reason: "Should track both rewrite and pull secret ImageConfigs when both are applied", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { return NewMockLayer(tarContent), nil }, }, nil }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, MockStore: func(_ string, rc io.ReadCloser) error { _, _ = io.Copy(io.Discard, rc) return nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "mirror-config", "private-registry.io/mirror/provider-aws:v1.0.0", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "secret-config", "registry-secret", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ pkg: &Package{ Package: NewTestPackage(t, providerMeta), Digest: testDigest, Version: testTag, Source: testSource, ResolvedVersion: testTag, ResolvedSource: "private-registry.io/mirror/provider-aws", AppliedImageConfigs: []ImageConfig{ {Name: "mirror-config", Reason: ImageConfigReasonRewrite}, {Name: "secret-config", Reason: ImageConfigReasonSetPullSecret}, }, }, }, }, "SuccessWithPullAlways": { reason: "Should bypass cache when PullAlways is specified", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { return NewMockLayer(tarContent), nil }, }, nil }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("cache should not be checked with PullAlways") }, MockStore: func(_ string, rc io.ReadCloser) error { _, _ = io.Copy(io.Discard, rc) return nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + ":" + testTag, opts: []GetOption{WithPullPolicy(corev1.PullAlways)}, }, want: want{ pkg: &Package{ Package: NewTestPackage(t, providerMeta), Digest: testDigest, Version: testTag, Source: testSource, ResolvedVersion: testTag, ResolvedSource: testSource, }, }, }, "ErrorPullNeverNotInCache": { reason: "Should return error when PullNever is specified and package not in cache", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return nil, errors.New("Fetch should not be called with PullNever") }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + ":" + testTag, opts: []GetOption{WithPullPolicy(corev1.PullNever)}, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorInvalidReference": { reason: "Should return error for invalid package reference", client: &CachedClient{ config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: "invalid::reference", }, want: want{ err: cmpopts.AnyError, }, }, "ErrorHeadFails": { reason: "Should return error when Head request fails", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return nil, errors.New("network error") }, }, cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorFetchFails": { reason: "Should return error when Fetch fails", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return nil, errors.New("fetch failed") }, }, cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorParseFails": { reason: "Should return error when package parsing fails", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { invalidYAML := CreateTarWithPackageYAML("invalid yaml content {{{") return &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { return NewMockLayer(invalidYAML), nil }, }, nil }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, MockStore: func(_ string, rc io.ReadCloser) error { _, _ = io.Copy(io.Discard, rc) return nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ err: cmpopts.AnyError, }, }, "SuccessWithVerification": { reason: "Should successfully verify and fetch a package when verification config exists", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { return NewMockLayer(tarContent), nil }, }, nil }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, MockStore: func(_ string, rc io.ReadCloser) error { _, _ = io.Copy(io.Discard, rc) return nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "test-verification-config", &v1beta1.ImageVerification{ Provider: v1beta1.ImageVerificationProviderCosign, }, nil }, }, validator: &MockValidator{ MockValidate: func(_ context.Context, _ name.Reference, _ *v1beta1.ImageVerification, _ ...string) error { return nil }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ pkg: &Package{ Package: NewTestPackage(t, providerMeta), Digest: testDigest, Version: testTag, Source: testSource, ResolvedVersion: testTag, ResolvedSource: testSource, AppliedImageConfigs: []ImageConfig{ {Name: "test-verification-config", Reason: ImageConfigReasonVerify}, }, }, }, }, "ErrorVerificationFails": { reason: "Should return error when signature verification fails", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, MockFetch: func(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return nil, errors.New("fetch should not be called") }, }, parser: NewTestParser(t), cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, MockStore: func(_ string, _ io.ReadCloser) error { return nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "test-verification-config", &v1beta1.ImageVerification{ Provider: v1beta1.ImageVerificationProviderCosign, }, nil }, }, validator: &MockValidator{ MockValidate: func(_ context.Context, _ name.Reference, _ *v1beta1.ImageVerification, _ ...string) error { return errors.New("signature verification failed") }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorRewritePathFails": { reason: "Should return error when RewritePath config lookup fails", client: &CachedClient{ config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", errors.New("cannot list ImageConfigs") }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorPullSecretForFails": { reason: "Should return error when PullSecretFor config lookup fails", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", errors.New("cannot list ImageConfigs") }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorImageVerificationConfigForFails": { reason: "Should return error when ImageVerificationConfigFor lookup fails", client: &CachedClient{ fetcher: &MockFetcher{ MockHead: func(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return &v1.Descriptor{ Digest: v1.Hash{ Algorithm: "sha256", Hex: "abc123def456789012345678901234567890123456789012345678901234abcd", }, }, nil }, }, cache: &MockCache{ MockGet: func(_ string) (io.ReadCloser, error) { return nil, errors.New("not in cache") }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, errors.New("cannot list ImageConfigs") }, }, }, args: args{ ref: testSource + ":" + testTag, }, want: want{ err: cmpopts.AnyError, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := tc.client.Get(context.Background(), tc.args.ref, tc.args.opts...) if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { t.Errorf("\n%s\nGet(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.pkg, got, PackageComparer()); diff != "" { t.Errorf("\n%s\nGet(...): -want Package, +got Package:\n%s", tc.reason, diff) } }) } } func TestClientListVersions(t *testing.T) { type args struct { source string opts []GetOption } type want struct { versions []string err error } cases := map[string]struct { reason string client *CachedClient args args want want }{ "Success": { reason: "Should successfully list and filter versions", client: &CachedClient{ fetcher: &MockFetcher{ MockTags: func(_ context.Context, _ name.Reference, _ ...string) ([]string, error) { return []string{"v1.0.0", "v1.1.0", "v2.0.0", "latest", "main"}, nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ source: testSource, }, want: want{ versions: []string{"v1.0.0", "v1.1.0", "v2.0.0"}, }, }, "SuccessWithImageConfigRewrite": { reason: "Should use rewritten path from ImageConfig", client: &CachedClient{ fetcher: &MockFetcher{ MockTags: func(_ context.Context, _ name.Reference, _ ...string) ([]string, error) { return []string{"v1.0.0"}, nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "private-registry.io/mirror/provider-aws", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ source: testSource, }, want: want{ versions: []string{"v1.0.0"}, }, }, "ErrorInvalidSource": { reason: "Should return error for invalid source", client: &CachedClient{ config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ source: "invalid::source", }, want: want{ err: cmpopts.AnyError, }, }, "ErrorTagsFails": { reason: "Should return error when Tags request fails", client: &CachedClient{ fetcher: &MockFetcher{ MockTags: func(_ context.Context, _ name.Reference, _ ...string) ([]string, error) { return nil, errors.New("network error") }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockImageVerificationConfigFor: func(_ context.Context, _ string) (string, *v1beta1.ImageVerification, error) { return "", nil, nil }, }, }, args: args{ source: testSource, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorRewritePathFails": { reason: "Should return error when RewritePath config lookup fails", client: &CachedClient{ config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", errors.New("cannot list ImageConfigs") }, }, }, args: args{ source: testSource, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorPullSecretForFails": { reason: "Should return error when PullSecretFor config lookup fails", client: &CachedClient{ fetcher: &MockFetcher{ MockTags: func(_ context.Context, _ name.Reference, _ ...string) ([]string, error) { return []string{"v1.0.0"}, nil }, }, config: &MockConfigStore{ MockRewritePath: func(_ context.Context, _ string) (string, string, error) { return "", "", nil }, MockPullSecretFor: func(_ context.Context, _ string) (string, string, error) { return "", "", errors.New("cannot list ImageConfigs") }, }, }, args: args{ source: testSource, }, want: want{ err: cmpopts.AnyError, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := tc.client.ListVersions(context.Background(), tc.args.source, tc.args.opts...) if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { t.Errorf("\n%s\nListVersions(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.versions, got); diff != "" { t.Errorf("\n%s\nListVersions(...): -want versions, +got versions:\n%s", tc.reason, diff) } }) } } func TestExtractPackageYAML(t *testing.T) { providerMeta := `{"apiVersion":"meta.pkg.crossplane.io/v1","kind":"Provider","metadata":{"name":"provider-aws"}}` tarContent := CreateTarWithPackageYAML(providerMeta) type want struct { content string err error } cases := map[string]struct { reason string img v1.Image want want }{ "SuccessWithAnnotatedLayer": { reason: "Should extract package.yaml from annotated layer", img: &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { return NewMockLayer(tarContent), nil }, }, want: want{ content: providerMeta, }, }, "SuccessWithoutAnnotatedLayer": { reason: "Should fall back to flattened extraction when no annotated layer", img: &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: nil, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayers: func() ([]v1.Layer, error) { return []v1.Layer{NewMockLayer(tarContent)}, nil }, }, want: want{ content: providerMeta, }, }, "ErrorManifestFails": { reason: "Should return error when manifest retrieval fails", img: &MockImage{ MockManifest: func() (*v1.Manifest, error) { return nil, errors.New("manifest error") }, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorLayerByDigestFails": { reason: "Should return error when layer retrieval fails", img: &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { return nil, errors.New("layer error") }, }, want: want{ err: cmpopts.AnyError, }, }, "ErrorPackageYAMLNotFound": { reason: "Should return error when package.yaml is not in tar", img: &MockImage{ MockManifest: func() (*v1.Manifest, error) { return &v1.Manifest{ Layers: []v1.Descriptor{ { Annotations: map[string]string{ AnnotationKey: PackageAnnotation, }, Digest: v1.Hash{Algorithm: "sha256", Hex: "layer123"}, }, }, }, nil }, MockLayerByDigest: func(_ v1.Hash) (v1.Layer, error) { var buf bytes.Buffer tw := tar.NewWriter(&buf) tw.WriteHeader(&tar.Header{ Name: "other-file.txt", Mode: 0o644, Size: 5, }) tw.Write([]byte("hello")) tw.Close() return NewMockLayer(buf.String()), nil }, }, want: want{ err: cmpopts.AnyError, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := ExtractPackageYAML(tc.img) if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { t.Errorf("\n%s\nExtractPackageYAML(...): -want error, +got error:\n%s", tc.reason, diff) } if err != nil { return } defer got.Close() content, err := io.ReadAll(got) if err != nil { t.Fatalf("\n%s\nExtractPackageYAML(...): failed to read content: %v", tc.reason, err) } if diff := cmp.Diff(tc.want.content, string(content)); diff != "" { t.Errorf("\n%s\nExtractPackageYAML(...): -want content, +got content:\n%s", tc.reason, diff) } }) } } func TestFilterAndSortVersions(t *testing.T) { type args struct { tags []string } type want struct { versions []string } cases := map[string]struct { reason string args args want want }{ "Success": { reason: "Should filter non-semver tags and sort ascending", args: args{ tags: []string{"v2.0.0", "latest", "v1.0.0", "main", "v1.1.0"}, }, want: want{ versions: []string{"v1.0.0", "v1.1.0", "v2.0.0"}, }, }, "EmptyInput": { reason: "Should handle empty input", args: args{ tags: []string{}, }, want: want{ versions: []string{}, }, }, "NoValidVersions": { reason: "Should return empty slice when no valid semver tags", args: args{ tags: []string{"latest", "main", "dev"}, }, want: want{ versions: []string{}, }, }, "PreReleaseVersions": { reason: "Should include pre-release versions", args: args{ tags: []string{"v1.0.0", "v1.1.0-alpha", "v1.1.0-beta", "v1.1.0"}, }, want: want{ versions: []string{"v1.0.0", "v1.1.0-alpha", "v1.1.0-beta", "v1.1.0"}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := FilterAndSortVersions(tc.args.tags) if diff := cmp.Diff(tc.want.versions, got); diff != "" { t.Errorf("\n%s\nFilterAndSortVersions(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestPackageDigestHex(t *testing.T) { const testHex = "abc123def456789012345678901234567890123456789012345678901234abcd" cases := map[string]struct { reason string digest string want string }{ "ValidDigest": { reason: "Should return hex part of valid SHA256 digest", digest: "sha256:" + testHex, want: testHex, }, "InvalidDigest": { reason: "Should return empty string for invalid digest", digest: "invalid-digest", want: "", }, "EmptyDigest": { reason: "Should return empty string for empty digest", digest: "", want: "", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { pkg := &Package{Digest: tc.digest} got := pkg.DigestHex() if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\n%s\nPackage.DigestHex(): -want, +got:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/xpkg/config.go ================================================ package xpkg import ( "context" "strings" "github.com/crossplane/crossplane/apis/v2/pkg/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) const ( errListImageConfigs = "cannot list ImageConfigs" errFindBestMatch = "cannot find best matching ImageConfig" ) // ConfigStore is a store for image configuration. type ConfigStore interface { // PullSecretFor returns the name of the selected image config and // name of the pull secret for a given image. PullSecretFor(ctx context.Context, image string) (imageConfig, pullSecret string, err error) // ImageVerificationConfigFor returns the ImageConfig for a given image. ImageVerificationConfigFor(ctx context.Context, image string) (imageConfig string, iv *v1beta1.ImageVerification, err error) // RewritePath returns the name of the selected image config and the // rewritten path of the given image based on that config. RewritePath(ctx context.Context, image string) (imageConfig, newPath string, err error) // RuntimeConfigFor returns the name of the selected image config and the // runtime config for a given image. RuntimeConfigFor(ctx context.Context, image string) (imageConfig string, runtimeConfig *v1beta1.ImageRuntime, err error) } // isValidConfig is a function that determines if an ImageConfig is valid while // finding the best match for an image. type isValidConfig func(c *v1beta1.ImageConfig) bool // ImageConfigStoreOption is an option for image configuration store. type ImageConfigStoreOption func(*ImageConfigStore) // NewImageConfigStore creates a new image configuration store. func NewImageConfigStore(client client.Client, namespace string, opts ...ImageConfigStoreOption) ConfigStore { s := &ImageConfigStore{ client: client, namespace: namespace, } for _, opt := range opts { opt(s) } return s } // ImageConfigStore is a store for image configuration. type ImageConfigStore struct { client client.Reader namespace string } // PullSecretFor returns the pull secret name for a given image as // well as the name of the ImageConfig resource that contains the pull secret. func (s *ImageConfigStore) PullSecretFor(ctx context.Context, image string) (imageConfig, pullSecret string, err error) { config, err := s.bestMatch(ctx, image, func(c *v1beta1.ImageConfig) bool { return c.Spec.Registry != nil && c.Spec.Registry.Authentication != nil && c.Spec.Registry.Authentication.PullSecretRef.Name != "" }) if err != nil { return "", "", errors.Wrap(err, errFindBestMatch) } if config == nil { // No ImageConfig with a pull secret found for this image, this is not // an error. return "", "", nil } return config.Name, config.Spec.Registry.Authentication.PullSecretRef.Name, nil } // ImageVerificationConfigFor returns the ImageConfig for a given image. func (s *ImageConfigStore) ImageVerificationConfigFor(ctx context.Context, image string) (imageConfig string, iv *v1beta1.ImageVerification, err error) { config, err := s.bestMatch(ctx, image, func(c *v1beta1.ImageConfig) bool { return c.Spec.Verification != nil }) if err != nil { return "", nil, errors.Wrap(err, errFindBestMatch) } if config == nil { // No ImageConfig with a verification config found for this image, this // is not an error. return "", nil, nil } if config.Spec.Verification.Cosign == nil { // Only cosign verification is supported for now. return config.Name, nil, errors.New("cosign verification config is missing") } return config.Name, config.Spec.Verification, nil } // RewritePath returns the name of the selected image config and the rewritten // path of the given image based on that config. func (s *ImageConfigStore) RewritePath(ctx context.Context, image string) (imageConfig, newPath string, err error) { config, err := s.bestMatch(ctx, image, func(c *v1beta1.ImageConfig) bool { return c.Spec.RewriteImage != nil }) if err != nil { return "", "", errors.Wrap(err, errFindBestMatch) } if config == nil { // No ImageConfig with a rewrite found for this image, this is not an // error. return "", "", nil } rewritePrefix := config.Spec.RewriteImage.Prefix if rewritePrefix == "" { return config.Name, "", errors.New("rewrite prefix is missing") } // Find the longest prefix match in the selected image config; this is what // we'll replace. matchPrefix := "" for _, m := range config.Spec.MatchImages { if !strings.HasPrefix(image, m.Prefix) { continue } if len(m.Prefix) > len(matchPrefix) { matchPrefix = m.Prefix } } return config.Name, rewritePrefix + strings.TrimPrefix(image, matchPrefix), nil } // RuntimeConfigFor returns the name of the selected image config and the // runtime config for a given image. func (s *ImageConfigStore) RuntimeConfigFor(ctx context.Context, image string) (imageConfig string, runtimeConfig *v1beta1.ImageRuntime, err error) { config, err := s.bestMatch(ctx, image, func(c *v1beta1.ImageConfig) bool { return c.Spec.Runtime != nil }) if err != nil { return "", nil, errors.Wrap(err, errFindBestMatch) } if config == nil { // No ImageConfig with a runtime config found for this image, this is // not an error. return "", nil, nil } return config.Name, config.Spec.Runtime, nil } // bestMatch finds the best matching ImageConfig for an image based on the // longest prefix match. func (s *ImageConfigStore) bestMatch(ctx context.Context, image string, valid isValidConfig) (*v1beta1.ImageConfig, error) { l := &v1beta1.ImageConfigList{} if err := s.client.List(ctx, l); err != nil { return nil, errors.Wrap(err, errListImageConfigs) } var ( config *v1beta1.ImageConfig longest int ) for _, c := range l.Items { if !valid(&c) { continue } for _, m := range c.Spec.MatchImages { if strings.HasPrefix(image, m.Prefix) && len(m.Prefix) > longest { longest = len(m.Prefix) config = &c } } } return config, nil } ================================================ FILE: pkg/xpkg/config_test.go ================================================ package xpkg import ( "context" "testing" "github.com/crossplane/crossplane/apis/v2/pkg/v1beta1" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var errBoom = errors.New("boom") func TestImageConfigStoreBestMatch(t *testing.T) { type args struct { client client.Client image string isValid isValidConfig } type want struct { config *v1beta1.ImageConfig err error } cases := map[string]struct { args args want want }{ "ErrListConfig": { args: args{ image: "registry1.com/acme-co/configuration-foo", isValid: func(_ *v1beta1.ImageConfig) bool { return true }, client: &test.MockClient{ MockList: func(_ context.Context, _ client.ObjectList, _ ...client.ListOption) error { return errBoom }, }, }, want: want{ err: errors.Wrap(errBoom, errListImageConfigs), }, }, "SingleConfig": { args: args{ image: "registry1.com/acme-co/configuration-foo", isValid: func(_ *v1beta1.ImageConfig) bool { return true }, client: &test.MockClient{ MockList: func(_ context.Context, list client.ObjectList, _ ...client.ListOption) error { *list.(*v1beta1.ImageConfigList) = v1beta1.ImageConfigList{ Items: []v1beta1.ImageConfig{ { ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, } return nil }, }, }, want: want{ config: &v1beta1.ImageConfig{ ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, }, "SingleConfigMultiPrefix": { args: args{ image: "registry1.com/acme-co/configuration-foo", isValid: func(_ *v1beta1.ImageConfig) bool { return true }, client: &test.MockClient{ MockList: func(_ context.Context, list client.ObjectList, _ ...client.ListOption) error { *list.(*v1beta1.ImageConfigList) = v1beta1.ImageConfigList{ Items: []v1beta1.ImageConfig{ { ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/some-other"}, {Prefix: "registry1.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, } return nil }, }, }, want: want{ config: &v1beta1.ImageConfig{ ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/some-other"}, {Prefix: "registry1.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, }, "MultiConfig": { args: args{ image: "registry1.com/acme-co/configuration-foo", isValid: func(_ *v1beta1.ImageConfig) bool { return true }, client: &test.MockClient{ MockList: func(_ context.Context, list client.ObjectList, _ ...client.ListOption) error { *list.(*v1beta1.ImageConfigList) = v1beta1.ImageConfigList{ Items: []v1beta1.ImageConfig{ { ObjectMeta: metav1.ObjectMeta{ Name: "registry2", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry2.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, } return nil }, }, }, want: want{ config: &v1beta1.ImageConfig{ ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, }, "MultiConfigMultiPrefix": { args: args{ image: "registry1.com/acme-co/configuration-foo", isValid: func(_ *v1beta1.ImageConfig) bool { return true }, client: &test.MockClient{ MockList: func(_ context.Context, list client.ObjectList, _ ...client.ListOption) error { *list.(*v1beta1.ImageConfigList) = v1beta1.ImageConfigList{ Items: []v1beta1.ImageConfig{ { ObjectMeta: metav1.ObjectMeta{ Name: "registry2", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry2.com/some-other"}, {Prefix: "registry2.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/some-other"}, {Prefix: "registry1.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, } return nil }, }, }, want: want{ config: &v1beta1.ImageConfig{ ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/some-other"}, {Prefix: "registry1.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, }, "MultiConfigMultiMatchFindsBest": { args: args{ image: "registry1.com/acme-co/configuration-foo", isValid: func(_ *v1beta1.ImageConfig) bool { return true }, client: &test.MockClient{ MockList: func(_ context.Context, list client.ObjectList, _ ...client.ListOption) error { *list.(*v1beta1.ImageConfigList) = v1beta1.ImageConfigList{ Items: []v1beta1.ImageConfig{ { ObjectMeta: metav1.ObjectMeta{ Name: "registry1-base", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "registry1-with-org", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/some-other"}, {Prefix: "registry1.com/acme-co"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "registry1-full-image-ref", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/some-other"}, {Prefix: "registry1.com/acme-co/configuration-foo"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, } return nil }, }, }, want: want{ config: &v1beta1.ImageConfig{ ObjectMeta: metav1.ObjectMeta{ Name: "registry1-full-image-ref", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com/some-other"}, {Prefix: "registry1.com/acme-co/configuration-foo"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, }, "SkipsInvalid": { args: args{ image: "registry1.com/acme-co/configuration-foo", isValid: func(c *v1beta1.ImageConfig) bool { return c.Spec.Registry != nil }, client: &test.MockClient{ MockList: func(_ context.Context, list client.ObjectList, _ ...client.ListOption) error { *list.(*v1beta1.ImageConfigList) = v1beta1.ImageConfigList{ Items: []v1beta1.ImageConfig{ { ObjectMeta: metav1.ObjectMeta{ Name: "registry1-no-pull-secret", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ // Best match but invalid, no pull secret defined {Prefix: "registry1.com/acme-co"}, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, } return nil }, }, }, want: want{ config: &v1beta1.ImageConfig{ ObjectMeta: metav1.ObjectMeta{ Name: "registry1", }, Spec: v1beta1.ImageConfigSpec{ MatchImages: []v1beta1.ImageMatch{ {Prefix: "registry1.com"}, }, Registry: &v1beta1.RegistryConfig{ Authentication: &v1beta1.RegistryAuthentication{ PullSecretRef: corev1.LocalObjectReference{Name: "test"}, }, }, }, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { s := &ImageConfigStore{ client: tc.args.client, } got, err := s.bestMatch(context.Background(), tc.args.image, tc.args.isValid) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("bestMatch() error -want +got: %s", diff) } if diff := cmp.Diff(tc.want.config, got, cmp.AllowUnexported(v1beta1.ImageConfig{})); diff != "" { t.Errorf("bestMatch() config -want +got: %s", diff) } }) } } ================================================ FILE: pkg/xpkg/doc.go ================================================ /* Copyright 2022 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package xpkg contains functionality pertaining to Crossplane packages. package xpkg ================================================ FILE: pkg/xpkg/fake/config.go ================================================ package fake import ( "context" "github.com/crossplane/crossplane/apis/v2/pkg/v1beta1" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg" ) var _ xpkg.ConfigStore = &MockConfigStore{} // MockConfigStore is a mock ConfigStore. type MockConfigStore struct { MockPullSecretFor func(ctx context.Context, image string) (imageConfig string, pullSecret string, err error) MockImageVerificationConfigFor func(ctx context.Context, image string) (imageConfig string, verificationConfig *v1beta1.ImageVerification, err error) MockRewritePath func(ctx context.Context, image string) (imageConfig, newPath string, err error) MockRuntimeConfigFor func(ctx context.Context, image string) (imageConfig string, runtimeConfig *v1beta1.ImageRuntime, err error) } // PullSecretFor calls the underlying MockPullSecretFor. func (s *MockConfigStore) PullSecretFor(ctx context.Context, image string) (imageConfig string, pullSecret string, err error) { return s.MockPullSecretFor(ctx, image) } // ImageVerificationConfigFor calls the underlying MockImageVerificationConfigFor. func (s *MockConfigStore) ImageVerificationConfigFor(ctx context.Context, image string) (imageConfig string, verificationConfig *v1beta1.ImageVerification, err error) { return s.MockImageVerificationConfigFor(ctx, image) } // RewritePath calls the underlying MockRewritePath. func (s *MockConfigStore) RewritePath(ctx context.Context, image string) (imageConfig, newPath string, err error) { return s.MockRewritePath(ctx, image) } // RuntimeConfigFor calls the underlying MockRuntimeConfigFor. func (s *MockConfigStore) RuntimeConfigFor(ctx context.Context, image string) (imageConfig string, runtimeConfig *v1beta1.ImageRuntime, err error) { return s.MockRuntimeConfigFor(ctx, image) } // NewMockConfigStorePullSecretForFn creates a new MockPullSecretFor function for MockConfigStore. func NewMockConfigStorePullSecretForFn(imageConfig, pullSecret string, err error) func(context.Context, string) (string, string, error) { return func(context.Context, string) (string, string, error) { return imageConfig, pullSecret, err } } // NewMockConfigStoreImageVerificationConfigForFn creates a new MockImageVerificationConfigFor function for MockConfigStore. func NewMockConfigStoreImageVerificationConfigForFn(imageConfig string, verificationConfig *v1beta1.ImageVerification, err error) func(context.Context, string) (string, *v1beta1.ImageVerification, error) { return func(context.Context, string) (string, *v1beta1.ImageVerification, error) { return imageConfig, verificationConfig, err } } // NewMockRewritePathFn creates a new MockRewritePath function for // MockConfigStore. func NewMockRewritePathFn(imageConfig, newPath string, err error) func(context.Context, string) (string, string, error) { return func(_ context.Context, _ string) (string, string, error) { return imageConfig, newPath, err } } // NewMockRuntimeConfigForFn creates a new MockRewritePath function for // MockConfigStore. func NewMockRuntimeConfigForFn(imageConfig string, runtimeConfig *v1beta1.ImageRuntime, err error) func(context.Context, string) (string, *v1beta1.ImageRuntime, error) { return func(_ context.Context, _ string) (string, *v1beta1.ImageRuntime, error) { return imageConfig, runtimeConfig, err } } ================================================ FILE: pkg/xpkg/fake/mocks.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package fake contains mock Crossplane package implementations. package fake import ( "context" "io" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg" ) var _ xpkg.PackageCache = &MockCache{} // MockCache is a mock Cache. type MockCache struct { MockHas func() bool MockGet func() (io.ReadCloser, error) MockStore func(s string, rc io.ReadCloser) error MockDelete func() error } // NewMockCacheHasFn creates a new MockGet function for MockCache. func NewMockCacheHasFn(has bool) func() bool { return func() bool { return has } } // NewMockCacheGetFn creates a new MockGet function for MockCache. func NewMockCacheGetFn(rc io.ReadCloser, err error) func() (io.ReadCloser, error) { return func() (io.ReadCloser, error) { return rc, err } } // NewMockCacheStoreFn creates a new MockStore function for MockCache. func NewMockCacheStoreFn(err error) func(s string, rc io.ReadCloser) error { return func(_ string, _ io.ReadCloser) error { return err } } // NewMockCacheDeleteFn creates a new MockDelete function for MockCache. func NewMockCacheDeleteFn(err error) func() error { return func() error { return err } } // Has calls the underlying MockHas. func (c *MockCache) Has(string) bool { return c.MockHas() } // Get calls the underlying MockGet. func (c *MockCache) Get(string) (io.ReadCloser, error) { return c.MockGet() } // Store calls the underlying MockStore. func (c *MockCache) Store(s string, rc io.ReadCloser) error { return c.MockStore(s, rc) } // Delete calls the underlying MockDelete. func (c *MockCache) Delete(string) error { return c.MockDelete() } var _ xpkg.Fetcher = &MockFetcher{} // MockFetcher is a mock fetcher. type MockFetcher struct { MockFetch func() (v1.Image, error) MockHead func(name.Reference) (*v1.Descriptor, error) MockTags func(name.Reference) ([]string, error) } // NewMockFetchFn creates a new MockFetch function for MockFetcher. func NewMockFetchFn(img v1.Image, err error) func() (v1.Image, error) { return func() (v1.Image, error) { return img, err } } // Fetch calls the underlying MockFetch. func (m *MockFetcher) Fetch(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return m.MockFetch() } // NewMockHeadFn creates a new MockHead function for MockFetcher. func NewMockHeadFn(d *v1.Descriptor, err error) func(name.Reference) (*v1.Descriptor, error) { return func(_ name.Reference) (*v1.Descriptor, error) { return d, err } } // Head calls the underlying MockHead. func (m *MockFetcher) Head(_ context.Context, ref name.Reference, _ ...string) (*v1.Descriptor, error) { return m.MockHead(ref) } // NewMockTagsFn creates a new MockTags function for MockFetcher. func NewMockTagsFn(tags []string, err error) func(name.Reference) ([]string, error) { return func(_ name.Reference) ([]string, error) { return tags, err } } // Tags calls the underlying MockTags. func (m *MockFetcher) Tags(_ context.Context, ref name.Reference, _ ...string) ([]string, error) { return m.MockTags(ref) } var _ xpkg.Client = &MockClient{} // MockClient is a mock xpkg.Client. type MockClient struct { MockGet func(ctx context.Context, ref string, opts ...xpkg.GetOption) (*xpkg.Package, error) MockListVersions func(ctx context.Context, source string, opts ...xpkg.GetOption) ([]string, error) } // Get calls the underlying MockGet. func (c *MockClient) Get(ctx context.Context, ref string, opts ...xpkg.GetOption) (*xpkg.Package, error) { return c.MockGet(ctx, ref, opts...) } // ListVersions calls the underlying MockListVersions. func (c *MockClient) ListVersions(ctx context.Context, source string, opts ...xpkg.GetOption) ([]string, error) { return c.MockListVersions(ctx, source, opts...) } // NewMockGetFn creates a new MockGet function for MockClient. func NewMockGetFn(pkg *xpkg.Package, err error) func(context.Context, string, ...xpkg.GetOption) (*xpkg.Package, error) { return func(context.Context, string, ...xpkg.GetOption) (*xpkg.Package, error) { return pkg, err } } // NewMockListVersionsFn creates a new MockListVersions function for MockClient. func NewMockListVersionsFn(versions []string, err error) func(context.Context, string, ...xpkg.GetOption) ([]string, error) { return func(context.Context, string, ...xpkg.GetOption) ([]string, error) { return versions, err } } ================================================ FILE: pkg/xpkg/fetch.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "context" "crypto/tls" "crypto/x509" "io" "net/http" "github.com/google/go-containerregistry/pkg/authn/k8schain" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/sirupsen/logrus" "k8s.io/client-go/kubernetes" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) func init() { //nolint:gochecknoinits // See comment below. // NOTE(hasheddan): we set the logrus package-level logger to discard output // due to the fact that the AWS ECR credential helper uses it to log errors // when parsing registry server URL, which happens any time a package is // pulled from a non-ECR registry. // https://github.com/awslabs/amazon-ecr-credential-helper/issues/308 logrus.SetOutput(io.Discard) } // Fetcher fetches package images. type Fetcher interface { Fetch(ctx context.Context, ref name.Reference, secrets ...string) (v1.Image, error) Head(ctx context.Context, ref name.Reference, secrets ...string) (*v1.Descriptor, error) Tags(ctx context.Context, ref name.Reference, secrets ...string) ([]string, error) } // K8sFetcher uses kubernetes credentials to fetch package images. type K8sFetcher struct { client kubernetes.Interface namespace string serviceAccount string transport http.RoundTripper userAgent string } // FetcherOpt can be used to add optional parameters to NewK8sFetcher. type FetcherOpt func(k *K8sFetcher) error // WithCustomCA is a FetcherOpt that can be used to add a custom CA bundle to a K8sFetcher. func WithCustomCA(rootCAs *x509.CertPool) FetcherOpt { return func(k *K8sFetcher) error { t, ok := k.transport.(*http.Transport) if !ok { return errors.New("Fetcher transport is not an HTTP transport") } t.TLSClientConfig = &tls.Config{RootCAs: rootCAs, MinVersion: tls.VersionTLS12} return nil } } // WithUserAgent is a FetcherOpt that can be used to set the user agent on all HTTP requests. func WithUserAgent(userAgent string) FetcherOpt { return func(k *K8sFetcher) error { // TODO(hasheddan): go-containerregistry currently does not allow for // removal of the go-containerregistry user-agent header, so the // provided one is appended rather than replacing. In the future, this // should be replaced with wrapping the transport with // transport.NewUserAgent. k.userAgent = userAgent return nil } } // WithNamespace is a FetcherOpt that sets the Namespace for fetching package // pull secrets. func WithNamespace(ns string) FetcherOpt { return func(k *K8sFetcher) error { k.namespace = ns return nil } } // WithServiceAccount is a FetcherOpt that sets the ServiceAccount name for // fetching package pull secrets. func WithServiceAccount(sa string) FetcherOpt { return func(k *K8sFetcher) error { k.serviceAccount = sa return nil } } // NewK8sFetcher creates a new K8sFetcher. func NewK8sFetcher(client kubernetes.Interface, opts ...FetcherOpt) (*K8sFetcher, error) { dt, ok := remote.DefaultTransport.(*http.Transport) if !ok { return nil, errors.Errorf("default transport was not a %T", &http.Transport{}) } k := &K8sFetcher{ client: client, transport: dt.Clone(), } for _, o := range opts { if err := o(k); err != nil { return nil, err } } return k, nil } // Fetch fetches a package image. func (i *K8sFetcher) Fetch(ctx context.Context, ref name.Reference, secrets ...string) (v1.Image, error) { auth, err := k8schain.New(ctx, i.client, k8schain.Options{ Namespace: i.namespace, ServiceAccountName: i.serviceAccount, ImagePullSecrets: secrets, }) if err != nil { return nil, err } return remote.Image(ref, remote.WithAuthFromKeychain(auth), remote.WithTransport(i.transport), remote.WithContext(ctx), remote.WithUserAgent(i.userAgent), ) } // Head fetches a package descriptor. func (i *K8sFetcher) Head(ctx context.Context, ref name.Reference, secrets ...string) (*v1.Descriptor, error) { auth, err := k8schain.New(ctx, i.client, k8schain.Options{ Namespace: i.namespace, ServiceAccountName: i.serviceAccount, ImagePullSecrets: secrets, }) if err != nil { return nil, err } d, err := remote.Head(ref, remote.WithAuthFromKeychain(auth), remote.WithTransport(i.transport), remote.WithContext(ctx), remote.WithUserAgent(i.userAgent), ) if err != nil || d == nil { rd, gErr := remote.Get(ref, remote.WithAuthFromKeychain(auth), remote.WithTransport(i.transport), remote.WithContext(ctx), remote.WithUserAgent(i.userAgent), ) if gErr != nil { return nil, errors.Wrapf(gErr, "failed to fetch package descriptor with a GET request after a previous HEAD request failure: %v", err) } return &rd.Descriptor, nil } return d, nil } // Tags fetches a package's tags. func (i *K8sFetcher) Tags(ctx context.Context, ref name.Reference, secrets ...string) ([]string, error) { auth, err := k8schain.New(ctx, i.client, k8schain.Options{ Namespace: i.namespace, ServiceAccountName: i.serviceAccount, ImagePullSecrets: secrets, }) if err != nil { return nil, err } return remote.List(ref.Context(), remote.WithAuthFromKeychain(auth), remote.WithTransport(i.transport), remote.WithContext(ctx), remote.WithUserAgent(i.userAgent), ) } // NopFetcher always returns an empty image and never returns error. type NopFetcher struct{} // NewNopFetcher creates a new NopFetcher. func NewNopFetcher() *NopFetcher { return &NopFetcher{} } // Fetch fetches an empty image and does not return error. func (n *NopFetcher) Fetch(_ context.Context, _ name.Reference, _ ...string) (v1.Image, error) { return empty.Image, nil } // Head returns a nil descriptor and does not return error. func (n *NopFetcher) Head(_ context.Context, _ name.Reference, _ ...string) (*v1.Descriptor, error) { return nil, nil } // Tags returns a nil slice and does not return error. func (n *NopFetcher) Tags(_ context.Context, _ name.Reference, _ ...string) ([]string, error) { return nil, nil } ================================================ FILE: pkg/xpkg/find.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "path/filepath" "github.com/spf13/afero" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) const ( errNoMatch = "directory does not contain a compiled crossplane package" errMultiMatch = "directory contains multiple compiled crossplane packages" ) // FindXpkgInDir finds compiled Crossplane packages in a directory. func FindXpkgInDir(fs afero.Fs, root string) (string, error) { f, err := fs.Open(root) if err != nil { return "", err } defer func() { _ = f.Close() }() files, err := f.Readdir(-1) if err != nil { return "", err } path := "" for _, file := range files { // Match only returns an error if XpkgMatchPattern is malformed. match, _ := filepath.Match(XpkgMatchPattern, file.Name()) if !match { continue } if path != "" && match { return "", errors.New(errMultiMatch) } path = file.Name() } if path == "" { return "", errors.New(errNoMatch) } return path, nil } ================================================ FILE: pkg/xpkg/find_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "os" "testing" "github.com/google/go-cmp/cmp" "github.com/spf13/afero" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) func TestFindXpkgInDir(t *testing.T) { match := afero.NewMemMapFs() _ = afero.WriteFile(match, "one.xpkg", []byte{}, StreamFileMode) multi := afero.NewMemMapFs() _ = afero.WriteFile(multi, "one.xpkg", []byte{}, StreamFileMode) _ = afero.WriteFile(multi, "two.xpkg", []byte{}, StreamFileMode) type args struct { root string fs afero.Fs } type want struct { path string err error } cases := map[string]struct { reason string args args want want }{ "NoMatch": { reason: "We should return an error if no matches.", args: args{ root: ".", fs: afero.NewMemMapFs(), }, want: want{ err: errors.New(errNoMatch), }, }, "MultiMatch": { reason: "We should return an error if multiple matches.", args: args{ root: ".", fs: multi, }, want: want{ err: errors.New(errMultiMatch), }, }, "NotExist": { reason: "We should return an error root does not exist.", args: args{ root: "/test", fs: afero.NewMemMapFs(), }, want: want{ err: &os.PathError{Op: "open", Path: "/test", Err: os.ErrNotExist}, }, }, "NotDir": { reason: "We should return an error if root is not a directory.", args: args{ root: "one.xpkg", fs: multi, }, want: want{ err: &os.PathError{Op: "readdir", Path: "one.xpkg", Err: errors.New("not a dir")}, }, }, "Successful": { reason: "We should return file path if one package exists.", args: args{ root: ".", fs: match, }, want: want{ path: "one.xpkg", }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { path, err := FindXpkgInDir(tc.args.fs, tc.args.root) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nFindXpkgInDir(...): -want, +got:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.path, path); diff != "" { t.Errorf("\n%s\nFindXpkgInDir(...): -want, +got:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/xpkg/fuzz_test.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "testing" fuzz "github.com/AdaLogics/go-fuzz-headers" "github.com/spf13/afero" ) func FuzzFindXpkgInDir(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { ff := fuzz.NewConsumer(data) noOfFiles, err := ff.GetInt() if err != nil { t.Skip() } fs := afero.NewMemMapFs() createdFiles := make([]string, 0) defer func() { for _, createdFile := range createdFiles { fs.Remove(createdFile) } }() for range noOfFiles % 500 { fname, err := ff.GetString() if err != nil { t.Skip() } fcontents, err := ff.GetBytes() if err != nil { t.Skip() } if err = afero.WriteFile(fs, fname, fcontents, 0o777); err != nil { t.Skip() } } _, _ = FindXpkgInDir(fs, "/") _, _ = ParseNameFromMeta(fs, "/") }) } ================================================ FILE: pkg/xpkg/layers.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "archive/tar" "bytes" "fmt" "io" "os" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) // Error strings. const ( errLayer = "cannot get image layers" errDigest = "cannot get image digest" ) // Layer creates a v1.Layer that represents the layer contents for the xpkg and // adds a corresponding label to the image Config for the layer. func Layer(r io.Reader, fileName, annotation string, fileSize int64, mode os.FileMode, cfg *v1.Config) (v1.Layer, error) { tarBuf := new(bytes.Buffer) tw := tar.NewWriter(tarBuf) exHdr := &tar.Header{ Name: fileName, Mode: int64(mode), Size: fileSize, } if err := writeLayer(tw, exHdr, r); err != nil { return nil, err } // TODO(hasheddan): we currently return a new reader every time here in // order to calculate digest, then subsequently write contents to disk. We // can greatly improve performance during package build by avoiding reading // every layer into memory. layer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(tarBuf.Bytes())), nil }) if err != nil { return nil, errors.Wrap(err, errLayerFromTar) } d, err := layer.Digest() if err != nil { return nil, errors.Wrap(err, errDigestInvalid) } // Add annotation label to config if a non-empty label is specified. This is // an intermediary step. AnnotateLayers must be called on an image for it to // have valid layer annotations. It propagates these labels to annotations // on the layers. if annotation != "" { cfg.Labels[Label(d.String())] = annotation } return layer, nil } func writeLayer(tw *tar.Writer, hdr *tar.Header, buf io.Reader) error { if err := tw.WriteHeader(hdr); err != nil { return errors.Wrap(err, errTarFromStream) } if _, err := io.Copy(tw, buf); err != nil { return errors.Wrap(err, errTarFromStream) } if err := tw.Close(); err != nil { return errors.Wrap(err, errTarFromStream) } return nil } // Label constructs a specially formated label using the annotationKey. func Label(annotation string) string { return fmt.Sprintf("%s:%s", AnnotationKey, annotation) } // NOTE(negz): AnnotateLayers originated in upbound/up. I was confused why we // store layer annotations as labels in the OCI config file when we build a // package, then propagate them to OCI layer annotations when we push one. I // believe this is because an xpkg file is really an OCI image tarball, and the // tarball format doesn't support layer annotations (or may just lose them in // some circumstances?), so we're using the config file to store them. // See https://github.com/upbound/up/pull/177#discussion_r866776584. // AnnotateLayers propagates labels from the supplied image's config file to // annotations on its layers. func AnnotateLayers(i v1.Image) (v1.Image, error) { cfgFile, err := i.ConfigFile() if err != nil { return nil, errors.Wrap(err, errConfigFile) } layers, err := i.Layers() if err != nil { return nil, errors.Wrap(err, errLayer) } addendums := make([]mutate.Addendum, 0) for _, l := range layers { d, err := l.Digest() if err != nil { return nil, errors.Wrap(err, errDigest) } if annotation, ok := cfgFile.Config.Labels[Label(d.String())]; ok { addendums = append(addendums, mutate.Addendum{ Layer: l, Annotations: map[string]string{ AnnotationKey: annotation, }, }) continue } addendums = append(addendums, mutate.Addendum{ Layer: l, }) } // we didn't find any annotations, return original image if len(addendums) == 0 { return i, nil } img := empty.Image for _, a := range addendums { img, err = mutate.Append(img, a) if err != nil { return nil, errors.Wrap(err, errBuildImage) } } return mutate.ConfigFile(img, cfgFile) } ================================================ FILE: pkg/xpkg/lint.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "github.com/Masterminds/semver/v3" v1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1" extv1alpha1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1alpha1" v2 "github.com/crossplane/crossplane/apis/v2/apiextensions/v2" "github.com/crossplane/crossplane/apis/v2/ops/v1alpha1" pkgmetav1 "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1" admv1 "k8s.io/api/admissionregistration/v1" extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" extv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/runtime" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/version" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser" ) const ( errNotExactlyOneMeta = "not exactly one package meta type" errNotMeta = "meta type is not a package" errNotMetaProvider = "package meta type is not Provider" errNotMetaConfiguration = "package meta type is not Configuration" errNotMetaFunction = "package meta type is not Function" errNotCRD = "object is not a CRD" errNotMRD = "object is not an MRD" errNotXRD = "object is not an XRD" errNotMutatingWebhookConfiguration = "object is not a MutatingWebhookConfiguration" errNotValidatingWebhookConfiguration = "object is not an ValidatingWebhookConfiguration" errNotComposition = "object is not a Composition" errNotActivationPolicy = "object is not an ManagedResourceActivationPolicy" errNotOperation = "object is not an Operation" errNotCronOperation = "object is not a CronOperation" errNotWatchOperation = "object is not a WatchOperation" errBadConstraints = "package version constraints are poorly formatted" errFmtCrossplaneIncompatible = "package is not compatible with Crossplane version (%s)" ) // NewProviderLinter is a convenience function for creating a package linter for // providers. func NewProviderLinter() parser.Linter { return parser.NewPackageLinter( parser.PackageLinterFns(OneMeta), parser.ObjectLinterFns(IsProvider, PackageValidSemver), parser.ObjectLinterFns(parser.Or( IsCRD, IsMRD, IsValidatingWebhookConfiguration, IsMutatingWebhookConfiguration, ))) } // NewConfigurationLinter is a convenience function for creating a package linter for // configurations. func NewConfigurationLinter() parser.Linter { return parser.NewPackageLinter( parser.PackageLinterFns(OneMeta), parser.ObjectLinterFns(IsConfiguration, PackageValidSemver), parser.ObjectLinterFns(parser.Or(IsXRD, IsActivationPolicy, IsComposition, IsOperation, IsCronOperation, IsWatchOperation))) } // NewFunctionLinter is a convenience function for creating a package linter for // functions. func NewFunctionLinter() parser.Linter { return parser.NewPackageLinter( parser.PackageLinterFns(OneMeta), parser.ObjectLinterFns(IsFunction, PackageValidSemver), parser.ObjectLinterFns(IsCRD)) } // OneMeta checks that there is only one meta object in the package. func OneMeta(pkg parser.Lintable) error { if len(pkg.GetMeta()) != 1 { return errors.New(errNotExactlyOneMeta) } return nil } // IsProvider checks that an object is a Provider meta type. func IsProvider(o runtime.Object) error { po, _ := TryConvert(o, &pkgmetav1.Provider{}) if _, ok := po.(*pkgmetav1.Provider); !ok { return errors.New(errNotMetaProvider) } return nil } // IsConfiguration checks that an object is a Configuration meta type. func IsConfiguration(o runtime.Object) error { po, _ := TryConvert(o, &pkgmetav1.Configuration{}) if _, ok := po.(*pkgmetav1.Configuration); !ok { return errors.New(errNotMetaConfiguration) } return nil } // IsFunction checks that an object is a Function meta type. func IsFunction(o runtime.Object) error { po, _ := TryConvert(o, &pkgmetav1.Function{}) if _, ok := po.(*pkgmetav1.Function); !ok { return errors.New(errNotMetaFunction) } return nil } // PackageCrossplaneCompatible checks that the current Crossplane version is // compatible with the package constraints. func PackageCrossplaneCompatible(v version.Operations) parser.ObjectLinterFn { return func(o runtime.Object) error { p, ok := TryConvertToPkg(o, &pkgmetav1.Provider{}, &pkgmetav1.Configuration{}, &pkgmetav1.Function{}) if !ok { return errors.New(errNotMeta) } if p.GetCrossplaneConstraints() == nil { return nil } in, err := v.InConstraints(p.GetCrossplaneConstraints().Version) if err != nil { return errors.Wrapf(err, errFmtCrossplaneIncompatible, v.GetVersionString()) } if !in { return errors.Errorf(errFmtCrossplaneIncompatible, v.GetVersionString()) } return nil } } // PackageValidSemver checks that the package uses valid semver ranges. func PackageValidSemver(o runtime.Object) error { p, ok := TryConvertToPkg(o, &pkgmetav1.Provider{}, &pkgmetav1.Configuration{}, &pkgmetav1.Function{}) if !ok { return errors.New(errNotMeta) } if p.GetCrossplaneConstraints() == nil { return nil } if _, err := semver.NewConstraint(p.GetCrossplaneConstraints().Version); err != nil { return errors.Wrap(err, errBadConstraints) } return nil } // IsCRD checks that an object is a CustomResourceDefinition. func IsCRD(o runtime.Object) error { switch o.(type) { case *extv1beta1.CustomResourceDefinition, *extv1.CustomResourceDefinition: return nil default: return errors.New(errNotCRD) } } // IsMRD checks that an object is a ManagedResourceDefinition. func IsMRD(o runtime.Object) error { switch o.(type) { case *extv1alpha1.ManagedResourceDefinition: return nil default: return errors.New(errNotMRD) } } // IsMutatingWebhookConfiguration checks that an object is a MutatingWebhookConfiguration. func IsMutatingWebhookConfiguration(o runtime.Object) error { if _, ok := o.(*admv1.MutatingWebhookConfiguration); !ok { return errors.New(errNotMutatingWebhookConfiguration) } return nil } // IsValidatingWebhookConfiguration checks that an object is a ValidatingWebhookConfiguration. func IsValidatingWebhookConfiguration(o runtime.Object) error { if _, ok := o.(*admv1.ValidatingWebhookConfiguration); !ok { return errors.New(errNotValidatingWebhookConfiguration) } return nil } // IsXRD checks that an object is a CompositeResourceDefinition. func IsXRD(o runtime.Object) error { switch o.(type) { case *v1.CompositeResourceDefinition, *v2.CompositeResourceDefinition: return nil default: return errors.New(errNotXRD) } } // IsComposition checks that an object is a Composition. func IsComposition(o runtime.Object) error { if _, ok := o.(*v1.Composition); !ok { return errors.New(errNotComposition) } return nil } // IsActivationPolicy checks that an object is an ManagedResourceActivationPolicy. func IsActivationPolicy(o runtime.Object) error { if _, ok := o.(*extv1alpha1.ManagedResourceActivationPolicy); !ok { return errors.New(errNotActivationPolicy) } return nil } // IsOperation checks that an object is an Operation. func IsOperation(o runtime.Object) error { if _, ok := o.(*v1alpha1.Operation); !ok { return errors.New(errNotOperation) } return nil } // IsCronOperation checks that an object is a CronOperation. func IsCronOperation(o runtime.Object) error { if _, ok := o.(*v1alpha1.CronOperation); !ok { return errors.New(errNotCronOperation) } return nil } // IsWatchOperation checks that an object is a WatchOperation. func IsWatchOperation(o runtime.Object) error { if _, ok := o.(*v1alpha1.WatchOperation); !ok { return errors.New(errNotWatchOperation) } return nil } ================================================ FILE: pkg/xpkg/lint_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "bytes" "context" "fmt" "io" "testing" v1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1" extv1alpha1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1alpha1" "github.com/crossplane/crossplane/apis/v2/ops/v1alpha1" pkgmetav1 "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1" pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1alpha1" pkgmetav1beta1 "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1beta1" "github.com/google/go-cmp/cmp" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/yaml" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/crossplane/crossplane-runtime/v2/pkg/version" "github.com/crossplane/crossplane-runtime/v2/pkg/version/fake" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser" ) var ( v1beta1CRDBytes = []byte(`apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: test`) v1CRDBytes = []byte(`apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: test`) v1alpha1ProvBytes = []byte(`apiVersion: meta.pkg.crossplane.io/v1alpha1 kind: Provider metadata: name: test`) v1alpha1ConfBytes = []byte(`apiVersion: meta.pkg.crossplane.io/v1alpha1 kind: Configuration metadata: name: test`) v1beta1FuncBytes = []byte(`apiVersion: meta.pkg.crossplane.io/v1beta1 kind: Function metadata: name: test`) v1ProvBytes = []byte(`apiVersion: meta.pkg.crossplane.io/v1 kind: Provider metadata: name: test`) v1ConfBytes = []byte(`apiVersion: meta.pkg.crossplane.io/v1 kind: Configuration metadata: name: test`) v1FuncBytes = []byte(`apiVersion: meta.pkg.crossplane.io/v1 kind: Function metadata: name: test`) v1XRDBytes = []byte(`apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: test`) v1CompBytes = []byte(`apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: test`) v1alpha1OpBytes = []byte(`apiVersion: ops.crossplane.io/v1alpha1 kind: Operation metadata: name: test`) v1alpha1CronOpBytes = []byte(`apiVersion: ops.crossplane.io/v1alpha1 kind: CronOperation metadata: name: test`) v1alpha1WatchOpBytes = []byte(`apiVersion: ops.crossplane.io/v1alpha1 kind: WatchOperation metadata: name: test`) v1alpha1MRDBytes = []byte(`apiVersion: apiextensions.crossplane.io/v1alpha1 kind: ManagedResourceDefinition metadata: name: test`) v1alpha1ActivationPolicyBytes = []byte(`apiVersion: apiextensions.crossplane.io/v1alpha1 kind: ManagedResourceActivationPolicy metadata: name: test`) v1beta1crd = &apiextensions.CustomResourceDefinition{} _ = yaml.Unmarshal(v1beta1CRDBytes, v1beta1crd) v1crd = &apiextensions.CustomResourceDefinition{} _ = yaml.Unmarshal(v1CRDBytes, v1crd) v1alpha1ProvMeta = &pkgmetav1alpha1.Provider{} _ = yaml.Unmarshal(v1alpha1ProvBytes, v1alpha1ProvMeta) v1alpha1ConfMeta = &pkgmetav1alpha1.Configuration{} _ = yaml.Unmarshal(v1alpha1ConfBytes, v1alpha1ConfMeta) v1beta1FuncMeta = &pkgmetav1beta1.Function{} _ = yaml.Unmarshal(v1beta1FuncBytes, v1beta1FuncMeta) v1ProvMeta = &pkgmetav1.Provider{} _ = yaml.Unmarshal(v1ProvBytes, v1ProvMeta) v1ConfMeta = &pkgmetav1.Configuration{} _ = yaml.Unmarshal(v1ConfBytes, v1ConfMeta) v1FuncMeta = &pkgmetav1.Function{} _ = yaml.Unmarshal(v1FuncBytes, v1FuncMeta) v1XRD = &v1.CompositeResourceDefinition{} _ = yaml.Unmarshal(v1XRDBytes, v1XRD) v1Comp = &v1.Composition{} _ = yaml.Unmarshal(v1CompBytes, v1Comp) v1alpha1Op = &v1alpha1.Operation{} _ = yaml.Unmarshal(v1alpha1OpBytes, v1alpha1Op) v1alpha1CronOp = &v1alpha1.CronOperation{} _ = yaml.Unmarshal(v1alpha1CronOpBytes, v1alpha1CronOp) v1alpha1WatchOp = &v1alpha1.WatchOperation{} _ = yaml.Unmarshal(v1alpha1WatchOpBytes, v1alpha1WatchOp) v1alpha1MRD = &extv1alpha1.ManagedResourceDefinition{} _ = yaml.Unmarshal(v1alpha1MRDBytes, v1alpha1MRD) v1alpha1ActivationPolicy = &extv1alpha1.ManagedResourceActivationPolicy{} _ = yaml.Unmarshal(v1alpha1ActivationPolicyBytes, v1alpha1ActivationPolicy) meta, _ = BuildMetaScheme() obj, _ = BuildObjectScheme() p = parser.New(meta, obj) ) func TestOneMeta(t *testing.T) { oneR := bytes.NewReader(bytes.Join([][]byte{v1beta1CRDBytes, v1alpha1ProvBytes}, []byte("\n---\n"))) oneMeta, _ := p.Parse(context.TODO(), io.NopCloser(oneR)) noneR := bytes.NewReader(v1beta1CRDBytes) noneMeta, _ := p.Parse(context.TODO(), io.NopCloser(noneR)) multiR := bytes.NewReader(bytes.Join([][]byte{v1alpha1ProvBytes, v1alpha1ProvBytes}, []byte("\n---\n"))) multiMeta, _ := p.Parse(context.TODO(), io.NopCloser(multiR)) cases := map[string]struct { reason string pkg *parser.Package err error }{ "Successful": { reason: "Should not return error if only one meta object.", pkg: oneMeta, }, "ErrNoMeta": { reason: "Should return error if no meta objects.", pkg: noneMeta, err: errors.New(errNotExactlyOneMeta), }, "ErrMultiMeta": { reason: "Should return error if multiple meta objects.", pkg: multiMeta, err: errors.New(errNotExactlyOneMeta), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := OneMeta(tc.pkg) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nOneMeta(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsProvider(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1alpha1": { reason: "Should not return error if object is a v1alpha1 provider.", obj: v1alpha1ProvMeta, }, "v1": { reason: "Should not return error if object is a v1 provider.", obj: v1ProvMeta, }, "ErrNotProvider": { reason: "Should return error if object is not provider.", obj: v1beta1crd, err: errors.New(errNotMetaProvider), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsProvider(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsProvider(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsConfiguration(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1alpha1": { reason: "Should not return error if object is a v1alpha1 configuration.", obj: v1alpha1ConfMeta, }, "v1": { reason: "Should not return error if object is a v1 configuration.", obj: v1ConfMeta, }, "ErrNotConfiguration": { reason: "Should return error if object is not configuration.", obj: v1beta1crd, err: errors.New(errNotMetaConfiguration), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsConfiguration(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsConfiguration(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsFunction(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ // Function packages were introduced at v1beta1. There was never a // v1alpha1 version of the package metadata. "v1beta1": { reason: "Should not return error if object is a v1beta1 function.", obj: v1beta1FuncMeta, }, "v1": { reason: "Should not return error if object is a v1 function.", obj: v1FuncMeta, }, "ErrNotFunction": { reason: "Should return error if object is not function.", obj: v1beta1crd, err: errors.New(errNotMetaFunction), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsFunction(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsFunction(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestPackageCrossplaneCompatible(t *testing.T) { crossplaneConstraint := ">v0.13.0" errBoom := errors.New("boom") type args struct { obj runtime.Object ver version.Operations } cases := map[string]struct { reason string args args err error }{ "Successful": { reason: "Should not return error if Crossplane version within constraints.", args: args{ obj: &pkgmetav1.Configuration{ Spec: pkgmetav1.ConfigurationSpec{ MetaSpec: pkgmetav1.MetaSpec{ Crossplane: &pkgmetav1.CrossplaneConstraints{ Version: crossplaneConstraint, }, }, }, }, ver: &fake.MockVersioner{ MockInConstraints: fake.NewMockInConstraintsFn(true, nil), }, }, }, "SuccessfulNoConstraints": { reason: "Should not return error if no constraints provided.", args: args{ obj: v1ProvMeta, }, }, "ErrInvalidConstraints": { reason: "Should return error if constraints are invalid.", args: args{ obj: &pkgmetav1.Configuration{ Spec: pkgmetav1.ConfigurationSpec{ MetaSpec: pkgmetav1.MetaSpec{ Crossplane: &pkgmetav1.CrossplaneConstraints{ Version: crossplaneConstraint, }, }, }, }, ver: &fake.MockVersioner{ MockInConstraints: fake.NewMockInConstraintsFn(false, errBoom), MockGetVersionString: fake.NewMockGetVersionStringFn("v0.12.0"), }, }, err: errors.Wrapf(errBoom, errFmtCrossplaneIncompatible, "v0.12.0"), }, "ErrOutsideConstraints": { reason: "Should return error if Crossplane version outside constraints.", args: args{ obj: &pkgmetav1.Configuration{ Spec: pkgmetav1.ConfigurationSpec{ MetaSpec: pkgmetav1.MetaSpec{ Crossplane: &pkgmetav1.CrossplaneConstraints{ Version: crossplaneConstraint, }, }, }, }, ver: &fake.MockVersioner{ MockInConstraints: fake.NewMockInConstraintsFn(false, nil), MockGetVersionString: fake.NewMockGetVersionStringFn("v0.12.0"), }, }, err: errors.Errorf(errFmtCrossplaneIncompatible, "v0.12.0"), }, "ErrNotMeta": { reason: "Should return error if object is not a meta package type.", args: args{ obj: v1crd, }, err: errors.New(errNotMeta), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := PackageCrossplaneCompatible(tc.args.ver)(tc.args.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nPackageCrossplaneCompatible(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestPackageValidSemver(t *testing.T) { validConstraint := ">v0.13.0" invalidConstraint := ">a0.13.0" type args struct { obj runtime.Object } cases := map[string]struct { reason string args args err error }{ "Valid": { reason: "Should not return error if constraints are valid.", args: args{ obj: &pkgmetav1.Configuration{ Spec: pkgmetav1.ConfigurationSpec{ MetaSpec: pkgmetav1.MetaSpec{ Crossplane: &pkgmetav1.CrossplaneConstraints{ Version: validConstraint, }, }, }, }, }, }, "ErrInvalidConstraints": { reason: "Should return error if constraints are invalid.", args: args{ obj: &pkgmetav1.Configuration{ Spec: pkgmetav1.ConfigurationSpec{ MetaSpec: pkgmetav1.MetaSpec{ Crossplane: &pkgmetav1.CrossplaneConstraints{ Version: invalidConstraint, }, }, }, }, }, err: errors.Wrap(fmt.Errorf("improper constraint: %s", invalidConstraint), errBadConstraints), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := PackageValidSemver(tc.args.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nPackageValidSemver(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsCRD(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1beta1": { reason: "Should not return error if object is a v1beta1 CRD.", obj: v1beta1crd, }, "v1": { reason: "Should not return error if object is a v1 CRD.", obj: v1crd, }, "ErrNotCRD": { reason: "Should return error if object is not CRD.", obj: v1alpha1ConfMeta, err: errors.New(errNotCRD), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsCRD(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsCRD(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsMRD(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1alpha1": { reason: "Should not return error if object is MRD.", obj: v1alpha1MRD, }, "ErrNotMRD": { reason: "Should return error if object is not MRD.", obj: v1beta1crd, err: errors.New(errNotMRD), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsMRD(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsMRD(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsXRD(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1": { reason: "Should not return error if object is XRD.", obj: v1XRD, }, "ErrNotConfiguration": { reason: "Should return error if object is not XRD.", obj: v1beta1crd, err: errors.New(errNotXRD), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsXRD(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsXRD(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsComposition(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1": { reason: "Should not return error if object is composition.", obj: v1Comp, }, "ErrNotComposition": { reason: "Should return error if object is not composition.", obj: v1beta1crd, err: errors.New(errNotComposition), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsComposition(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsComposition(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsActivationPolicy(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1alpha1": { reason: "Should not return error if object is an activation policy.", obj: v1alpha1ActivationPolicy, }, "ErrNotActivationPolicy": { reason: "Should return error if object is not an activation policy.", obj: v1beta1crd, err: errors.New(errNotActivationPolicy), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsActivationPolicy(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsActivationPolicy(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsOperation(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1": { reason: "Should not return error if object is an operation.", obj: v1alpha1Op, }, "ErrNotOperation": { reason: "Should return error if object is not an operation.", obj: v1beta1crd, err: errors.New(errNotOperation), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsOperation(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsOperation(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsCronOperation(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1": { reason: "Should not return error if object is a cron operation.", obj: v1alpha1CronOp, }, "ErrNotCronOperation": { reason: "Should return error if object is not a cron operation.", obj: v1beta1crd, err: errors.New(errNotCronOperation), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsCronOperation(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsCronOperation(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } func TestIsWatchOperation(t *testing.T) { cases := map[string]struct { reason string obj runtime.Object err error }{ "v1": { reason: "Should not return error if object is a watch operation.", obj: v1alpha1WatchOp, }, "ErrNotWatchOperation": { reason: "Should return error if object is not a watch operation.", obj: v1beta1crd, err: errors.New(errNotWatchOperation), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := IsWatchOperation(tc.obj) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nIsWatchOperation(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/xpkg/name.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "os" "path/filepath" "strings" "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/afero" "sigs.k8s.io/yaml" ) const ( // MetaFile is the name of a Crossplane package metadata file. MetaFile string = "crossplane.yaml" // StreamFile is the name of the file in a Crossplane package image that // contains its YAML stream. StreamFile string = "package.yaml" // StreamFileMode determines the permissions on the stream file. StreamFileMode os.FileMode = 0o644 // XpkgExtension is the extension for compiled Crossplane packages. XpkgExtension string = ".xpkg" // XpkgMatchPattern is the match pattern for identifying compiled Crossplane packages. XpkgMatchPattern string = "*" + XpkgExtension // XpkgExamplesFile is the name of the file in a Crossplane package image // that contains the examples YAML stream. XpkgExamplesFile string = ".up/examples.yaml" // AnnotationKey is the key value for xpkg annotations. AnnotationKey string = "io.crossplane.xpkg" // PackageAnnotation is the annotation value used for the package.yaml // layer. PackageAnnotation string = "base" // ExamplesAnnotation is the annotation value used for the examples.yaml // layer. // TODO(lsviben) Consider changing this to "examples". This has been preserved // to not break existing packages. ExamplesAnnotation string = "upbound" ) const ( // identifierDelimeters is the set of valid OCI image identifier delimeter // characters. identifierDelimeters string = ":@" ) func truncate(str string, num int) string { t := str if len(str) > num { t = str[0:num] } return t } // FriendlyID builds a valid DNS label string made up of the name of a package // and its image digest. func FriendlyID(name, hash string) string { return ToDNSLabel(strings.Join([]string{truncate(name, 50), truncate(hash, 12)}, "-")) } // ToDNSLabel converts the string to a valid DNS label. func ToDNSLabel(s string) string { var cut strings.Builder for i := range s { b := s[i] if ('a' <= b && b <= 'z') || ('0' <= b && b <= '9') { cut.WriteByte(b) } if (b == '.' || b == '/' || b == ':' || b == '-') && (i != 0 && i != 62 && i != len(s)-1) { cut.WriteByte('-') } if i == 62 { break } } return strings.Trim(cut.String(), "-") } // BuildPath builds a path with the provided extension. func BuildPath(path, name, ext string) string { full := filepath.Join(path, name) existExt := filepath.Ext(full) return full[0:len(full)-len(existExt)] + ext } // ParseNameFromMeta extracts the package name from its meta file. func ParseNameFromMeta(fs afero.Fs, path string) (string, error) { bs, err := afero.ReadFile(fs, filepath.Clean(path)) if err != nil { return "", err } pkgName, err := parseNameFromPackage(bs) if err != nil { return "", err } return pkgName, nil } // ParsePackageSourceFromReference parses a package source from an OCI image // reference. A source is defined as an OCI image reference with the identifier // (tag or digest) stripped and no other changes to the original reference // source. This is necessary because go-containerregistry will convert docker.io // to index.docker.io for backwards compatibility before pulling an image. We do // not want to do that in cases where we are not pulling an image because it // breaks comparison with dependencies defined in a Configuration manifest. func ParsePackageSourceFromReference(ref name.Reference) string { return strings.TrimRight(strings.TrimSuffix(ref.String(), ref.Identifier()), identifierDelimeters) } type metaPkg struct { Metadata struct { Name string `json:"name"` } } func parseNameFromPackage(bs []byte) (string, error) { p := &metaPkg{} err := yaml.Unmarshal(bs, p) return p.Metadata.Name, err } // ReplaceExt replaces the file extension of the given path. func ReplaceExt(path, ext string) string { old := filepath.Ext(path) return path[0:len(path)-len(old)] + ext } ================================================ FILE: pkg/xpkg/name_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-containerregistry/pkg/name" ) func TestFriendlyID(t *testing.T) { type args struct { pkg string hash string } cases := map[string]struct { reason string args args want string }{ "BothUnderLimit": { reason: "If both package and hash are under limit neither should be truncated.", args: args{ pkg: "provider-aws", hash: "1234567", }, want: "provider-aws-1234567", }, "PackageOverLimit": { reason: "If package is over limit it should be truncated.", args: args{ pkg: "provider-aws-plusabunchofothernonsensethatisgoingtogetslicedoff", hash: "1234567", }, want: "provider-aws-plusabunchofothernonsensethatisgoingt-1234567", }, "HashOverLimit": { reason: "If hash is over limit it should be truncated.", args: args{ pkg: "provider-aws", hash: "1234567891234567", }, want: "provider-aws-123456789123", }, "BothOverLimit": { reason: "If both package and hash are over limit both should be truncated.", args: args{ pkg: "provider-aws-plusabunchofothernonsensethatisgoingtogetslicedoff", hash: "1234567891234567", }, want: "provider-aws-plusabunchofothernonsensethatisgoingt-123456789123", }, "ReplacePeriod": { reason: "All period characters should be replaced with a dash.", args: args{ pkg: "provider.aws-plusabunchofothernonsensethatisgoingtogetslicedoff", hash: "1234.567891234567", }, want: "provider-aws-plusabunchofothernonsensethatisgoingt-1234-5678912", }, "DigestIsName": { reason: "A valid DNS label should be returned when package digest is a name.", args: args{ pkg: "provider-in-cluster", hash: "provider-in-cluster", }, want: "provider-in-cluster-provider-in", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { want := FriendlyID(tc.args.pkg, tc.args.hash) if diff := cmp.Diff(tc.want, want); diff != "" { t.Errorf("\n%s\nFriendlyID(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestToDNSLabel(t *testing.T) { cases := map[string]struct { reason string arg string want string }{ "ReplaceAll": { reason: "All valid symbols should be replaced with dash.", arg: "-hi/my.name/is-", want: "hi-my-name-is", }, "TrimTo63": { reason: "A string longer than 63 valid or replaceable characters should be truncated.", arg: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", want: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, "TrimTo63MinusDashes": { reason: "A string longer than 63 valid or replaceable characters should be truncated with trailing symbol removed.", arg: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa----", want: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { want := ToDNSLabel(tc.arg) if diff := cmp.Diff(tc.want, want); diff != "" { t.Errorf("\n%s\nToDNSLabel(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestSourceFromReference(t *testing.T) { cases := map[string]struct { reason string arg name.Reference want string }{ "SuccessfulTagWithDocker": { reason: "If registry is docker.io it should be reflected in parsed source.", arg: func() name.Reference { ref, _ := name.ParseReference("docker.io/hasheddan/xpkg-test:v0.1.0") return ref }(), want: "docker.io/hasheddan/xpkg-test", }, "SuccessfulTagWithDockerIndex": { reason: "If registry is index.docker.io it should be reflected in parsed source.", arg: func() name.Reference { ref, _ := name.ParseReference("index.docker.io/hasheddan/xpkg-test:v0.1.0") return ref }(), want: "index.docker.io/hasheddan/xpkg-test", }, "SuccessfulTagWithRegistryDefaulting": { reason: "If no registry is supplied, but defaulting is enabled, default registry should not be reflected in parsed source.", arg: func() name.Reference { ref, _ := name.ParseReference("hasheddan/xpkg-test:v0.1.0", name.WithDefaultRegistry("registry.crossplane.io")) return ref }(), want: "hasheddan/xpkg-test", }, "SuccessfulDigestWithRegistryDefaulting": { reason: "If no registry is supplied, but defaulting is enabled, default registry should not be reflected in parsed source.", arg: func() name.Reference { ref, _ := name.ParseReference("hasheddan/xpkg-test@sha256:c88b938d6e7b2ed43d40b71e5a55df9c60fa653bea0c0961f3294fac46d5b56e", name.WithDefaultRegistry("registry.crossplane.io")) return ref }(), want: "hasheddan/xpkg-test", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { want := ParsePackageSourceFromReference(tc.arg) if diff := cmp.Diff(tc.want, want); diff != "" { t.Errorf("\n%s\nParseSourceFromReference(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestBuildPath(t *testing.T) { type args struct { path string name string ext string } cases := map[string]struct { reason string args args want string }{ "NoExtension": { reason: "We should append extension if it does not exist.", args: args{ path: "path/to/somewhere", name: "test", ext: XpkgExtension, }, want: "path/to/somewhere/test.xpkg", }, "ReplaceExtensionName": { reason: "We should replace an extension if one exists in name.", args: args{ path: "path/to/somewhere", name: "test.tar", ext: XpkgExtension, }, want: "path/to/somewhere/test.xpkg", }, "ReplaceExtensionPath": { reason: "We should replace an extension if one exists in path.", args: args{ path: "path/to/somewhere.tar", name: "", ext: XpkgExtension, }, want: "path/to/somewhere.xpkg", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { full := BuildPath(tc.args.path, tc.args.name, tc.args.ext) if diff := cmp.Diff(tc.want, full); diff != "" { t.Errorf("\n%s\nBuildPath(...): -want, +got:\n%s", tc.reason, diff) } }) } } func TestReplaceExt(t *testing.T) { type args struct { path string ext string } cases := map[string]struct { reason string args args want string }{ "ReplaceWithTxt": { reason: "Should replace the existing extension with .txt", args: args{ path: "file.doc", ext: ".txt", }, want: "file.txt", }, "ReplaceWithEmpty": { reason: "Should remove the extension if an empty string is given", args: args{ path: "file.doc", ext: "", }, want: "file", }, "NoExtensionToAdd": { reason: "Should add an extension if there was none before", args: args{ path: "file", ext: ".txt", }, want: "file.txt", }, "MultipleDots": { reason: "Should correctly replace only the last extension", args: args{ path: "archive.tar.gz", ext: ".zip", }, want: "archive.tar.zip", }, "HiddenFile": { reason: "Should correctly replace extension of hidden files", args: args{ path: ".hiddenfile.conf", ext: ".bak", }, want: ".hiddenfile.bak", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := ReplaceExt(tc.args.path, tc.args.ext) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\n%s\nReplaceExt(...): -want, +got:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/xpkg/parser/examples/parser.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package examples contains utilities for parsing examples. package examples import ( "bufio" "context" "io" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/yaml" k8syaml "sigs.k8s.io/yaml" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser" ) // Examples is the set of metadata and objects in a package. type Examples struct { objects []unstructured.Unstructured } // Parser is a Parser implementation for parsing examples. type Parser struct{} // NewExamples creates a new Examples object. func NewExamples() *Examples { return &Examples{} } // New creates a new Package. func New() *Parser { return &Parser{} } // Parse is the underlying logic for parsing examples. func (p *Parser) Parse(_ context.Context, reader io.ReadCloser) (*Examples, error) { ex := NewExamples() if reader == nil { return ex, nil } defer func() { _ = reader.Close() }() yr := yaml.NewYAMLReader(bufio.NewReader(reader)) for { bytes, err := yr.Read() if err != nil && !errors.Is(err, io.EOF) { return ex, annotateErr(err, reader) } if errors.Is(err, io.EOF) { break } if len(bytes) == 0 { continue } var obj unstructured.Unstructured if err := k8syaml.Unmarshal(bytes, &obj); err != nil { return ex, annotateErr(err, reader) } ex.objects = append(ex.objects, obj) } return ex, nil } // annotateErr annotates an error with context if the reader implements // parser.AnnotatedReadCloser. Returns nil if err is nil. func annotateErr(err error, reader io.ReadCloser) error { if err == nil { return nil } if anno, ok := reader.(parser.AnnotatedReadCloser); ok { return errors.Wrapf(err, "%+v", anno.Annotate()) } return err } ================================================ FILE: pkg/xpkg/parser/examples/parser_test.go ================================================ /* Copyright 2026 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package examples import ( "context" "io" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) // mockAnnotatedReadCloser implements parser.AnnotatedReadCloser for testing. type mockAnnotatedReadCloser struct { io.ReadCloser annotation any } func (m *mockAnnotatedReadCloser) Annotate() any { return m.annotation } func TestParse(t *testing.T) { invalidYAML := `apiVersion: v1 kind: ConfigMap metadata: name: test invalid: [broken` validYAML := `apiVersion: v1 kind: ConfigMap metadata: name: test ` type annotation struct { path string position int } type args struct { reader io.ReadCloser annotation any } type want struct { objCount int err error errContains string expectAnyError bool } cases := map[string]struct { reason string args args want want }{ "NilReader": { reason: "Should return empty examples when reader is nil.", args: args{ reader: nil, }, want: want{ objCount: 0, err: nil, }, }, "ValidYAML": { reason: "Should successfully parse valid YAML.", args: args{ reader: io.NopCloser(strings.NewReader(validYAML)), }, want: want{ objCount: 1, err: nil, }, }, "InvalidYAMLNoAnnotation": { reason: "Should return error without annotation when reader is not AnnotatedReadCloser.", args: args{ reader: io.NopCloser(strings.NewReader(invalidYAML)), }, want: want{ objCount: 0, expectAnyError: true, }, }, "InvalidYAMLWithAnnotation": { reason: "Should include annotation in error message when reader is AnnotatedReadCloser.", args: args{ reader: io.NopCloser(strings.NewReader(invalidYAML)), annotation: annotation{path: "/examples/test.yaml", position: 42}, }, want: want{ objCount: 0, expectAnyError: true, errContains: "/examples/test.yaml", }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { p := New() reader := tc.args.reader if tc.args.annotation != nil { reader = &mockAnnotatedReadCloser{ ReadCloser: tc.args.reader, annotation: tc.args.annotation, } } ex, err := p.Parse(context.Background(), reader) if tc.want.expectAnyError { if err == nil { t.Errorf("\n%s\nParse(...): expected error, got nil", tc.reason) return } if tc.want.errContains != "" && !strings.Contains(err.Error(), tc.want.errContains) { t.Errorf("\n%s\nParse(...): expected error to contain %q, got %q", tc.reason, tc.want.errContains, err.Error()) } } if !tc.want.expectAnyError { if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nParse(...): -want err, +got err:\n%s", tc.reason, diff) } } if diff := cmp.Diff(tc.want.objCount, len(ex.objects)); diff != "" { t.Errorf("\n%s\nParse(...): -want objCount, +got objCount:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/xpkg/parser/fsreader.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package parser import ( "errors" "io" "os" "path/filepath" "github.com/spf13/afero" ) var _ AnnotatedReadCloser = &FsReadCloser{} // FsReadCloserAnnotation annotates data for an FsReadCloser. type FsReadCloserAnnotation struct { path string position int } // FsReadCloser implements io.ReadCloser for an Afero filesystem. type FsReadCloser struct { fs afero.Fs dir string paths []string index int position int writeBreak bool wroteBreak bool } // A FilterFn filters files when the FsReadCloser walks the filesystem. // Returning true indicates the file should be skipped. Returning an error will // cause the FsReadCloser to stop walking the filesystem and return. type FilterFn func(path string, info os.FileInfo) (bool, error) // SkipPath skips files at a certain path. func SkipPath(pattern string) FilterFn { return func(path string, _ os.FileInfo) (bool, error) { return filepath.Match(pattern, path) } } // SkipDirs skips directories. func SkipDirs() FilterFn { return func(_ string, info os.FileInfo) (bool, error) { if info.IsDir() { return true, nil } return false, nil } } // SkipEmpty skips empty files. func SkipEmpty() FilterFn { return func(_ string, info os.FileInfo) (bool, error) { return info.Size() == 0, nil } } // SkipNotYAML skips files that do not have YAML extension. func SkipNotYAML() FilterFn { return func(path string, _ os.FileInfo) (bool, error) { if filepath.Ext(path) != ".yaml" && filepath.Ext(path) != ".yml" { return true, nil } return false, nil } } // NewFsReadCloser returns an FsReadCloser that implements io.ReadCloser. It // walks the filesystem ahead of time, then reads file contents when Read is // invoked. It does not follow symbolic links. func NewFsReadCloser(fs afero.Fs, dir string, fns ...FilterFn) (*FsReadCloser, error) { paths := []string{} err := afero.Walk(fs, dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } for _, fn := range fns { filter, err := fn(path, info) if err != nil { return err } if filter { return nil } } paths = append(paths, path) return nil }) return &FsReadCloser{ fs: fs, dir: dir, paths: paths, index: 0, position: 0, writeBreak: false, wroteBreak: false, }, err } func (r *FsReadCloser) Read(p []byte) (n int, err error) { if r.wroteBreak { r.index++ r.position = 0 r.wroteBreak = false n = copy(p, "\n---\n") return n, nil } if r.index == len(r.paths) { return 0, io.EOF } if r.writeBreak { n = copy(p, "\n...\n") r.writeBreak = false r.wroteBreak = true return n, nil } b, err := afero.ReadFile(r.fs, r.paths[r.index]) n = copy(p, b[r.position:]) r.position += n if errors.Is(err, io.EOF) || n == 0 { r.writeBreak = true err = nil } return n, err } // Close is a no op for an FsReadCloser. func (r *FsReadCloser) Close() error { return nil } // Annotate returns additional about the data currently being read. func (r *FsReadCloser) Annotate() any { // Index will be out of bounds if we error after the final file has been // read. index := r.index if index == len(r.paths) { index-- } return FsReadCloserAnnotation{ path: r.paths[index], position: r.position, } } ================================================ FILE: pkg/xpkg/parser/fuzz_test.go ================================================ package parser import ( "bytes" "context" "io" "testing" "k8s.io/apimachinery/pkg/runtime" ) func FuzzParse(f *testing.F) { f.Fuzz(func(_ *testing.T, data []byte) { objScheme := runtime.NewScheme() metaScheme := runtime.NewScheme() p := New(metaScheme, objScheme) r := io.NopCloser(bytes.NewReader(data)) _, _ = p.Parse(context.Background(), r) }) } ================================================ FILE: pkg/xpkg/parser/linter.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package parser import ( "strings" "k8s.io/apimachinery/pkg/runtime" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) const ( errNilLinterFn = "linter function is nil" errOrFmt = "object did not pass any of the linters with following errors: %s" ) // A Linter lints packages. type Linter interface { Lint(l Lintable) error } // PackageLinterFn lints an entire package. If function applies a check for // multiple objects, consider using an ObjectLinterFn. type PackageLinterFn func(Lintable) error // PackageLinterFns is a convenience function to pass multiple PackageLinterFn // to a function that cannot accept variadic arguments. func PackageLinterFns(fns ...PackageLinterFn) []PackageLinterFn { return fns } // ObjectLinterFn lints an object in a package. type ObjectLinterFn func(runtime.Object) error // ObjectLinterFns is a convenience function to pass multiple ObjectLinterFn to // a function that cannot accept variadic arguments. func ObjectLinterFns(fns ...ObjectLinterFn) []ObjectLinterFn { return fns } // PackageLinter lints packages by applying package and object linter functions // to it. type PackageLinter struct { pre []PackageLinterFn perMeta []ObjectLinterFn perObject []ObjectLinterFn } // NewPackageLinter creates a new PackageLinter. func NewPackageLinter(pre []PackageLinterFn, perMeta, perObject []ObjectLinterFn) *PackageLinter { return &PackageLinter{ pre: pre, perMeta: perMeta, perObject: perObject, } } // Lint executes all linter functions against a package. func (l *PackageLinter) Lint(pkg Lintable) error { for _, fn := range l.pre { if err := fn(pkg); err != nil { return err } } for _, o := range pkg.GetMeta() { for _, fn := range l.perMeta { if err := fn(o); err != nil { return err } } } for _, o := range pkg.GetObjects() { for _, fn := range l.perObject { if err := fn(o); err != nil { return err } } } return nil } // Or checks that at least one of the passed linter functions does not return an // error. func Or(linters ...ObjectLinterFn) ObjectLinterFn { return func(o runtime.Object) error { var errs []string for _, l := range linters { if l == nil { return errors.New(errNilLinterFn) } err := l(o) if err == nil { return nil } errs = append(errs, err.Error()) } return errors.Errorf(errOrFmt, strings.Join(errs, ", ")) } } ================================================ FILE: pkg/xpkg/parser/linter_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package parser import ( "testing" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/runtime" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/test" ) var _ Linter = &PackageLinter{} var ( errBoom = errors.New("boom") pkgPass = func(_ Lintable) error { return nil } pkgFail = func(_ Lintable) error { return errBoom } objPass = func(_ runtime.Object) error { return nil } objFail = func(_ runtime.Object) error { return errBoom } ) func TestLinter(t *testing.T) { type args struct { linter Linter pkg *Package } cases := map[string]struct { reason string args args err error }{ "SuccessfulNoOp": { reason: "Passing no checks should always be successful.", args: args{ linter: NewPackageLinter(nil, nil, nil), pkg: NewPackage(), }, }, "SuccessfulNoObjects": { reason: "Passing object linters on empty package should always be successful.", args: args{ linter: NewPackageLinter(nil, ObjectLinterFns(objFail), ObjectLinterFns(objFail)), // Object linters do not run if a package has no objects. pkg: NewPackage(), }, }, "SuccessfulWithChecks": { reason: "Passing checks for a valid package should always be successful.", args: args{ linter: NewPackageLinter(PackageLinterFns(pkgPass), ObjectLinterFns(objPass), ObjectLinterFns(Or(objPass, objFail))), pkg: &Package{ meta: []runtime.Object{deploy}, objects: []runtime.Object{crd}, }, }, }, "ErrorPackageLint": { reason: "Passing package linters for an invalid package should always fail.", args: args{ linter: NewPackageLinter(PackageLinterFns(pkgFail), ObjectLinterFns(objPass), ObjectLinterFns(objPass)), pkg: &Package{ meta: []runtime.Object{deploy}, objects: []runtime.Object{crd}, }, }, err: errBoom, }, "ErrorMetaLint": { reason: "Passing meta linters for a package with invalid meta should always fail.", args: args{ linter: NewPackageLinter(PackageLinterFns(pkgPass), ObjectLinterFns(objFail), ObjectLinterFns(objPass)), pkg: &Package{ meta: []runtime.Object{deploy}, objects: []runtime.Object{crd}, }, }, err: errBoom, }, "ErrorObjectLint": { reason: "Passing object linters for a package with invalid objects should always fail.", args: args{ linter: NewPackageLinter(PackageLinterFns(pkgPass), ObjectLinterFns(objPass), ObjectLinterFns(Or(objFail, objFail))), pkg: &Package{ meta: []runtime.Object{deploy}, objects: []runtime.Object{crd}, }, }, err: errors.Errorf(errOrFmt, errBoom.Error()+", "+errBoom.Error()), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := tc.args.linter.Lint(tc.args.pkg) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nl.Lint(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } var _ ObjectLinterFn = Or(nil, nil) func TestOr(t *testing.T) { type args struct { one ObjectLinterFn two ObjectLinterFn } cases := map[string]struct { reason string args args err error }{ "SuccessfulBothPass": { reason: "Passing two successful linters should never return error.", args: args{ one: objPass, two: objPass, }, }, "SuccessfulOnePass": { reason: "Passing one successful linters should never return error.", args: args{ one: objPass, two: objFail, }, }, "ErrNeitherPass": { reason: "Passing two unsuccessful linters should always return error.", args: args{ one: objFail, two: objFail, }, err: errors.Errorf(errOrFmt, errBoom.Error()+", "+errBoom.Error()), }, "ErrNilLinter": { reason: "Passing a nil linter will should always return error.", args: args{ one: nil, two: objPass, }, err: errors.New(errNilLinterFn), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := Or(tc.args.one, tc.args.two)(crd) if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nOr(...): -want error, +got error:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/xpkg/parser/parser.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package parser implements a parser for Crossplane packages. package parser import ( "bufio" "context" "io" "strings" "unicode" "github.com/spf13/afero" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/kubernetes" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) // Lintable defines the common API for lintable packages. type Lintable interface { // GetMeta returns metadata objects of the lintable package, such as // Provider, Configuration or Function. GetMeta() []runtime.Object // GetObjects returns objects of the lintable package. GetObjects() []runtime.Object } // AnnotatedReadCloser is a wrapper around io.ReadCloser that allows // implementations to supply additional information about data that is read. type AnnotatedReadCloser interface { io.ReadCloser Annotate() any } // ObjectCreaterTyper know how to create and determine the type of objects. type ObjectCreaterTyper interface { runtime.ObjectCreater runtime.ObjectTyper } // Package is the set of metadata and objects in a package. type Package struct { meta []runtime.Object objects []runtime.Object } // NewPackage creates a new Package. func NewPackage() *Package { return &Package{} } // GetMeta gets metadata from the package. func (p *Package) GetMeta() []runtime.Object { return p.meta } // GetObjects gets objects from the package. func (p *Package) GetObjects() []runtime.Object { return p.objects } // Parser is a package parser. type Parser interface { Parse(ctx context.Context, rc io.ReadCloser) (*Package, error) } // PackageParser is a Parser implementation for parsing packages. type PackageParser struct { metaScheme ObjectCreaterTyper objScheme ObjectCreaterTyper } // New returns a new PackageParser. func New(meta, obj ObjectCreaterTyper) *PackageParser { return &PackageParser{ metaScheme: meta, objScheme: obj, } } // Parse is the underlying logic for parsing packages. It first attempts to // decode objects recognized by the meta scheme, then attempts to decode objects // recognized by the object scheme. Objects not recognized by either scheme // return an error rather than being skipped. func (p *PackageParser) Parse(_ context.Context, reader io.ReadCloser) (*Package, error) { pkg := NewPackage() if reader == nil { return pkg, nil } defer func() { _ = reader.Close() }() yr := yaml.NewYAMLReader(bufio.NewReader(reader)) dm := json.NewSerializerWithOptions(json.DefaultMetaFactory, p.metaScheme, p.metaScheme, json.SerializerOptions{Yaml: true}) do := json.NewSerializerWithOptions(json.DefaultMetaFactory, p.objScheme, p.objScheme, json.SerializerOptions{Yaml: true}) for { content, err := yr.Read() if err != nil && !errors.Is(err, io.EOF) { return pkg, err } if errors.Is(err, io.EOF) { break } if isEmptyYAML(content) { continue } m, _, err := dm.Decode(content, nil, nil) if err != nil { // NOTE(hasheddan): we only try to decode with object scheme if the // error is due the object not being registered in the meta scheme. if !runtime.IsNotRegisteredError(err) { return pkg, annotateErr(err, reader) } o, _, err := do.Decode(content, nil, nil) if err != nil { return pkg, annotateErr(err, reader) } pkg.objects = append(pkg.objects, o) continue } pkg.meta = append(pkg.meta, m) } return pkg, nil } // isEmptyYAML checks whether the provided YAML can be considered empty. This // is useful for filtering out empty YAML documents that would otherwise // cause issues when decoded. func isEmptyYAML(y []byte) bool { for line := range strings.SplitSeq(string(y), "\n") { trimmed := strings.TrimLeftFunc(line, unicode.IsSpace) // We don't want to return an empty document with only separators that // have nothing in-between. if trimmed != "" && trimmed != "---" && trimmed != "..." && !strings.HasPrefix(trimmed, "#") { return false } } return true } // annotateErr annotates an error if the reader is an AnnotatedReadCloser. func annotateErr(err error, reader io.ReadCloser) error { if anno, ok := reader.(AnnotatedReadCloser); ok { return errors.Wrapf(err, "%+v", anno.Annotate()) } return err } // BackendOption modifies the parser backend. Backends may accept options at // creation time, but must accept them at initialization. type BackendOption func(Backend) // Backend provides a source for a parser. type Backend interface { Init(ctx context.Context, o ...BackendOption) (io.ReadCloser, error) } // PodLogBackend is a parser backend that uses Kubernetes pod logs as source. type PodLogBackend struct { client kubernetes.Interface name string namespace string } // NewPodLogBackend returns a new PodLogBackend. func NewPodLogBackend(bo ...BackendOption) *PodLogBackend { p := &PodLogBackend{} for _, o := range bo { o(p) } return p } // Init initializes a PodLogBackend. func (p *PodLogBackend) Init(ctx context.Context, bo ...BackendOption) (io.ReadCloser, error) { for _, o := range bo { o(p) } logs := p.client.CoreV1().Pods(p.namespace).GetLogs(p.name, &corev1.PodLogOptions{}) reader, err := logs.Stream(ctx) if err != nil { return nil, err } return reader, nil } // PodName sets the pod name of a PodLogBackend. func PodName(name string) BackendOption { return func(p Backend) { pl, ok := p.(*PodLogBackend) if !ok { return } pl.name = name } } // PodNamespace sets the pod namespace of a PodLogBackend. func PodNamespace(namespace string) BackendOption { return func(p Backend) { pl, ok := p.(*PodLogBackend) if !ok { return } pl.namespace = namespace } } // PodClient sets the pod client of a PodLogBackend. func PodClient(client kubernetes.Interface) BackendOption { return func(p Backend) { pl, ok := p.(*PodLogBackend) if !ok { return } pl.client = client } } // NopBackend is a parser backend with empty source. type NopBackend struct{} // NewNopBackend returns a new NopBackend. func NewNopBackend(...BackendOption) *NopBackend { return &NopBackend{} } // Init initializes a NopBackend. func (p *NopBackend) Init(_ context.Context, _ ...BackendOption) (io.ReadCloser, error) { return nil, nil } // FsBackend is a parser backend that uses a filestystem as source. type FsBackend struct { fs afero.Fs dir string skips []FilterFn } // NewFsBackend returns an FsBackend. func NewFsBackend(fs afero.Fs, bo ...BackendOption) *FsBackend { f := &FsBackend{ fs: fs, } for _, o := range bo { o(f) } return f } // Init initializes an FsBackend. func (p *FsBackend) Init(_ context.Context, bo ...BackendOption) (io.ReadCloser, error) { for _, o := range bo { o(p) } return NewFsReadCloser(p.fs, p.dir, p.skips...) } // FsDir sets the directory of an FsBackend. func FsDir(dir string) BackendOption { return func(p Backend) { f, ok := p.(*FsBackend) if !ok { return } f.dir = dir } } // FsFilters adds FilterFns to an FsBackend. func FsFilters(skips ...FilterFn) BackendOption { return func(p Backend) { f, ok := p.(*FsBackend) if !ok { return } f.skips = skips } } // EchoBackend is a parser backend that uses string input as source. type EchoBackend struct { echo string } // NewEchoBackend returns a new EchoBackend. func NewEchoBackend(echo string) Backend { return &EchoBackend{ echo: echo, } } // Init initializes an EchoBackend. func (p *EchoBackend) Init(_ context.Context, bo ...BackendOption) (io.ReadCloser, error) { for _, o := range bo { o(p) } return io.NopCloser(strings.NewReader(p.echo)), nil } ================================================ FILE: pkg/xpkg/parser/parser_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package parser import ( "bytes" "context" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/afero" appsv1 "k8s.io/api/apps/v1" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/yaml" ) var _ Parser = &PackageParser{} var ( crdBytes = []byte(`apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: test`) whitespaceBytes = []byte(`--- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: test --- --- --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: test`) deployBytes = []byte(`apiVersion: apps/v1 kind: Deployment metadata: name: test annotations: crossplane.io/managed: | #!/bin/bash some script some script `) commentedOutBytes = []byte(`# apiVersion: apps/v1 # kind: Deployment # metadata: # name: test`) manifestWithComments = []byte(` apiVersion: apiextensions.k8s.io/v1beta1 # Some Comment kind: CustomResourceDefinition metadata: name: test`) crd = &apiextensions.CustomResourceDefinition{} _ = yaml.Unmarshal(crdBytes, crd) deploy = &appsv1.Deployment{} _ = yaml.Unmarshal(deployBytes, deploy) ) func TestParser(t *testing.T) { allBytes := bytes.Join([][]byte{crdBytes, deployBytes}, []byte("\n---\n")) fs := afero.NewMemMapFs() _ = afero.WriteFile(fs, "crd.yaml", crdBytes, 0o644) _ = afero.WriteFile(fs, "whitespace.yaml", whitespaceBytes, 0o644) _ = afero.WriteFile(fs, "deployment.yaml", deployBytes, 0o644) _ = afero.WriteFile(fs, "some/nested/dir/crd.yaml", crdBytes, 0o644) _ = afero.WriteFile(fs, ".crossplane/bad.yaml", crdBytes, 0o644) allFs := afero.NewMemMapFs() _ = afero.WriteFile(allFs, "all.yaml", allBytes, 0o644) errFs := afero.NewMemMapFs() _ = afero.WriteFile(errFs, "bad.yaml", []byte("definitely not yaml"), 0o644) emptyFs := afero.NewMemMapFs() _ = afero.WriteFile(emptyFs, "empty.yaml", []byte(""), 0o644) _ = afero.WriteFile(emptyFs, "bad.yam", []byte("definitely not yaml"), 0o644) commentedFs := afero.NewMemMapFs() _ = afero.WriteFile(commentedFs, "commented.yaml", commentedOutBytes, 0o644) _ = afero.WriteFile(commentedFs, ".crossplane/realmanifest.yaml", manifestWithComments, 0o644) objScheme := runtime.NewScheme() _ = apiextensions.AddToScheme(objScheme) metaScheme := runtime.NewScheme() _ = appsv1.AddToScheme(metaScheme) cases := map[string]struct { reason string parser Parser backend Backend pkg *Package wantErr bool }{ "EchoBackendEmpty": { reason: "should have empty output with empty input", parser: New(metaScheme, objScheme), backend: NewEchoBackend(""), pkg: NewPackage(), }, "EchoBackendError": { reason: "should have error with invalid yaml", parser: New(metaScheme, objScheme), backend: NewEchoBackend("definitely not yaml"), pkg: NewPackage(), wantErr: true, }, "EchoBackend": { reason: "should parse input stream successfully", parser: New(metaScheme, objScheme), backend: NewEchoBackend(string(allBytes)), pkg: &Package{ meta: []runtime.Object{deploy}, objects: []runtime.Object{crd}, }, }, "NopBackend": { reason: "should never parse any objects and never return an error", parser: New(metaScheme, objScheme), backend: NewNopBackend(), pkg: NewPackage(), }, "FsBackend": { reason: "should parse filesystem successfully", parser: New(metaScheme, objScheme), backend: NewFsBackend(fs, FsDir("."), FsFilters(SkipDirs(), SkipNotYAML(), SkipPath(".crossplane/*"))), pkg: &Package{ meta: []runtime.Object{deploy}, objects: []runtime.Object{crd, crd, crd, crd}, }, }, "FsBackendCommentedOut": { reason: "should parse filesystem successfully even if all the files are commented out", parser: New(metaScheme, objScheme), backend: NewFsBackend(commentedFs, FsDir("."), FsFilters(SkipDirs(), SkipNotYAML(), SkipPath(".crossplane/*"))), pkg: &Package{ meta: nil, objects: nil, }, }, "FsBackendWithComments": { reason: "should parse filesystem successfully when some of the manifests contain comments", parser: New(metaScheme, objScheme), backend: NewFsBackend(commentedFs, FsDir("."), FsFilters(SkipDirs(), SkipNotYAML())), pkg: &Package{ meta: nil, objects: []runtime.Object{crd}, }, }, "FsBackendAll": { reason: "should parse filesystem successfully with multiple objects in single file", parser: New(metaScheme, objScheme), backend: NewFsBackend(allFs, FsDir("."), FsFilters(SkipDirs(), SkipNotYAML(), SkipPath(".crossplane/*"))), pkg: &Package{ meta: []runtime.Object{deploy}, objects: []runtime.Object{crd}, }, }, "FsBackendError": { reason: "should error if yaml file with invalid yaml", parser: New(metaScheme, objScheme), backend: NewFsBackend(fs, FsDir(".")), pkg: NewPackage(), wantErr: true, }, "FsBackendSkip": { reason: "should skip empty files and files without yaml extension", parser: New(metaScheme, objScheme), backend: NewFsBackend(emptyFs, FsDir("."), FsFilters(SkipDirs(), SkipEmpty(), SkipNotYAML())), pkg: NewPackage(), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r, err := tc.backend.Init(context.TODO()) if err != nil { t.Errorf("backend.Init(...): unexpected error: %s", err) } pkg, err := tc.parser.Parse(context.TODO(), r) if err != nil && !tc.wantErr { t.Errorf("parser.Parse(...): unexpected error: %s", err) } if tc.wantErr { return } if diff := cmp.Diff(tc.pkg.GetObjects(), pkg.GetObjects(), cmpopts.SortSlices(func(i, j runtime.Object) bool { return i.GetObjectKind().GroupVersionKind().String() > j.GetObjectKind().GroupVersionKind().String() })); diff != "" { t.Errorf("Objects: -want, +got:\n%s", diff) } if diff := cmp.Diff(tc.pkg.GetMeta(), pkg.GetMeta(), cmpopts.SortSlices(func(i, j runtime.Object) bool { return i.GetObjectKind().GroupVersionKind().String() > j.GetObjectKind().GroupVersionKind().String() })); diff != "" { t.Errorf("Meta: -want, +got:\n%s", diff) } }) } } func TestCleanYAML(t *testing.T) { type args struct { in []byte } type want struct { out bool } cases := map[string]struct { reason string args args want want }{ "Empty": { reason: "Should return true on empty input", args: args{in: []byte("")}, want: want{out: true}, }, "EmptyLine": { reason: "Should return true on an input with an empty line", args: args{in: []byte("\n")}, want: want{out: true}, }, "WhitespaceOnly": { reason: "Should return true on an input with only whitespaces", args: args{in: []byte(" \n\t ")}, want: want{out: true}, }, "OnlyYAMLSeparators": { reason: "Should return true on an input with only YAML separators", args: args{in: []byte("---\n...")}, want: want{out: true}, }, "YAMLWithWhitespaceLineAndNonEmptyLine": { reason: "Should return false on having whitespace and non empty line in the input", args: args{in: []byte(" \nkey: value")}, want: want{out: false}, }, "CommentedOut": { reason: "Should return true on a fully commented out input", args: args{in: []byte(`# apiVersion: apps/v1 # kind: Deployment # metadata: # name: test`)}, want: want{out: true}, }, "CommentedOutExceptSeparator": { reason: "Should return true on a fully commented out input with a separator not commented", args: args{in: []byte(`--- # apiVersion: apps/v1 # kind: Deployment # metadata: # name: test`)}, want: want{out: true}, }, "NotFullyCommentedOut": { reason: "Should return false on a partially commented out input", args: args{in: []byte(`--- # some comment apiVersion: apps/v1 kind: Deployment metadata: name: test`)}, want: want{out: false}, }, "ShebangAnnotation": { reason: "Should return false with just a shebang annotation", args: args{in: []byte(`--- apiVersion: apps/v1 kind: Deployment metadata: name: test annotations: someScriptWithAShebang: | #!/bin/bash some script`)}, want: want{out: false}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := isEmptyYAML(tc.args.in) if diff := cmp.Diff(tc.want.out, got); diff != "" { t.Errorf("isEmptyYAML: -want, +got:\n%s", diff) } }) } } ================================================ FILE: pkg/xpkg/parser/yaml/parser.go ================================================ /* Copyright 2023 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package yaml contains utilities for reading yaml packages. package yaml import ( "errors" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg" "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser" ) const ( errBuildMetaScheme = "failed to build meta scheme for package parser" errBuildObjectScheme = "failed to build object scheme for package parser" ) // New returns a new PackageParser that targets yaml files. func New() (*parser.PackageParser, error) { metaScheme, err := xpkg.BuildMetaScheme() if err != nil { return nil, errors.New(errBuildMetaScheme) } objScheme, err := xpkg.BuildObjectScheme() if err != nil { return nil, errors.New(errBuildObjectScheme) } return parser.New(metaScheme, objScheme), nil } ================================================ FILE: pkg/xpkg/reader.go ================================================ /* Copyright 2022 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "compress/gzip" "io" ) var _ io.ReadCloser = &gzipReadCloser{} // gzipReadCloser reads compressed contents from a file. type gzipReadCloser struct { rc io.ReadCloser gzip *gzip.Reader } // GzipReadCloser constructs a new gzipReadCloser from the passed file. func GzipReadCloser(rc io.ReadCloser) (io.ReadCloser, error) { r, err := gzip.NewReader(rc) if err != nil { return nil, err } return &gzipReadCloser{ rc: rc, gzip: r, }, nil } // Read calls the underlying gzip reader's Read method. func (g *gzipReadCloser) Read(p []byte) (n int, err error) { return g.gzip.Read(p) } // Close first closes the gzip reader, then closes the underlying closer. func (g *gzipReadCloser) Close() error { if err := g.gzip.Close(); err != nil { _ = g.rc.Close() return err } return g.rc.Close() } var _ io.ReadCloser = &teeReadCloser{} // teeReadCloser is a TeeReader that also closes the underlying writer. type teeReadCloser struct { w io.WriteCloser r io.ReadCloser t io.Reader } // TeeReadCloser constructs a teeReadCloser from the passed reader and writer. func TeeReadCloser(r io.ReadCloser, w io.WriteCloser) io.ReadCloser { return &teeReadCloser{ w: w, r: r, t: io.TeeReader(r, w), } } // Read calls the underlying TeeReader Read method. func (t *teeReadCloser) Read(b []byte) (int, error) { return t.t.Read(b) } // Close closes the underlying ReadCloser, then the Writer for the TeeReader. func (t *teeReadCloser) Close() error { if err := t.r.Close(); err != nil { _ = t.w.Close() return err } return t.w.Close() } var _ io.ReadCloser = &joinedReadCloser{} // joinedReadCloser joins a reader and a closer. It is typically used in the // context of a ReadCloser being wrapped by a Reader. type joinedReadCloser struct { r io.Reader c io.Closer } // JoinedReadCloser constructs a new joinedReadCloser from the passed reader and // closer. func JoinedReadCloser(r io.Reader, c io.Closer) io.ReadCloser { return &joinedReadCloser{ r: r, c: c, } } // Read calls the underlying reader Read method. func (r *joinedReadCloser) Read(b []byte) (int, error) { return r.r.Read(b) } // Close closes the closer for the JoinedReadCloser. func (r *joinedReadCloser) Close() error { return r.c.Close() } ================================================ FILE: pkg/xpkg/scheme.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( v1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1" "github.com/crossplane/crossplane/apis/v2/apiextensions/v1alpha1" v2 "github.com/crossplane/crossplane/apis/v2/apiextensions/v2" opsv1alpha1 "github.com/crossplane/crossplane/apis/v2/ops/v1alpha1" pkgmetav1 "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1" pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1alpha1" pkgmetav1beta1 "github.com/crossplane/crossplane/apis/v2/pkg/meta/v1beta1" admv1 "k8s.io/api/admissionregistration/v1" extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" extv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/conversion" ) // BuildMetaScheme builds the default scheme used for identifying metadata in a // Crossplane package. func BuildMetaScheme() (*runtime.Scheme, error) { metaScheme := runtime.NewScheme() if err := pkgmetav1alpha1.SchemeBuilder.AddToScheme(metaScheme); err != nil { return nil, err } if err := pkgmetav1beta1.SchemeBuilder.AddToScheme(metaScheme); err != nil { return nil, err } if err := pkgmetav1.SchemeBuilder.AddToScheme(metaScheme); err != nil { return nil, err } return metaScheme, nil } // BuildObjectScheme builds the default scheme used for identifying objects in a // Crossplane package. func BuildObjectScheme() (*runtime.Scheme, error) { objScheme := runtime.NewScheme() if err := v1.AddToScheme(objScheme); err != nil { return nil, err } if err := v1alpha1.AddToScheme(objScheme); err != nil { return nil, err } if err := opsv1alpha1.AddToScheme(objScheme); err != nil { return nil, err } if err := v2.AddToScheme(objScheme); err != nil { return nil, err } if err := extv1beta1.AddToScheme(objScheme); err != nil { return nil, err } if err := extv1.AddToScheme(objScheme); err != nil { return nil, err } if err := admv1.AddToScheme(objScheme); err != nil { return nil, err } return objScheme, nil } // TryConvert converts the supplied object to the first supplied candidate that // does not return an error. Returns the converted object and true when // conversion succeeds, or the original object and false if it does not. func TryConvert(obj runtime.Object, candidates ...conversion.Hub) (runtime.Object, bool) { // Note that if we already converted the supplied object to one of the // supplied Hubs in a previous call this will ensure we skip conversion if // and when it's called again. cvt, ok := obj.(conversion.Convertible) if !ok { return obj, false } for _, c := range candidates { if err := cvt.ConvertTo(c); err == nil { return c, true } } return obj, false } // TryConvertToPkg converts the supplied object to a pkgmeta.Pkg, if possible. func TryConvertToPkg(obj runtime.Object, candidates ...conversion.Hub) (pkgmetav1.Pkg, bool) { po, _ := TryConvert(obj, candidates...) m, ok := po.(pkgmetav1.Pkg) return m, ok } ================================================ FILE: pkg/xpkg/scheme_test.go ================================================ /* Copyright 2020 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import ( "testing" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/conversion" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) type mockHub struct{ runtime.Object } func (h mockHub) Hub() {} type mockConvertible struct { conversion.Convertible Fail bool } func (c *mockConvertible) ConvertTo(_ conversion.Hub) error { if c.Fail { return errors.New("nope") } return nil } func TestTryConvert(t *testing.T) { type args struct { meta runtime.Object candidates []conversion.Hub } type want struct { meta runtime.Object ok bool } cases := map[string]struct { reason string args args want want }{ "NotConvertible": { reason: "We should return the object unchanged if we try to convert an object that is not convertible.", args: args{ meta: nil, }, want: want{ meta: nil, ok: false, }, }, "ErrNoConversion": { reason: "We should return false if none of the supplied candidates convert successfully.", args: args{ meta: &mockConvertible{Fail: true}, candidates: []conversion.Hub{&mockHub{}}, }, want: want{ meta: &mockConvertible{Fail: true}, ok: false, }, }, "SuccessfulConversion": { reason: "We should not return true if one of the supplied candidates converted successfully.", args: args{ meta: &mockConvertible{}, candidates: []conversion.Hub{&mockHub{}}, }, want: want{ meta: &mockHub{}, ok: true, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, ok := TryConvert(tc.args.meta, tc.args.candidates...) if diff := cmp.Diff(tc.want.ok, ok); diff != "" { t.Errorf("\n%s\nTryConvert(...): -want ok, +got ok:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.meta, got); diff != "" { t.Errorf("\n%s\nTryConvert(...): -want, +got:\n%s", tc.reason, diff) } }) } } ================================================ FILE: pkg/xpkg/signature/attestation.go ================================================ // // Copyright 2022 The Sigstore Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Note(turkenh): This file is copied from https://github.com/sigstore/cosign/blob/ad478088320a3c04a96b3c183bbde2205fff7bbb/pkg/policy/attestation.go#L59 // with little modification to remove the dependency on "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" // which brings in a lot of dependencies. Keeping the original license above. package signature import ( "context" "encoding/base64" "encoding/json" "fmt" attestationv1 "github.com/in-toto/attestation/go/v1" "github.com/in-toto/in-toto-golang/in_toto" slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" "github.com/sigstore/cosign/v3/pkg/oci" "google.golang.org/protobuf/encoding/protojson" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) const ( predicateCustom = "custom" predicateSLSA = "slsaprovenance" predicateSLSA02 = "slsaprovenance02" predicateSLSA1 = "slsaprovenance1" predicateSPDX = "spdx" predicateSPDXJSON = "spdxjson" predicateCycloneDX = "cyclonedx" predicateLink = "link" predicateVuln = "vuln" predicateOpenVEX = "openvex" ) // AttestationToPayloadJSON takes in a verified Attestation (oci.Signature) and // marshals it into a JSON depending on the payload that's then consumable // by policy engine like cue, rego, etc. // // Anything fed here must have been validated with either // `VerifyLocalImageAttestations` or `VerifyImageAttestations` // // If there's no error, and payload is empty means the predicateType did not // match the attestation. // Returns the attestation type (PredicateType) if the payload was decoded // before the error happened, or in the case the predicateType that was // requested does not match. This is useful for callers to be able to provide // better error messages. For example, if there's a typo in the predicateType, // or the predicateType is not the one they are looking for. Without returning // this, it's hard for users to know which attestations/predicateTypes were // inspected. func attestationToPayloadJSON(_ context.Context, predicateType string, verifiedAttestation oci.Signature) ([]byte, string, error) { //nolint:gocognit // Copied from cosign, see the above note. // PredicateTypeMap is the mapping between the predicate `type` option to predicate URI. PredicateTypeMap := map[string]string{ predicateCustom: attestation.CosignCustomProvenanceV01, predicateSLSA: slsa02.PredicateSLSAProvenance, predicateSLSA02: slsa02.PredicateSLSAProvenance, predicateSLSA1: slsa1.PredicateSLSAProvenance, predicateSPDX: in_toto.PredicateSPDX, predicateSPDXJSON: in_toto.PredicateSPDX, predicateCycloneDX: in_toto.PredicateCycloneDX, predicateLink: in_toto.PredicateLinkV1, predicateVuln: attestation.CosignVulnProvenanceV01, predicateOpenVEX: attestation.OpenVexNamespace, } if predicateType == "" { return nil, "", errors.New("missing predicate type") } predicateURI, ok := PredicateTypeMap[predicateType] if !ok { // Not a custom one, use it as is. predicateURI = predicateType } var payloadData map[string]any p, err := verifiedAttestation.Payload() if err != nil { return nil, "", fmt.Errorf("getting payload: %w", err) } err = json.Unmarshal(p, &payloadData) if err != nil { return nil, "", fmt.Errorf("unmarshaling payload data") } var decodedPayload []byte if val, ok := payloadData["payload"]; ok { decodedPayload, err = base64.StdEncoding.DecodeString(val.(string)) //nolint:forcetypeassert // TODO(negz): Will this always be a string? if err != nil { return nil, "", fmt.Errorf("decoding payload: %w", err) } } else { return nil, "", fmt.Errorf("could not find payload in payload data") } // Only apply the policy against the requested predicate type var statement attestationv1.Statement if err := protojson.Unmarshal(decodedPayload, &statement); err != nil { return nil, "", fmt.Errorf("unmarshal in-toto statement: %w", err) } if statement.GetPredicateType() != predicateURI { // This is not the predicate we're looking for, so skip it. return nil, statement.GetPredicateType(), nil } // NB: In many (all?) of these cases, we could just return the // 'json.Marshal', but we check for errors here to decorate them // with more meaningful error message. var payload []byte switch predicateType { case predicateCustom: payload, err = protojson.Marshal(&statement) if err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("generating CosignStatement: %w", err) } case predicateLink: var linkStatement in_toto.LinkStatement if err := json.Unmarshal(decodedPayload, &linkStatement); err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("unmarshaling LinkStatement: %w", err) } payload, err = json.Marshal(linkStatement) if err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("marshaling LinkStatement: %w", err) } case predicateSLSA: var slsaProvenanceStatement in_toto.ProvenanceStatementSLSA02 if err := json.Unmarshal(decodedPayload, &slsaProvenanceStatement); err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("unmarshaling ProvenanceStatementSLSA02): %w", err) } payload, err = json.Marshal(slsaProvenanceStatement) if err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("marshaling ProvenanceStatementSLSA02: %w", err) } case predicateSPDX, predicateSPDXJSON: var spdxStatement in_toto.SPDXStatement if err := json.Unmarshal(decodedPayload, &spdxStatement); err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("unmarshaling SPDXStatement: %w", err) } payload, err = json.Marshal(spdxStatement) if err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("marshaling SPDXStatement: %w", err) } case predicateCycloneDX: var cyclonedxStatement in_toto.CycloneDXStatement if err := json.Unmarshal(decodedPayload, &cyclonedxStatement); err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("unmarshaling CycloneDXStatement: %w", err) } payload, err = json.Marshal(cyclonedxStatement) if err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("marshaling CycloneDXStatement: %w", err) } case predicateVuln: var vulnStatement attestation.CosignVulnStatement if err := json.Unmarshal(decodedPayload, &vulnStatement); err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("unmarshaling CosignVulnStatement: %w", err) } payload, err = json.Marshal(vulnStatement) if err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("marshaling CosignVulnStatement: %w", err) } default: // Valid URI type reaches here. payload, err = protojson.Marshal(&statement) if err != nil { return nil, statement.GetPredicateType(), fmt.Errorf("generating Statement: %w", err) } } return payload, statement.GetPredicateType(), nil } ================================================ FILE: pkg/xpkg/signature/doc.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package signature implements image signature verification for Crossplane packages. package signature ================================================ FILE: pkg/xpkg/signature/validate.go ================================================ package signature import ( "context" "crypto" "fmt" "strings" "time" "github.com/crossplane/crossplane/apis/v2/pkg/v1beta1" "github.com/google/go-containerregistry/pkg/authn/k8schain" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/oci" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/fulcioroots" "github.com/sigstore/sigstore/pkg/signature" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) const fetchCertTimeout = 30 * time.Second // Validator validates image signatures. type Validator interface { Validate(ctx context.Context, ref name.Reference, config *v1beta1.ImageVerification, pullSecrets ...string) error } // NopValidator is a Validator that always succeeds. type NopValidator struct{} // Validate always returns nil, skipping signature verification. func (NopValidator) Validate(context.Context, name.Reference, *v1beta1.ImageVerification, ...string) error { return nil } // NewCosignValidator returns a new CosignValidator. func NewCosignValidator(c client.Reader, k kubernetes.Interface, namespace, serviceAccount string) (*CosignValidator, error) { ctx, cancel := context.WithTimeout(context.Background(), fetchCertTimeout) defer cancel() var err error opts := cosign.CheckOpts{} opts.RootCerts, err = fulcioroots.Get() if err != nil { return nil, errors.Errorf("cannot fetch Fulcio roots: %w", err) } opts.IntermediateCerts, err = fulcioroots.GetIntermediates() if err != nil { return nil, fmt.Errorf("cannot fetch Fulcio intermediates: %w", err) } opts.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) if err != nil { return nil, fmt.Errorf("cannot fetch CTLog public keys: %w", err) } opts.RekorPubKeys, err = cosign.GetRekorPubs(ctx) if err != nil { return nil, fmt.Errorf("cannot fetch Rekor public keys: %w", err) } return &CosignValidator{ client: c, clientset: k, namespace: namespace, serviceAccount: serviceAccount, baseCheckOpts: opts, }, nil } // CosignValidator validates image signatures using cosign. type CosignValidator struct { client client.Reader clientset kubernetes.Interface namespace string serviceAccount string baseCheckOpts cosign.CheckOpts } // Validate validates the image signature. func (c *CosignValidator) Validate(ctx context.Context, ref name.Reference, config *v1beta1.ImageVerification, pullSecrets ...string) error { if config.Provider != v1beta1.ImageVerificationProviderCosign { return errors.New("unsupported image verification provider") } auth, err := k8schain.New(ctx, c.clientset, k8schain.Options{ Namespace: c.namespace, ServiceAccountName: c.serviceAccount, ImagePullSecrets: pullSecrets, }) if err != nil { return errors.Wrap(err, "cannot create k8s auth chain") } var errs []error for _, a := range config.Cosign.Authorities { co, err := c.buildCosignCheckOpts(ctx, a, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(auth))) if err != nil { errs = append(errs, errors.Errorf("authority %q: cannot build cosign check options %v", a.Name, err)) continue } var res []oci.Signature var ok bool co.ClaimVerifier = cosign.SimpleClaimVerifier if len(a.Attestations) > 0 { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier res, ok, err = cosign.VerifyImageAttestations(ctx, ref, co) } else { res, ok, err = cosign.VerifyImageSignatures(ctx, ref, co) } if err != nil { errs = append(errs, errors.Errorf("authority %q: signature verification failed with %v", a.Name, err)) continue } if !ok { errs = append(errs, errors.Errorf("authority %q: signature verification failed", a.Name)) continue } // If there are no attestations, return success given that the signature // verification was successful for this authority. if len(a.Attestations) == 0 { return nil } // If there are attestations to be verified, check if the attestation // is valid for at least one of the resulting/checked // signatures/attestations. for _, att := range a.Attestations { for _, s := range res { b, _, err := attestationToPayloadJSON(ctx, att.PredicateType, s) if err != nil { errs = append(errs, errors.Errorf("authority %q: cannot convert attestation %q to payload JSON: %v", a.Name, att.Name, err)) continue } if len(b) == 0 { errs = append(errs, errors.Errorf("authority %q: no attestation of type %q found for %q", a.Name, att.PredicateType, att.Name)) continue } // If the attestation is valid for at least one of the resulting // payloads, return nil. Otherwise, continue with the next // signature. return nil } } } // If we reach this point, none of the authorities were able to verify the // image signature or attestations. So, return an error with all the errors // encountered. return errors.Join(errs...) } func (c *CosignValidator) buildCosignCheckOpts(ctx context.Context, a v1beta1.CosignAuthority, remoteOpts ...ociremote.Option) (*cosign.CheckOpts, error) { opts := c.baseCheckOpts opts.RegistryClientOpts = remoteOpts if kl := a.Keyless; kl != nil { for _, id := range kl.Identities { opts.Identities = append(opts.Identities, cosign.Identity{ Issuer: id.Issuer, Subject: id.Subject, IssuerRegExp: id.IssuerRegExp, SubjectRegExp: id.SubjectRegExp, }) } if kl.InsecureIgnoreSCT != nil { opts.IgnoreSCT = *kl.InsecureIgnoreSCT } } if kr := a.Key; kr != nil { s := &corev1.Secret{} if err := c.client.Get(ctx, types.NamespacedName{Name: kr.SecretRef.Name, Namespace: c.namespace}, s); err != nil { return nil, errors.Wrap(err, "cannot get secret") } v := s.Data[kr.SecretRef.Key] if len(v) == 0 { return nil, errors.Errorf("no data found for key %q in secret %q", kr.SecretRef.Key, kr.SecretRef.Name) } publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(v) if err != nil || publicKey == nil { return nil, errors.Errorf("secret %q contains an invalid public key: %w", kr.SecretRef.Key, err) } ha, err := hashAlgorithm(a.Key.HashAlgorithm) if err != nil { return nil, errors.Wrap(err, "invalid hash algorithm") } opts.SigVerifier, err = signature.LoadVerifier(publicKey, ha) if err != nil { return nil, errors.Wrap(err, "cannot load signature verifier") } } return &opts, nil } func hashAlgorithm(algorithm string) (crypto.Hash, error) { switch strings.ToLower(strings.TrimSpace(algorithm)) { case "sha224": return crypto.SHA224, nil case "sha256": return crypto.SHA256, nil case "sha384": return crypto.SHA384, nil case "sha512": return crypto.SHA512, nil default: return 0, errors.Errorf("unsupported algorithm %q", algorithm) } } ================================================ FILE: pkg/xpkg/testdata/examples/ec2/instance.yaml ================================================ # Copyright 2023 the Crossplane authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. apiVersion: ec2.aws.crossplane.io/v1alpha2 kind: Instance metadata: name: sample-instance spec: forProvider: region: us-west-1 ami: ami-07b068f843ec78e72 instanceType: t2.micro networkInterface: - deviceIndex: 0 networkInterfaceIdRef: name: sample-ni creditSpecification: - cpuCredits: unlimited ================================================ FILE: pkg/xpkg/testdata/examples/ec2/internetgateway.yaml ================================================ # Copyright 2023 the Crossplane authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. apiVersion: ec2.aws.crossplane.io/v1alpha2 kind: InternetGateway metadata: name: example spec: forProvider: region: us-west-1 tags: Name: main vpcIdRef: name: example providerConfigRef: name: example ================================================ FILE: pkg/xpkg/testdata/examples/ecr/repository.yaml ================================================ # Copyright 2023 the Crossplane authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. apiVersion: ecr.aws.crossplane.io/v1alpha2 kind: Repository metadata: name: sample-repository spec: forProvider: region: us-east-1 imageScanningConfiguration: - scanOnPush: true imageTagMutability: "IMMUTABLE" tags: key1: value1 ================================================ FILE: pkg/xpkg/testdata/examples/provider.yaml ================================================ # Copyright 2023 the Crossplane authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. --- apiVersion: pkg.crossplane.io/v1 kind: Provider metadata: name: provider-aws spec: package: crossplane/provider-aws:main ================================================ FILE: pkg/xpkg/testdata/provider_meta.yaml ================================================ apiVersion: meta.pkg.crossplane.io/v1alpha1 kind: Provider metadata: annotations: company: Crossplane description: | The Amazon Web Services (AWS) Crossplane provider adds support for managing AWS resources in Kubernetes. descriptionShort: | The AWS Crossplane provider enables infrastructure management for Amazon Web Services. friendly-group-name.meta.crossplane.io/acm.aws.crossplane.io: Certificate Manager friendly-group-name.meta.crossplane.io/acmpca.aws.crossplane.io: Private CA friendly-group-name.meta.crossplane.io/apigatewayv2.aws.crossplane.io: API Gateway friendly-group-name.meta.crossplane.io/cache.aws.crossplane.io: ElastiCache friendly-group-name.meta.crossplane.io/database.aws.crossplane.io: Databases friendly-group-name.meta.crossplane.io/dynamodb.aws.crossplane.io: DynamoDB friendly-group-name.meta.crossplane.io/ec2.aws.crossplane.io: Elastic Compute friendly-group-name.meta.crossplane.io/ecr.aws.crossplane.io: Elastic Container Registry friendly-group-name.meta.crossplane.io/efs.aws.crossplane.io: Elastic Filesystem friendly-group-name.meta.crossplane.io/eks.aws.crossplane.io: Elastic Kubernetes friendly-group-name.meta.crossplane.io/elasticloadbalancing.aws.crossplane.io: Elastic Load Balancing friendly-group-name.meta.crossplane.io/identity.aws.crossplane.io: IAM friendly-group-name.meta.crossplane.io/kms.aws.crossplane.io: Key Managment Service friendly-group-name.meta.crossplane.io/notification.aws.crossplane.io: SNS friendly-group-name.meta.crossplane.io/rds.aws.crossplane.io: RDS friendly-group-name.meta.crossplane.io/redshift.aws.crossplane.io: Redshift friendly-group-name.meta.crossplane.io/route53.aws.crossplane.io: Route 53 friendly-group-name.meta.crossplane.io/s3.aws.crossplane.io: S3 friendly-group-name.meta.crossplane.io/secretsmanager.aws.crossplane.io: Secrets Manager friendly-group-name.meta.crossplane.io/sfn.aws.crossplane.io: Step Functions friendly-group-name.meta.crossplane.io/sqs.aws.crossplane.io: SQS friendly-kind-name.meta.crossplane.io/activity.sfn.aws.crossplane.io: Activity friendly-kind-name.meta.crossplane.io/address.ec2.aws.crossplane.io: Address friendly-kind-name.meta.crossplane.io/api.apigatewayv2.aws.crossplane.io: API friendly-kind-name.meta.crossplane.io/apimapping.apigatewayv2.aws.crossplane.io: API Mapping friendly-kind-name.meta.crossplane.io/authorizer.apigatewayv2.aws.crossplane.io: Authorizer friendly-kind-name.meta.crossplane.io/backup.dynamodb.aws.crossplane.io: Backup friendly-kind-name.meta.crossplane.io/bucket.s3.aws.crossplane.io: Bucket friendly-kind-name.meta.crossplane.io/bucketpolicy.s3.aws.crossplane.io: Bucket Policy friendly-kind-name.meta.crossplane.io/cachecluster.cache.aws.crossplane.io: Cache Cluster friendly-kind-name.meta.crossplane.io/cachesubnetgroup.cache.aws.crossplane.io: Cache Subnet Group friendly-kind-name.meta.crossplane.io/certificate.acm.aws.crossplane.io: Certificate friendly-kind-name.meta.crossplane.io/certificateauthority.acmpca.aws.crossplane.io: CA friendly-kind-name.meta.crossplane.io/certificateauthoritypermission.acmpca.aws.crossplane.io: CA Permission friendly-kind-name.meta.crossplane.io/cluster.eks.aws.crossplane.io: EKS Cluster friendly-kind-name.meta.crossplane.io/cluster.redshift.aws.crossplane.io: Redshift Cluster friendly-kind-name.meta.crossplane.io/dbcluster.rds.aws.crossplane.io: Database Cluster friendly-kind-name.meta.crossplane.io/dbparametergroup.rds.aws.crossplane.io: Database Parameter Group friendly-kind-name.meta.crossplane.io/dbsubnetgroup.database.aws.crossplane.io: Database Subnet Group friendly-kind-name.meta.crossplane.io/deployment.apigatewayv2.aws.crossplane.io: Deployment friendly-kind-name.meta.crossplane.io/domainname.apigatewayv2.aws.crossplane.io: Domain Name friendly-kind-name.meta.crossplane.io/elb.elasticloadbalancing.aws.crossplane.io: Elastic Load Balancer friendly-kind-name.meta.crossplane.io/elbattachment.elasticloadbalancing.aws.crossplane.io: ELB Attachment friendly-kind-name.meta.crossplane.io/fargateprofile.eks.aws.crossplane.io: Fargate Profile friendly-kind-name.meta.crossplane.io/filesystem.efs.aws.crossplane.io: Filesystem friendly-kind-name.meta.crossplane.io/globaltable.dynamodb.aws.crossplane.io: Global Table friendly-kind-name.meta.crossplane.io/hostedzone.route53.aws.crossplane.io: Hosted Zone friendly-kind-name.meta.crossplane.io/iamaccesskey.identity.aws.crossplane.io: IAM Access Key friendly-kind-name.meta.crossplane.io/iamgroup.identity.aws.crossplane.io: IAM Group friendly-kind-name.meta.crossplane.io/iamgrouppolicyattachment.identity.aws.crossplane.io: IAM Group Policy Attachment friendly-kind-name.meta.crossplane.io/iamgroupusermembership.identity.aws.crossplane.io: IAM Group User Membership friendly-kind-name.meta.crossplane.io/iampolicy.identity.aws.crossplane.io: IAM Policy friendly-kind-name.meta.crossplane.io/iamrole.identity.aws.crossplane.io: IAM Role friendly-kind-name.meta.crossplane.io/iamrolepolicyattachment.identity.aws.crossplane.io: IAM Role Policy Attachment friendly-kind-name.meta.crossplane.io/iamuser.identity.aws.crossplane.io: IAM User friendly-kind-name.meta.crossplane.io/iamuserpolicyattachment.identity.aws.crossplane.io: IAM User Policy Attachment friendly-kind-name.meta.crossplane.io/integration.apigatewayv2.aws.crossplane.io: Integration friendly-kind-name.meta.crossplane.io/integrationresponse.apigatewayv2.aws.crossplane.io: Integration Response friendly-kind-name.meta.crossplane.io/internetgateway.ec2.aws.crossplane.io: Internet Gateway friendly-kind-name.meta.crossplane.io/key.kms.aws.crossplane.io: Key friendly-kind-name.meta.crossplane.io/model.apigatewayv2.aws.crossplane.io: Model friendly-kind-name.meta.crossplane.io/natgateway.ec2.aws.crossplane.io: NAT Gateway friendly-kind-name.meta.crossplane.io/nodegroup.eks.aws.crossplane.io: EKS Node Group friendly-kind-name.meta.crossplane.io/queue.sqs.aws.crossplane.io: SQS Queue friendly-kind-name.meta.crossplane.io/rdsinstance.database.aws.crossplane.io: RDS Instance friendly-kind-name.meta.crossplane.io/replicationgroup.cache.aws.crossplane.io: Replication Group friendly-kind-name.meta.crossplane.io/repository.ecr.aws.crossplane.io: Repository friendly-kind-name.meta.crossplane.io/repositorypolicy.ecr.aws.crossplane.io: Repository Policy friendly-kind-name.meta.crossplane.io/resourcerecordset.route53.aws.crossplane.io: Resource Record Set friendly-kind-name.meta.crossplane.io/route.apigatewayv2.aws.crossplane.io: Route friendly-kind-name.meta.crossplane.io/routeresponse.apigatewayv2.aws.crossplane.io: Route Response friendly-kind-name.meta.crossplane.io/routetable.ec2.aws.crossplane.io: Route Table friendly-kind-name.meta.crossplane.io/secret.secretsmanager.aws.crossplane.io: Secret friendly-kind-name.meta.crossplane.io/securitygroup.ec2.aws.crossplane.io: Security Group friendly-kind-name.meta.crossplane.io/snssubscription.notification.aws.crossplane.io: Subscription friendly-kind-name.meta.crossplane.io/snstopic.notification.aws.crossplane.io: Topic friendly-kind-name.meta.crossplane.io/stage.apigatewayv2.aws.crossplane.io: Stage friendly-kind-name.meta.crossplane.io/statemachine.sfn.aws.crossplane.io: State Machine friendly-kind-name.meta.crossplane.io/subnet.ec2.aws.crossplane.io: Subnet friendly-kind-name.meta.crossplane.io/table.dynamodb.aws.crossplane.io: Table friendly-kind-name.meta.crossplane.io/vpc.ec2.aws.crossplane.io: VPC friendly-kind-name.meta.crossplane.io/vpccidrblock.ec2.aws.crossplane.io: VPC CIDR Block friendly-kind-name.meta.crossplane.io/vpclink.apigatewayv2.aws.crossplane.io: VPC Link friendly-name.meta.crossplane.io: Provider AWS iconData: CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNjUiIGhlaWdodD0iNjUiPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiB4PSIuNSIgeT0iLjUiIGZpbGw9IiNGQUZBRkEiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjRDhEOERBIiByeD0iMTYiLz48cGF0aCBmaWxsPSIjMjUyRjNFIiBkPSJNMjMuMjQ2MzY1NiAyOS45Njg5OTdjMCAuNTY0MTg0NC4wNTY0MjQ2IDEuMDIxNjMxMi4xNTUxNjc2IDEuMzU3MDkyMi4xMTI4NDkyLjMzNTQ2MS4yNTM5MTA3LjcwMTQxODQuNDUxMzk2OCAxLjA5Nzg3MjMuMDcwNTMwOC4xMjE5ODU4LjA5ODc0MzEuMjQzOTcxNi4wOTg3NDMxLjM1MDcwOTIgMCAuMTUyNDgyMy0uMDg0NjM2OS4zMDQ5NjQ2LS4yNjgwMTY5LjQ1NzQ0NjhsLS44ODg2ODc1LjY0MDQyNTZjLS4xMjY5NTUzLjA5MTQ4OTMtLjI1NDA1MTcuMTM3MjM0LS4zNjY3NTk5LjEzNzIzNC0uMTQxMDYxNSAwLS4yODIxMjMtLjA3NjI0MTEtLjQyMzE4NDUtLjIxMzQ3NTItLjE5NzQ4NjEtLjIyODcyMzQtLjM2Njc1OTktLjQ3MjY5NS0uNTA3ODIxNC0uNzE2NjY2Ni0uMTQxMDYxNS0uMjU5MjE5OS0uMjgyMTIzLS41NDg5MzYyLS40MzcyOTA2LS44OTk2NDU0LTEuMTAwNDIwNzggMS40MDI4MzY4LTIuNDgyNjgyNDIgMi4xMDQyNTUzLTQuMTQ3MjA4MTMgMi4xMDQyNTUzLTEuMTg0OTE2NjEgMC0yLjEzMDAyODY2LS4zNjU5NTc1LTIuODIxMjMwMDItMS4wOTc4NzI0LS42OTEyMDEzNS0uNzMxOTE0OC0xLjA0Mzg1NTEtMS43MDc4MDE0LTEuMDQzODU1MS0yLjkyNzY1OTUgMC0xLjI5NjA5OTMyLjQyMzA0MzQ0LTIuMzQ4MjI2OTggMS4yODM2NTk2NS0zLjE0MTEzNDc4Ljg2MDQ3NTE2LS43OTI5MDc4IDIuMDAzMDczMzEtMS4xODkzNjE3IDMuNDU2MDA2NzctMS4xODkzNjE3LjQ3OTQ2ODA0IDAgLjk3MzMyNDM2LjA0NTc0NDY4IDEuNDk1MjUxOTEuMTIxOTg1ODEuNTIxOTI3NTUuMDc2MjQxMTQgMS4wNTc5NjEyNi4xOTgyMjY5NSAxLjYyMjIwNzI2LjMzNTQ2MXYtMS4xMTMxMjA1N2MwLTEuMTU4ODY1MjUtLjIyNTY5ODQtMS45NjcwMjEyOC0uNjYyOTg5MDUtMi40Mzk3MTYzMS0uNDUxMzk2ODEtLjQ3MjY5NTA0LTEuMjEzMTI4OTEtLjcwMTQxODQ0LTIuMjk5MzAyNDctLjcwMTQxODQ0LS40OTM3MTUyNSAwLTEuMDAxNTM2NjUuMDYwOTkyOTEtMS41MjM0NjQyLjE5ODIyNjk1LS41MjE5Mjc1Ni4xMzcyMzQwNC0xLjAyOTc0ODk2LjMwNDk2NDU0LTEuNTIzNDY0MjEuNTE4NDM5NzEtLjIyNTY5ODQuMTA2NzM3NTktLjM5NDk3MjIuMTY3NzMwNS0uNDkzNzE1MjYuMTk4MjI2OTYtLjA5ODc0MzA1LjAzMDQ5NjQ1LS4xNjkyNzM4LjA0NTc0NDY4LS4yMjU4Mzk0Ni4wNDU3NDQ2OC0uMTk3MzQ1MDQgMC0uMjk2MDg4MDktLjE1MjQ4MjI3LS4yOTYwODgwOS0uNDcyNjk1MDR2LS43NDcxNjMxMmMwLS4yNDM5NzE2My4wMjgyMTIzLS40MjY5NTAzNS4wOTg3NDMwNS0uNTMzNjg3OTRzLjE5NzM0NTA0LS4yMTM0NzUxOC4zOTQ5NzIyLS4zMjAyMTI3N2MuNDkzNTc0Mi0uMjc0NDY4MDggMS4wODYxNzM1Ni0uNTAzMTkxNDkgMS43NzczNzQ5MS0uNjg2MTcwMjEuNjkxMjAxMzYtLjE5ODIyNjk1IDEuNDI0NzIxMTYtLjI4OTcxNjMxIDIuMjAwNTU5NDItLjI4OTcxNjMxIDEuNjc4NjMxODUgMCAyLjkwNTg2NjkyLjQxMTcwMjEyIDMuNjk1ODExMzIgMS4yMzUxMDYzOC43NzU4MzgyLjgyMzQwNDI1IDEuMTcwODEwNSAyLjA3Mzc1ODg2IDEuMTcwODEwNSAzLjc1MTA2MzgzVjI5Ljk2ODk5N2guMDI4MjEyM3ptLTUuNzI3MDk2OTggMi4zMTc3MzA1Yy40NjU1MDI5NSAwIC45NDQ5NzA5OS0uMDkxNDg5NCAxLjQ1MjkzMzQ2LS4yNzQ0NjgxLjUwNzgyMTQtLjE4Mjk3ODguOTU5MjE4Mi0uNTE4NDM5NyAxLjM0MDA4NDI2LS45NzU4ODY2LjIyNTY5ODQtLjI4OTcxNjMuMzk0ODMxMTQtLjYwOTkyOS40Nzk2MDkxLS45NzU4ODY1LjA4NDYzNjktLjM2NTk1NzQuMTQxMDYxNS0uODA4MTU2MDEuMTQxMDYxNS0xLjMyNjU5NTcydi0uNjQwNDI1NTNjLS40MDkwNzgzNS0uMTA2NzM3NTktLjg0NjM2OTAxLS4xOTgyMjY5NS0xLjI5Nzc2NTgxLS4yNTkyMTk4Ni0uNDUxMzk2OC0uMDYwOTkyOTEtLjg4ODY4NzQ1LS4wOTE0ODkzNi0xLjMyNTk3ODEtLjA5MTQ4OTM2LS45NDUxMTIwNiAwLTEuNjM2MzEzNDEuMTk4MjI2OTUtMi4xMDE4MTYzNy42MDk5MjkwNy0uNDY1NTAyOTUuNDExNzAyMTMtLjY5MTIwMTM1Ljk5MTEzNDc2LS42OTEyMDEzNSAxLjc1MzU0NjEgMCAuNzE2NjY2Ny4xNjkyNzM4IDEuMjUwMzU0Ni41MjE5Mjc1NSAxLjYxNjMxMjEuMzM4NTQ3NjEuMzgxMjA1Ni44MzIyNjI4Ni41NjQxODQ0IDEuNDgxMTQ1NzYuNTY0MTg0NHpNMjguODQ2NTA3MSAzMy45MzM1MzZjLS4yNTM5MTA3IDAtLjQyMzE4NDUtLjA0NTc0NDctLjUzNjAzMzctLjE1MjQ4MjMtLjExMjg0OTItLjA5MTQ4OTQtLjIxMTU5MjItLjMwNDk2NDUtLjI5NjIyOTEtLjU5NDY4MDlMMjQuNjk5Mjk5IDIxLjM5OTQ5MzQxYy0uMDg0Nzc4LS4zMDQ5NjQ1My0uMTI2OTU1My0uNTAzMTkxNDgtLjEyNjk1NTMtLjYwOTkyOTA3IDAtLjI0Mzk3MTYzLjExMjcwODEtLjM4MTIwNTY4LjMzODU0NzYtLjM4MTIwNTY4aDEuMzgyNDAyN2MuMjY3ODc1OCAwIC40NTEzOTY4LjA0NTc0NDY4LjU1MDEzOTguMTUyNDgyMjcuMTEyODQ5Mi4wOTE0ODkzNi4xOTc0ODYxLjMwNDk2NDU0LjI4MjEyMy41OTQ2ODA4NUwyOS40OTUzOSAzMS4yNDk4NDhsMi4yMDA0MTg0LTEwLjA5NDMyNjIyYy4wNzA2NzE4LS4zMDQ5NjQ1NC4xNTUxNjc2LS41MDMxOTE0OS4yNjgxNTc5LS41OTQ2ODA4NS4xMTI4NDkyLS4wOTE0ODkzNi4zMTAxOTQyLS4xNTI0ODIyNy41NjQyNDYtLjE1MjQ4MjI3aDEuMTI4NDkyYy4yNjc4NzU4IDAgLjQ1MTM5NjguMDQ1NzQ0NjguNTY0MjQ2LjE1MjQ4MjI3LjExMjg0OTIuMDkxNDg5MzYuMjExNTkyMy4zMDQ5NjQ1NC4yNjgwMTY5LjU5NDY4MDg1bDIuMjI4NjMwNiAxMC4yMTYzMTIwMiAyLjQ0MDUwNS0xMC4yMTYzMTIwMmMuMDg0NjM2OS0uMzA0OTY0NTQuMTgzMzgtLjUwMzE5MTQ5LjI4MjEyMy0uNTk0NjgwODUuMTEyODQ5Mi0uMDkxNDg5MzYuMjk2MjI5Mi0uMTUyNDgyMjcuNTUwMTM5OS0uMTUyNDgyMjdoMS4zMTE4NzE5Yy4yMjU2OTg0IDAgLjM1MjY1MzguMTIxOTg1ODIuMzUyNjUzOC4zODEyMDU2OCAwIC4wNzYyNDExMy0uMDE0MTA2Mi4xNTI0ODIyNy0uMDI4MjEyMy4yNDM5NzE2My0uMDE0MjQ3Mi4wOTE0ODkzNi0uMDQyMzE4NS4yMTM0NzUxNy0uMDk4NzQzMS4zODEyMDU2N0wzOC4xMjgzNTM5IDMzLjIwMTYyMTFjLS4wODQ3NzguMzA0OTY0NS0uMTgzNTIxLjUwMzE5MTUtLjI5NjM3MDIuNTk0NjgwOC0uMTEyNzA4Mi4wOTE0ODk0LS4yOTYwODgxLjE1MjQ4MjMtLjUzNTg5MjcuMTUyNDgyM2gtMS4yMTMyNjk5Yy0uMjY3ODc1OCAwLS40NTEyNTU4LS4wNDU3NDQ3LS41NjQyNDYtLjE1MjQ4MjMtLjExMjg0OTItLjEwNjczNzYtLjIxMTU5MjMtLjMwNDk2NDUtLjI2Nzg3NTgtLjYwOTkyOTFsLTIuMTg2NDUzMy05LjgzNTEwNjM0LTIuMTcyMzQ3MSA5LjgxOTg1ODE0Yy0uMDcwNjcxOC4zMDQ5NjQ2LS4xNTUxNjc3LjUwMzE5MTUtLjI2ODAxNjkuNjA5OTI5MS0uMTEyODQ5Mi4xMDY3Mzc2LS4zMTAzMzUzLjE1MjQ4MjMtLjU2NDI0Ni4xNTI0ODIzaC0xLjIxMzEyODl6bTE4LjEyNjQwMjkuNDExNzAyMWMtLjczMzUxOTggMC0xLjQ2NzAzOTYtLjA5MTQ4OTQtMi4xNzIzNDcxLS4yNzQ0Njgxcy0xLjI1NTQ0NzQtLjM4MTIwNTctMS42MjIyMDczLS42MDk5MjkxYy0uMjI1Njk4NC0uMTM3MjM0LS4zODEwMDcxLS4yODk3MTYzLS40MzcyOTA2LS40MjY5NTAzLS4wNTY0MjQ2LS4xMzcyMzQxLS4wODQ2MzY5LS4yODk3MTYzLS4wODQ2MzY5LS40MjY5NTA0di0uNzc3NjU5NmMwLS4zMjAyMTI3LjExMjg0OTItLjQ3MjY5NS4zMjQ0NDE0LS40NzI2OTUuMDg0NjM2OSAwIC4xNjkxMzI3LjAxNTI0ODIuMjUzNzY5Ni4wNDU3NDQ3LjA4NDc3OC4wMzA0OTY0LjIxMTU5MjMuMDkxNDg5NC4zNTI3OTQ5LjE1MjQ4MjMuNDc5NDY4LjIyODcyMzQgMS4wMDE1MzY2LjQxMTcwMjEgMS41NTE1MzU0LjUzMzY4NzkuNTY0Mzg3MS4xMjE5ODU4IDEuMTE0Mzg1OS4xODI5Nzg3IDEuNjc4NzcyOS4xODI5Nzg3Ljg4ODY4NzUgMCAxLjU3OTg4ODgtLjE2NzczMDUgMi4wNTkzNTY5LS41MDMxOTE1LjQ3OTc1MDEtLjMzNTQ2MS43MzM2NjA4LS44MjM0MDQyLjczMzY2MDgtMS40NDg1ODE1IDAtLjQyNjk1MDQtLjEyNjk1NTMtLjc3NzY1OTYtLjM4MTAwNzEtMS4wNjczNzU5MS0uMjUzNzY5Ni0uMjg5NzE2MzEtLjczMzM3ODctLjU0ODkzNjE3LTEuNDI0NzIxMS0uNzkyOTA3OGwtMi4wNDUyNTA3LS42ODYxNzAyMWMtMS4wMjk3NDktLjM1MDcwOTIyLTEuNzkxNDgxMS0uODY5MTQ4OTQtMi4yNTY5ODQtMS41NTUzMTkxNS0uNDY1NTAzLS42NzA5MjE5OS0uNzA1NDQ4Ni0xLjQxODA4NTExLS43MDU0NDg2LTIuMjEwOTkyOTEgMC0uNjQwNDI1NTMuMTI3MDk2NC0xLjIwNDYwOTkzLjM4MTAwNzEtMS42OTI1NTMxOS4yNTM3Njk2LS40ODc5NDMyNi41OTI0NTgzLS45MTQ4OTM2MiAxLjAxNTY0MjgtMS4yNTAzNTQ2MS40MjMxODQ1LS4zNTA3MDkyMi45MDI3OTM2LS42MDk5MjkwOCAxLjQ2NzAzOTYtLjc5MjkwNzguNTY0MjQ2LS4xODI5Nzg3MiAxLjE1NjcwNDMtLjI1OTIxOTg2IDEuNzc3Mzc0OS0uMjU5MjE5ODYuMzEwMTk0MyAwIC42MzQ3NzY4LjAxNTI0ODIzLjk0NDk3MS4wNjA5OTI5MS4zMjQ1ODI1LjA0NTc0NDY4LjYyMDY3MDYuMTA2NzM3NTkuOTE2ODk5OC4xNjc3MzA1LjI4MjEyMy4wNzYyNDExMy41NTAxMzk4LjE1MjQ4MjI3LjgwNDE5MTYuMjQzOTcxNjMuMjUzNzY5Ni4wOTE0ODkzNi40NTEzOTY4LjE4Mjk3ODcyLjU5MjQ1ODMuMjc0NDY4MDguMTk3NDg2MS4xMjE5ODU4Mi4zMzg1NDc2LjI0Mzk3MTYzLjQyMzE4NDUuMzgxMjA1NjguMDg0NjM2OS4xMjE5ODU4MS4xMjY5NTUzLjI4OTcxNjMxLjEyNjk1NTMuNTAzMTkxNDl2LjcxNjY2NjY2YzAgLjMyMDIxMjc3LS4xMTI4NDkyLjQ4Nzk0MzI2LS4zMjQ0NDE0LjQ4Nzk0MzI2LS4xMTI4NDkyIDAtLjI5NjIyOTItLjA2MDk5MjktLjUzNjAzMzctLjE4Mjk3ODcyLS44MDQwNTA2LS4zOTY0NTM5LTEuNzA2ODQ0Mi0uNTk0NjgwODUtMi43MDgzODA4LS41OTQ2ODA4NS0uODA0MDUwNiAwLTEuNDM4ODI3My4xMzcyMzQwNC0xLjg3NjExOC40MjY5NTAzNS0uNDM3MjkwNi4yODk3MTYzMi0uNjYyOTg5LjczMTkxNDktLjY2Mjk4OSAxLjM1NzA5MjIgMCAuNDI2OTUwMzYuMTQxMDYxNS43OTI5MDc4LjQyMzE4NDUgMS4wODI2MjQxMi4yODIxMjMuMjg5NzE2MzEuODA0MDUwNS41Nzk0MzI2MiAxLjU1MTY3NjUuODM4NjUyNDhsMi4wMDMwNzMzLjY4NjE3MDIxYzEuMDE1NjQyOC4zNTA3MDkyMiAxLjc0OTE2MjYuODM4NjUyNDggMi4xODY0NTMyIDEuNDYzODI5NzkuNDM3MjkwNy42MjUxNzczLjY0ODg4MyAxLjM0MTg0Mzk3LjY0ODg4MyAyLjEzNDc1MTc1IDAgLjY1NTY3MzgtLjEyNjk1NTQgMS4yNTAzNTQ2LS4zNjY3NTk5IDEuNzY4Nzk0My0uMjUzOTEwOC41MTg0Mzk4LS41OTI0NTg0Ljk3NTg4NjYtMS4wMjk3NDkgMS4zNDE4NDQtLjQzNzI5MDcuMzgxMjA1Ny0uOTU5MjE4Mi42NTU2NzM4LTEuNTY1NzgyNy44NTM5MDA3LS42MzQ3NzY3LjIxMzQ3NTItMS4yOTc3NjU4LjMyMDIxMjgtMi4wMTcxNzk0LjMyMDIxMjh6Ii8+PHBhdGggZmlsbD0iI0ZGOTkwMCIgZD0iTTQ5Ljg3NTUwNyA0MS4wMDU5MzcyYy00LjcwNTEzNjcgMy42NzQ0MDUxLTExLjU0MTE3MTIgNS42MjUwMTUyLTE3LjQxODg3MzggNS42MjUwMTUyLTguMjM3NTY0NiAwLTE1LjY1OTk1MzYxLTMuMjIwNzc0OC0yMS4yNjYwNzM5OC04LjU3MzYxMTktLjQ0MzQ4NDE2LS40MjMzODgyLS4wNDI5MDM5OC0uOTk3OTg2NS40ODYyNDUxNC0uNjY1MzI0NCA2LjA2Mzc2Mjg1IDMuNzE5NzY4MiAxMy41NDMyMTQxNCA1Ljk3Mjc5ODQgMjEuMjgwMzc1MjQgNS45NzI3OTg0IDUuMjE5OTg0NiAwIDEwLjk1NDgxNjktMS4xNDkxOTY2IDE2LjIzMjAwNjctMy41MDgwNzQuNzg2NTczLS4zNzgwMjUyIDEuNDU4NzM1NC41NDQzNTYzLjY4NjMyMDcgMS4xNDkxOTY3em0xLjk1OTQyNDktMi4zNTg4NzczYy0uNjAwNzk4OC0uODE2NTM0NS0zLjk3NTc2OS0uMzkzMTQ2My01LjUwNjAxMTEtLjE5NjU3MzItLjQ1Nzc4NTUuMDYwNDg0MS0uNTI5MjkyMS0uMzYyOTA0Mi0uMTE0NDEwNi0uNjgwNDQ1MyAyLjY4ODY0OTYtMS45OTU5NzMyIDcuMTA3NjE2Ny0xLjQyMTM3NDggNy42MjI2MDc2LS43NTYwNTA1LjUxNDg0NzcuNjgwNDQ1NC0uMTQzMDEzMyA1LjM1MjgzNzEtMi42NjAxOSA3LjU5MDc0NjMtLjM4NTk5MjguMzQ3NzgzMi0uNzU3ODI3My4xNjYzMzExLS41ODYzNTQ0LS4yODcyOTkxLjU3MjA1MzEtMS40OTY5Nzk5IDEuODQ1MDE0My00Ljg2ODk2NDggMS4yNDQzNTg1LTUuNjcwMzc4MnoiLz48L2c+PC9zdmc+ license: Apache-2.0 maintainer: Crossplane Maintainers meta.crossplane.io/description: | The Amazon Web Services (AWS) Crossplane provider adds support for managing AWS resources in Kubernetes. meta.crossplane.io/iconURI: data:image/svg+xml;base64,CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNjUiIGhlaWdodD0iNjUiPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiB4PSIuNSIgeT0iLjUiIGZpbGw9IiNGQUZBRkEiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjRDhEOERBIiByeD0iMTYiLz48cGF0aCBmaWxsPSIjMjUyRjNFIiBkPSJNMjMuMjQ2MzY1NiAyOS45Njg5OTdjMCAuNTY0MTg0NC4wNTY0MjQ2IDEuMDIxNjMxMi4xNTUxNjc2IDEuMzU3MDkyMi4xMTI4NDkyLjMzNTQ2MS4yNTM5MTA3LjcwMTQxODQuNDUxMzk2OCAxLjA5Nzg3MjMuMDcwNTMwOC4xMjE5ODU4LjA5ODc0MzEuMjQzOTcxNi4wOTg3NDMxLjM1MDcwOTIgMCAuMTUyNDgyMy0uMDg0NjM2OS4zMDQ5NjQ2LS4yNjgwMTY5LjQ1NzQ0NjhsLS44ODg2ODc1LjY0MDQyNTZjLS4xMjY5NTUzLjA5MTQ4OTMtLjI1NDA1MTcuMTM3MjM0LS4zNjY3NTk5LjEzNzIzNC0uMTQxMDYxNSAwLS4yODIxMjMtLjA3NjI0MTEtLjQyMzE4NDUtLjIxMzQ3NTItLjE5NzQ4NjEtLjIyODcyMzQtLjM2Njc1OTktLjQ3MjY5NS0uNTA3ODIxNC0uNzE2NjY2Ni0uMTQxMDYxNS0uMjU5MjE5OS0uMjgyMTIzLS41NDg5MzYyLS40MzcyOTA2LS44OTk2NDU0LTEuMTAwNDIwNzggMS40MDI4MzY4LTIuNDgyNjgyNDIgMi4xMDQyNTUzLTQuMTQ3MjA4MTMgMi4xMDQyNTUzLTEuMTg0OTE2NjEgMC0yLjEzMDAyODY2LS4zNjU5NTc1LTIuODIxMjMwMDItMS4wOTc4NzI0LS42OTEyMDEzNS0uNzMxOTE0OC0xLjA0Mzg1NTEtMS43MDc4MDE0LTEuMDQzODU1MS0yLjkyNzY1OTUgMC0xLjI5NjA5OTMyLjQyMzA0MzQ0LTIuMzQ4MjI2OTggMS4yODM2NTk2NS0zLjE0MTEzNDc4Ljg2MDQ3NTE2LS43OTI5MDc4IDIuMDAzMDczMzEtMS4xODkzNjE3IDMuNDU2MDA2NzctMS4xODkzNjE3LjQ3OTQ2ODA0IDAgLjk3MzMyNDM2LjA0NTc0NDY4IDEuNDk1MjUxOTEuMTIxOTg1ODEuNTIxOTI3NTUuMDc2MjQxMTQgMS4wNTc5NjEyNi4xOTgyMjY5NSAxLjYyMjIwNzI2LjMzNTQ2MXYtMS4xMTMxMjA1N2MwLTEuMTU4ODY1MjUtLjIyNTY5ODQtMS45NjcwMjEyOC0uNjYyOTg5MDUtMi40Mzk3MTYzMS0uNDUxMzk2ODEtLjQ3MjY5NTA0LTEuMjEzMTI4OTEtLjcwMTQxODQ0LTIuMjk5MzAyNDctLjcwMTQxODQ0LS40OTM3MTUyNSAwLTEuMDAxNTM2NjUuMDYwOTkyOTEtMS41MjM0NjQyLjE5ODIyNjk1LS41MjE5Mjc1Ni4xMzcyMzQwNC0xLjAyOTc0ODk2LjMwNDk2NDU0LTEuNTIzNDY0MjEuNTE4NDM5NzEtLjIyNTY5ODQuMTA2NzM3NTktLjM5NDk3MjIuMTY3NzMwNS0uNDkzNzE1MjYuMTk4MjI2OTYtLjA5ODc0MzA1LjAzMDQ5NjQ1LS4xNjkyNzM4LjA0NTc0NDY4LS4yMjU4Mzk0Ni4wNDU3NDQ2OC0uMTk3MzQ1MDQgMC0uMjk2MDg4MDktLjE1MjQ4MjI3LS4yOTYwODgwOS0uNDcyNjk1MDR2LS43NDcxNjMxMmMwLS4yNDM5NzE2My4wMjgyMTIzLS40MjY5NTAzNS4wOTg3NDMwNS0uNTMzNjg3OTRzLjE5NzM0NTA0LS4yMTM0NzUxOC4zOTQ5NzIyLS4zMjAyMTI3N2MuNDkzNTc0Mi0uMjc0NDY4MDggMS4wODYxNzM1Ni0uNTAzMTkxNDkgMS43NzczNzQ5MS0uNjg2MTcwMjEuNjkxMjAxMzYtLjE5ODIyNjk1IDEuNDI0NzIxMTYtLjI4OTcxNjMxIDIuMjAwNTU5NDItLjI4OTcxNjMxIDEuNjc4NjMxODUgMCAyLjkwNTg2NjkyLjQxMTcwMjEyIDMuNjk1ODExMzIgMS4yMzUxMDYzOC43NzU4MzgyLjgyMzQwNDI1IDEuMTcwODEwNSAyLjA3Mzc1ODg2IDEuMTcwODEwNSAzLjc1MTA2MzgzVjI5Ljk2ODk5N2guMDI4MjEyM3ptLTUuNzI3MDk2OTggMi4zMTc3MzA1Yy40NjU1MDI5NSAwIC45NDQ5NzA5OS0uMDkxNDg5NCAxLjQ1MjkzMzQ2LS4yNzQ0NjgxLjUwNzgyMTQtLjE4Mjk3ODguOTU5MjE4Mi0uNTE4NDM5NyAxLjM0MDA4NDI2LS45NzU4ODY2LjIyNTY5ODQtLjI4OTcxNjMuMzk0ODMxMTQtLjYwOTkyOS40Nzk2MDkxLS45NzU4ODY1LjA4NDYzNjktLjM2NTk1NzQuMTQxMDYxNS0uODA4MTU2MDEuMTQxMDYxNS0xLjMyNjU5NTcydi0uNjQwNDI1NTNjLS40MDkwNzgzNS0uMTA2NzM3NTktLjg0NjM2OTAxLS4xOTgyMjY5NS0xLjI5Nzc2NTgxLS4yNTkyMTk4Ni0uNDUxMzk2OC0uMDYwOTkyOTEtLjg4ODY4NzQ1LS4wOTE0ODkzNi0xLjMyNTk3ODEtLjA5MTQ4OTM2LS45NDUxMTIwNiAwLTEuNjM2MzEzNDEuMTk4MjI2OTUtMi4xMDE4MTYzNy42MDk5MjkwNy0uNDY1NTAyOTUuNDExNzAyMTMtLjY5MTIwMTM1Ljk5MTEzNDc2LS42OTEyMDEzNSAxLjc1MzU0NjEgMCAuNzE2NjY2Ny4xNjkyNzM4IDEuMjUwMzU0Ni41MjE5Mjc1NSAxLjYxNjMxMjEuMzM4NTQ3NjEuMzgxMjA1Ni44MzIyNjI4Ni41NjQxODQ0IDEuNDgxMTQ1NzYuNTY0MTg0NHpNMjguODQ2NTA3MSAzMy45MzM1MzZjLS4yNTM5MTA3IDAtLjQyMzE4NDUtLjA0NTc0NDctLjUzNjAzMzctLjE1MjQ4MjMtLjExMjg0OTItLjA5MTQ4OTQtLjIxMTU5MjItLjMwNDk2NDUtLjI5NjIyOTEtLjU5NDY4MDlMMjQuNjk5Mjk5IDIxLjM5OTQ5MzQxYy0uMDg0Nzc4LS4zMDQ5NjQ1My0uMTI2OTU1My0uNTAzMTkxNDgtLjEyNjk1NTMtLjYwOTkyOTA3IDAtLjI0Mzk3MTYzLjExMjcwODEtLjM4MTIwNTY4LjMzODU0NzYtLjM4MTIwNTY4aDEuMzgyNDAyN2MuMjY3ODc1OCAwIC40NTEzOTY4LjA0NTc0NDY4LjU1MDEzOTguMTUyNDgyMjcuMTEyODQ5Mi4wOTE0ODkzNi4xOTc0ODYxLjMwNDk2NDU0LjI4MjEyMy41OTQ2ODA4NUwyOS40OTUzOSAzMS4yNDk4NDhsMi4yMDA0MTg0LTEwLjA5NDMyNjIyYy4wNzA2NzE4LS4zMDQ5NjQ1NC4xNTUxNjc2LS41MDMxOTE0OS4yNjgxNTc5LS41OTQ2ODA4NS4xMTI4NDkyLS4wOTE0ODkzNi4zMTAxOTQyLS4xNTI0ODIyNy41NjQyNDYtLjE1MjQ4MjI3aDEuMTI4NDkyYy4yNjc4NzU4IDAgLjQ1MTM5NjguMDQ1NzQ0NjguNTY0MjQ2LjE1MjQ4MjI3LjExMjg0OTIuMDkxNDg5MzYuMjExNTkyMy4zMDQ5NjQ1NC4yNjgwMTY5LjU5NDY4MDg1bDIuMjI4NjMwNiAxMC4yMTYzMTIwMiAyLjQ0MDUwNS0xMC4yMTYzMTIwMmMuMDg0NjM2OS0uMzA0OTY0NTQuMTgzMzgtLjUwMzE5MTQ5LjI4MjEyMy0uNTk0NjgwODUuMTEyODQ5Mi0uMDkxNDg5MzYuMjk2MjI5Mi0uMTUyNDgyMjcuNTUwMTM5OS0uMTUyNDgyMjdoMS4zMTE4NzE5Yy4yMjU2OTg0IDAgLjM1MjY1MzguMTIxOTg1ODIuMzUyNjUzOC4zODEyMDU2OCAwIC4wNzYyNDExMy0uMDE0MTA2Mi4xNTI0ODIyNy0uMDI4MjEyMy4yNDM5NzE2My0uMDE0MjQ3Mi4wOTE0ODkzNi0uMDQyMzE4NS4yMTM0NzUxNy0uMDk4NzQzMS4zODEyMDU2N0wzOC4xMjgzNTM5IDMzLjIwMTYyMTFjLS4wODQ3NzguMzA0OTY0NS0uMTgzNTIxLjUwMzE5MTUtLjI5NjM3MDIuNTk0NjgwOC0uMTEyNzA4Mi4wOTE0ODk0LS4yOTYwODgxLjE1MjQ4MjMtLjUzNTg5MjcuMTUyNDgyM2gtMS4yMTMyNjk5Yy0uMjY3ODc1OCAwLS40NTEyNTU4LS4wNDU3NDQ3LS41NjQyNDYtLjE1MjQ4MjMtLjExMjg0OTItLjEwNjczNzYtLjIxMTU5MjMtLjMwNDk2NDUtLjI2Nzg3NTgtLjYwOTkyOTFsLTIuMTg2NDUzMy05LjgzNTEwNjM0LTIuMTcyMzQ3MSA5LjgxOTg1ODE0Yy0uMDcwNjcxOC4zMDQ5NjQ2LS4xNTUxNjc3LjUwMzE5MTUtLjI2ODAxNjkuNjA5OTI5MS0uMTEyODQ5Mi4xMDY3Mzc2LS4zMTAzMzUzLjE1MjQ4MjMtLjU2NDI0Ni4xNTI0ODIzaC0xLjIxMzEyODl6bTE4LjEyNjQwMjkuNDExNzAyMWMtLjczMzUxOTggMC0xLjQ2NzAzOTYtLjA5MTQ4OTQtMi4xNzIzNDcxLS4yNzQ0Njgxcy0xLjI1NTQ0NzQtLjM4MTIwNTctMS42MjIyMDczLS42MDk5MjkxYy0uMjI1Njk4NC0uMTM3MjM0LS4zODEwMDcxLS4yODk3MTYzLS40MzcyOTA2LS40MjY5NTAzLS4wNTY0MjQ2LS4xMzcyMzQxLS4wODQ2MzY5LS4yODk3MTYzLS4wODQ2MzY5LS40MjY5NTA0di0uNzc3NjU5NmMwLS4zMjAyMTI3LjExMjg0OTItLjQ3MjY5NS4zMjQ0NDE0LS40NzI2OTUuMDg0NjM2OSAwIC4xNjkxMzI3LjAxNTI0ODIuMjUzNzY5Ni4wNDU3NDQ3LjA4NDc3OC4wMzA0OTY0LjIxMTU5MjMuMDkxNDg5NC4zNTI3OTQ5LjE1MjQ4MjMuNDc5NDY4LjIyODcyMzQgMS4wMDE1MzY2LjQxMTcwMjEgMS41NTE1MzU0LjUzMzY4NzkuNTY0Mzg3MS4xMjE5ODU4IDEuMTE0Mzg1OS4xODI5Nzg3IDEuNjc4NzcyOS4xODI5Nzg3Ljg4ODY4NzUgMCAxLjU3OTg4ODgtLjE2NzczMDUgMi4wNTkzNTY5LS41MDMxOTE1LjQ3OTc1MDEtLjMzNTQ2MS43MzM2NjA4LS44MjM0MDQyLjczMzY2MDgtMS40NDg1ODE1IDAtLjQyNjk1MDQtLjEyNjk1NTMtLjc3NzY1OTYtLjM4MTAwNzEtMS4wNjczNzU5MS0uMjUzNzY5Ni0uMjg5NzE2MzEtLjczMzM3ODctLjU0ODkzNjE3LTEuNDI0NzIxMS0uNzkyOTA3OGwtMi4wNDUyNTA3LS42ODYxNzAyMWMtMS4wMjk3NDktLjM1MDcwOTIyLTEuNzkxNDgxMS0uODY5MTQ4OTQtMi4yNTY5ODQtMS41NTUzMTkxNS0uNDY1NTAzLS42NzA5MjE5OS0uNzA1NDQ4Ni0xLjQxODA4NTExLS43MDU0NDg2LTIuMjEwOTkyOTEgMC0uNjQwNDI1NTMuMTI3MDk2NC0xLjIwNDYwOTkzLjM4MTAwNzEtMS42OTI1NTMxOS4yNTM3Njk2LS40ODc5NDMyNi41OTI0NTgzLS45MTQ4OTM2MiAxLjAxNTY0MjgtMS4yNTAzNTQ2MS40MjMxODQ1LS4zNTA3MDkyMi45MDI3OTM2LS42MDk5MjkwOCAxLjQ2NzAzOTYtLjc5MjkwNzguNTY0MjQ2LS4xODI5Nzg3MiAxLjE1NjcwNDMtLjI1OTIxOTg2IDEuNzc3Mzc0OS0uMjU5MjE5ODYuMzEwMTk0MyAwIC42MzQ3NzY4LjAxNTI0ODIzLjk0NDk3MS4wNjA5OTI5MS4zMjQ1ODI1LjA0NTc0NDY4LjYyMDY3MDYuMTA2NzM3NTkuOTE2ODk5OC4xNjc3MzA1LjI4MjEyMy4wNzYyNDExMy41NTAxMzk4LjE1MjQ4MjI3LjgwNDE5MTYuMjQzOTcxNjMuMjUzNzY5Ni4wOTE0ODkzNi40NTEzOTY4LjE4Mjk3ODcyLjU5MjQ1ODMuMjc0NDY4MDguMTk3NDg2MS4xMjE5ODU4Mi4zMzg1NDc2LjI0Mzk3MTYzLjQyMzE4NDUuMzgxMjA1NjguMDg0NjM2OS4xMjE5ODU4MS4xMjY5NTUzLjI4OTcxNjMxLjEyNjk1NTMuNTAzMTkxNDl2LjcxNjY2NjY2YzAgLjMyMDIxMjc3LS4xMTI4NDkyLjQ4Nzk0MzI2LS4zMjQ0NDE0LjQ4Nzk0MzI2LS4xMTI4NDkyIDAtLjI5NjIyOTItLjA2MDk5MjktLjUzNjAzMzctLjE4Mjk3ODcyLS44MDQwNTA2LS4zOTY0NTM5LTEuNzA2ODQ0Mi0uNTk0NjgwODUtMi43MDgzODA4LS41OTQ2ODA4NS0uODA0MDUwNiAwLTEuNDM4ODI3My4xMzcyMzQwNC0xLjg3NjExOC40MjY5NTAzNS0uNDM3MjkwNi4yODk3MTYzMi0uNjYyOTg5LjczMTkxNDktLjY2Mjk4OSAxLjM1NzA5MjIgMCAuNDI2OTUwMzYuMTQxMDYxNS43OTI5MDc4LjQyMzE4NDUgMS4wODI2MjQxMi4yODIxMjMuMjg5NzE2MzEuODA0MDUwNS41Nzk0MzI2MiAxLjU1MTY3NjUuODM4NjUyNDhsMi4wMDMwNzMzLjY4NjE3MDIxYzEuMDE1NjQyOC4zNTA3MDkyMiAxLjc0OTE2MjYuODM4NjUyNDggMi4xODY0NTMyIDEuNDYzODI5NzkuNDM3MjkwNy42MjUxNzczLjY0ODg4MyAxLjM0MTg0Mzk3LjY0ODg4MyAyLjEzNDc1MTc1IDAgLjY1NTY3MzgtLjEyNjk1NTQgMS4yNTAzNTQ2LS4zNjY3NTk5IDEuNzY4Nzk0My0uMjUzOTEwOC41MTg0Mzk4LS41OTI0NTg0Ljk3NTg4NjYtMS4wMjk3NDkgMS4zNDE4NDQtLjQzNzI5MDcuMzgxMjA1Ny0uOTU5MjE4Mi42NTU2NzM4LTEuNTY1NzgyNy44NTM5MDA3LS42MzQ3NzY3LjIxMzQ3NTItMS4yOTc3NjU4LjMyMDIxMjgtMi4wMTcxNzk0LjMyMDIxMjh6Ii8+PHBhdGggZmlsbD0iI0ZGOTkwMCIgZD0iTTQ5Ljg3NTUwNyA0MS4wMDU5MzcyYy00LjcwNTEzNjcgMy42NzQ0MDUxLTExLjU0MTE3MTIgNS42MjUwMTUyLTE3LjQxODg3MzggNS42MjUwMTUyLTguMjM3NTY0NiAwLTE1LjY1OTk1MzYxLTMuMjIwNzc0OC0yMS4yNjYwNzM5OC04LjU3MzYxMTktLjQ0MzQ4NDE2LS40MjMzODgyLS4wNDI5MDM5OC0uOTk3OTg2NS40ODYyNDUxNC0uNjY1MzI0NCA2LjA2Mzc2Mjg1IDMuNzE5NzY4MiAxMy41NDMyMTQxNCA1Ljk3Mjc5ODQgMjEuMjgwMzc1MjQgNS45NzI3OTg0IDUuMjE5OTg0NiAwIDEwLjk1NDgxNjktMS4xNDkxOTY2IDE2LjIzMjAwNjctMy41MDgwNzQuNzg2NTczLS4zNzgwMjUyIDEuNDU4NzM1NC41NDQzNTYzLjY4NjMyMDcgMS4xNDkxOTY3em0xLjk1OTQyNDktMi4zNTg4NzczYy0uNjAwNzk4OC0uODE2NTM0NS0zLjk3NTc2OS0uMzkzMTQ2My01LjUwNjAxMTEtLjE5NjU3MzItLjQ1Nzc4NTUuMDYwNDg0MS0uNTI5MjkyMS0uMzYyOTA0Mi0uMTE0NDEwNi0uNjgwNDQ1MyAyLjY4ODY0OTYtMS45OTU5NzMyIDcuMTA3NjE2Ny0xLjQyMTM3NDggNy42MjI2MDc2LS43NTYwNTA1LjUxNDg0NzcuNjgwNDQ1NC0uMTQzMDEzMyA1LjM1MjgzNzEtMi42NjAxOSA3LjU5MDc0NjMtLjM4NTk5MjguMzQ3NzgzMi0uNzU3ODI3My4xNjYzMzExLS41ODYzNTQ0LS4yODcyOTkxLjU3MjA1MzEtMS40OTY5Nzk5IDEuODQ1MDE0My00Ljg2ODk2NDggMS4yNDQzNTg1LTUuNjcwMzc4MnoiLz48L2c+PC9zdmc+ meta.crossplane.io/license: Apache-2.0 meta.crossplane.io/maintainer: Crossplane Maintainers meta.crossplane.io/readme: | `provider-aws` is the Crossplane infrastructure provider for [Amazon Web Services (AWS)](https://aws.amazon.com/). Available resources and their fields can be found in the [CRD Docs](https://doc.crds.dev/github.com/crossplane/provider-aws). If you encounter an issue please reach out on [slack.crossplane.io](https://slack.crossplane.io) and create an issue in the [crossplane/provider-aws](https://github.com/crossplane/provider-aws) repo. meta.crossplane.io/source: github.com/crossplane/provider-aws readme: | `provider-aws` is the Crossplane infrastructure provider for [Amazon Web Services (AWS)](https://aws.amazon.com/). Available resources and their fields can be found in the [CRD Docs](https://doc.crds.dev/github.com/crossplane/provider-aws). If you encounter an issue please reach out on [slack.crossplane.io](https://slack.crossplane.io) and create an issue in the [crossplane/provider-aws](https://github.com/crossplane/provider-aws) repo. source: github.com/crossplane/provider-aws creationTimestamp: null name: provider-aws spec: controller: image: crossplane/provider-aws-controller:v0.20.0 ================================================ FILE: pkg/xpkg/testdata/providerconfigs.helm.crossplane.io.yaml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.6.1 name: providerconfigs.helm.crossplane.io spec: group: helm.crossplane.io names: categories: - crossplane - provider - helm kind: ProviderConfig listKind: ProviderConfigList plural: providerconfigs singular: providerconfig scope: Cluster versions: - additionalPrinterColumns: - jsonPath: .metadata.creationTimestamp name: AGE type: date - jsonPath: .spec.credentialsSecretRef.name name: SECRET-NAME priority: 1 type: string name: v1alpha1 schema: openAPIV3Schema: description: A ProviderConfig configures a Helm 'provider', i.e. a connection to a particular properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: A ProviderConfigSpec defines the desired state of a Provider. properties: credentials: description: Credentials used to connect to the Kubernetes API. Typically a kubeconfig file. Use InjectedIdentity for in-cluster config. properties: env: description: Env is a reference to an environment variable that contains credentials that must be used to connect to the provider. properties: name: description: Name is the name of an environment variable. type: string required: - name type: object fs: description: Fs is a reference to a filesystem location that contains credentials that must be used to connect to the provider. properties: path: description: Path is a filesystem path. type: string required: - path type: object secretRef: description: A SecretRef is a reference to a secret key that contains the credentials that must be used to connect to the provider. properties: key: description: The key to select. type: string name: description: Name of the secret. type: string namespace: description: Namespace of the secret. type: string required: - key - name - namespace type: object source: description: Source of the provider credentials. enum: - None - Secret - InjectedIdentity - Environment - Filesystem type: string required: - source type: object identity: description: Identity used to authenticate to the Kubernetes API. The identity credentials can be used to supplement kubeconfig 'credentials', for example by configuring a bearer token source such as OAuth. properties: env: description: Env is a reference to an environment variable that contains credentials that must be used to connect to the provider. properties: name: description: Name is the name of an environment variable. type: string required: - name type: object fs: description: Fs is a reference to a filesystem location that contains credentials that must be used to connect to the provider. properties: path: description: Path is a filesystem path. type: string required: - path type: object secretRef: description: A SecretRef is a reference to a secret key that contains the credentials that must be used to connect to the provider. properties: key: description: The key to select. type: string name: description: Name of the secret. type: string namespace: description: Namespace of the secret. type: string required: - key - name - namespace type: object source: description: Source of the provider credentials. enum: - None - Secret - InjectedIdentity - Environment - Filesystem type: string type: description: Type of identity. enum: - GoogleApplicationCredentials type: string required: - source - type type: object required: - credentials type: object status: description: A ProviderConfigStatus defines the status of a Provider. properties: conditions: description: Conditions of the resource. items: description: A Condition that may apply to a resource. properties: lastTransitionTime: description: LastTransitionTime is the last time this condition transitioned from one status to another. format: date-time type: string message: description: A Message containing details about this condition's last transition from one status to another, if any. type: string reason: description: A Reason for this condition's last transition from one status to another. type: string status: description: Status of this condition; is it currently True, False, or Unknown? type: string type: description: Type of this condition. At most one of each condition type may apply to a resource at any point in time. type: string required: - lastTransitionTime - reason - status - type type: object type: array users: description: Users of this provider configuration. format: int64 type: integer type: object required: - spec type: object served: true storage: false subresources: status: {} - additionalPrinterColumns: - jsonPath: .metadata.creationTimestamp name: AGE type: date - jsonPath: .spec.credentialsSecretRef.name name: SECRET-NAME priority: 1 type: string name: v1beta1 schema: openAPIV3Schema: description: A ProviderConfig configures a Helm 'provider', i.e. a connection to a particular properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: A ProviderConfigSpec defines the desired state of a Provider. properties: credentials: description: Credentials used to connect to the Kubernetes API. Typically a kubeconfig file. Use InjectedIdentity for in-cluster config. properties: env: description: Env is a reference to an environment variable that contains credentials that must be used to connect to the provider. properties: name: description: Name is the name of an environment variable. type: string required: - name type: object fs: description: Fs is a reference to a filesystem location that contains credentials that must be used to connect to the provider. properties: path: description: Path is a filesystem path. type: string required: - path type: object secretRef: description: A SecretRef is a reference to a secret key that contains the credentials that must be used to connect to the provider. properties: key: description: The key to select. type: string name: description: Name of the secret. type: string namespace: description: Namespace of the secret. type: string required: - key - name - namespace type: object source: description: Source of the provider credentials. enum: - None - Secret - InjectedIdentity - Environment - Filesystem type: string required: - source type: object identity: description: Identity used to authenticate to the Kubernetes API. The identity credentials can be used to supplement kubeconfig 'credentials', for example by configuring a bearer token source such as OAuth. properties: env: description: Env is a reference to an environment variable that contains credentials that must be used to connect to the provider. properties: name: description: Name is the name of an environment variable. type: string required: - name type: object fs: description: Fs is a reference to a filesystem location that contains credentials that must be used to connect to the provider. properties: path: description: Path is a filesystem path. type: string required: - path type: object secretRef: description: A SecretRef is a reference to a secret key that contains the credentials that must be used to connect to the provider. properties: key: description: The key to select. type: string name: description: Name of the secret. type: string namespace: description: Namespace of the secret. type: string required: - key - name - namespace type: object source: description: Source of the provider credentials. enum: - None - Secret - InjectedIdentity - Environment - Filesystem type: string type: description: Type of identity. enum: - GoogleApplicationCredentials type: string required: - source - type type: object required: - credentials type: object status: description: A ProviderConfigStatus defines the status of a Provider. properties: conditions: description: Conditions of the resource. items: description: A Condition that may apply to a resource. properties: lastTransitionTime: description: LastTransitionTime is the last time this condition transitioned from one status to another. format: date-time type: string message: description: A Message containing details about this condition's last transition from one status to another, if any. type: string reason: description: A Reason for this condition's last transition from one status to another. type: string status: description: Status of this condition; is it currently True, False, or Unknown? type: string type: description: Type of this condition. At most one of each condition type may apply to a resource at any point in time. type: string required: - lastTransitionTime - reason - status - type type: object type: array users: description: Users of this provider configuration. format: int64 type: integer type: object required: - spec type: object served: true storage: true subresources: status: {} status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: [] ================================================ FILE: pkg/xpkg/validate.go ================================================ /* Copyright 2025 The Crossplane Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package xpkg import "github.com/crossplane/crossplane-runtime/v2/pkg/xpkg/parser" // Validator validates packages before installation is attempted. type Validator parser.Linter // NewProviderValidator is a convenience function for creating a package // validator for providers. func NewProviderValidator() Validator { return parser.NewPackageLinter( parser.PackageLinterFns(OneMeta), parser.ObjectLinterFns(IsProvider, PackageValidSemver), parser.ObjectLinterFns()) } // NewConfigurationValidator is a convenience function for creating a package // validator for configurations. func NewConfigurationValidator() Validator { return parser.NewPackageLinter( parser.PackageLinterFns(OneMeta), parser.ObjectLinterFns(IsConfiguration, PackageValidSemver), parser.ObjectLinterFns()) } // NewFunctionValidator is a convenience function for creating a package // validator for functions. func NewFunctionValidator() Validator { return parser.NewPackageLinter( parser.PackageLinterFns(OneMeta), parser.ObjectLinterFns(IsFunction, PackageValidSemver), parser.ObjectLinterFns()) } ================================================ FILE: test/fuzz/oss_fuzz_build.sh ================================================ #!/bin/bash -eu # # IMPORTANT: Fuzz* test cases should be in a dedicated file, conventionally # called `fuzz_test.go`, but that's not a requirement. Otherwise once the file # name is changed to have the _test_fuzz.go termination instead of _test.go as # required by oss-fuzz, the code won't compile as other Test* test cases might # not find some requirements given that they are not in a _test.go file. # # DO NOT DELETE: this script is used from oss-fuzz. You can find more details # in the official documentation: # https://google.github.io/oss-fuzz/getting-started/new-project-guide/go-lang/ # # To run this locally you can go through the following steps: - $ git clone # https://github.com/google/oss-fuzz --depth=1 - $ cd # oss-fuzz/projects/crossplane # - modify Dockerfile to point to your branch with all the fuzzers being merged. # - modify build.sh to call the build script in Crossplanes repository # - $ python3 ../../infra/helper.py build_image crossplane # - $ python3 ../../infra/helper.py build_fuzzers crossplane set -o nounset set -o pipefail set -o errexit set -x printf "package main\nimport ( \n _ \"github.com/AdamKorcz/go-118-fuzz-build/testing\"\n )\n" > register.go # Moving all the fuzz_test.go to fuzz_test_fuzz.go, as oss-fuzz uses go build to build fuzzers # shellcheck disable=SC2016 grep --line-buffered --include '*_test.go' -Pr 'func Fuzz.*\(.* \*testing\.F' | cut -d: -f1 | sort -u | xargs -I{} sh -c ' file="{}" file_no_ext="$(basename "$file" | cut -d"." -f1)" folder="$(dirname $file)" mv "$file" "$folder/${file_no_ext}_fuzz.go" ' # Now we can tidy and download all our dependencies go mod tidy go mod vendor # Find all native fuzzers and compile them # shellcheck disable=SC2016 grep --line-buffered --include '*_test_fuzz.go' -Pr 'func Fuzz.*\(.* \*testing\.F' | sed -E 's/(func Fuzz(.*)\(.*)/\2/' | xargs -I{} sh -c ' file="$(echo "{}" | cut -d: -f1)" folder="$(dirname $file)" func="Fuzz$(echo "{}" | cut -d: -f2)" compile_native_go_fuzzer github.com/crossplane/crossplane-runtime/v2/$folder $func $func '