Compare commits

...

57 Commits

Author SHA1 Message Date
Hasan Turken 1e3dba95ee
Merge pull request #854 from erhancagirici/ec-xpv2
add v2 apis
2025-07-22 17:33:02 +03:00
Erhan Cagirici cf256f8643 use correct namespace from MR in MultiNamespacedResolution
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
2025-07-22 16:59:23 +03:00
Erhan Cagirici 14718766b2 make generate: protobuf updates after protoc-gen-go-grpc bump
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
2025-07-22 13:16:29 +03:00
Erhan Cagirici 8ba120d73e make generate: refactored common api deepcopy methods
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
2025-07-22 13:15:24 +03:00
Erhan Cagirici 5c98d3248a bump controller-tools to v1.18.0
fixes deepcopy method generation for structs that has aliased types

Signed-off-by: Erhan Cagirici <erhan@upbound.io>
2025-07-22 13:12:11 +03:00
Erhan Cagirici c056ae9de1 refactor shared api types to common package
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
2025-07-22 13:12:01 +03:00
Erhan Cagirici 54f050c217 add unit tests for v2
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
2025-07-21 18:26:28 +03:00
Erhan Cagirici f09b0a3b35 add v2 apis
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
2025-07-21 18:25:20 +03:00
Nic Cope ab24452c43
Merge pull request #829 from orange-cloudfoundry/feature-namespace-restricted-option
Add filter functions to event recorder
2025-07-18 18:40:28 -07:00
Nic Cope f5f608c93d
Merge pull request #856 from n3wscott/gate-until
Adding Gate and a CustomResourceGate reconciler
2025-07-11 15:08:17 -07:00
Scott Nichols 990e45c003 check for nil gates in setup, not reconcile.
Signed-off-by: Scott Nichols <n3wscott@upbound.io>
2025-07-10 18:01:26 -07:00
Scott Nichols 066eac1759 use satisfied to make them bools
Signed-off-by: Scott Nichols <n3wscott@upbound.io>
2025-07-10 17:57:42 -07:00
Scott Nichols 68a06acc30 nit on underscores
Signed-off-by: Scott Nichols <n3wscott@upbound.io>
2025-07-10 17:55:55 -07:00
Scott Nichols d8f858d664 react to feedback, make the interface tighter on the gate
Signed-off-by: Scott Nichols <n3wscott@upbound.io>
2025-07-10 17:54:21 -07:00
Scott Nichols 8cc1013bff Adding Gate implementation and a reconciler that reconciles and tracks CRDs to use and unlock the gates registered
Signed-off-by: Scott Nichols <n3wscott@upbound.io>
2025-07-10 14:57:24 -07:00
Philippe Scorsolini 3029c4a90a
Merge pull request #852 from negz/y-u-do-this-golangci
Bump to Go v1.24, and golangci-lint v2.2.0
2025-07-04 16:42:44 +02:00
Nic Cope 678177c524 Run golangci-lint run --fix
This commit is entirely generated by earthly +reviewable

Signed-off-by: Nic Cope <nicc@rk0n.org>
2025-07-03 12:51:13 -07:00
Nic Cope aad05b013a Address false positive recvcheck linter warning
Signed-off-by: Nic Cope <nicc@rk0n.org>
2025-07-03 12:49:57 -07:00
Nic Cope a592ede87e Address linter warnings about embedded structs
Specifically it wants them to appear first, before non-embedded things.

Signed-off-by: Nic Cope <nicc@rk0n.org>
2025-07-03 11:34:05 -07:00
Nic Cope d65515ca89 Bump Go to v1.24, golangci-lint to v2.2
Signed-off-by: Nic Cope <nicc@rk0n.org>
2025-07-03 11:33:30 -07:00
Nic Cope ebceaacf21 Drop c/c import rule
We don't import c/c here

Signed-off-by: Nic Cope <nicc@rk0n.org>
2025-07-03 11:28:02 -07:00
Nic Cope a2ee2dddbd Copy v2 golangci config from c/c
Signed-off-by: Nic Cope <nicc@rk0n.org>
2025-07-03 11:27:14 -07:00
Nic Cope 0d81d3f7c2
Merge pull request #849 from negz/x-treme
Backport c/c internal/xresource
2025-06-20 11:57:48 -07:00
Nic Cope 794eae126b Backport c/c internal/xresource
Signed-off-by: Nic Cope <nicc@rk0n.org>
2025-06-17 17:27:55 -07:00
François Rigaut 61bc9edaf2 add filter functions to event recorder
Signed-off-by: François Rigaut <francois.rigaut@orange.com>
2025-06-11 14:21:25 +02:00
Nic Cope 2b288ff362
Merge pull request #841 from nolancon/requeue-deterministic-mr-if-pending
Avoid non-requeue for MRs with determinstic external names.
2025-06-09 13:19:44 -07:00
Jared Watts 0063d2988c
Merge pull request #843 from jbw976/v2-providers
feat: enable namespaced reference resolution
2025-06-09 12:05:46 +02:00
Jared Watts dd27392560
feat: enable namespaced reference resolution
Signed-off-by: Jared Watts <jbw976@gmail.com>
2025-06-09 11:48:38 +02:00
Jared Watts 28c1851bf5
Merge pull request #802 from guilhem/ResolveMultipleOrder
fix: order values in MultiResolutionResponse
2025-06-09 11:16:26 +02:00
nolancon 6120cd37b6 Add debug log for pending + deterministic name case
Signed-off-by: nolancon <cmsnolan@gmail.com>
2025-06-09 08:33:12 +00:00
Jared Watts 27a394364d
Merge pull request #846 from negz/dirty-dirty-repo
Run `go mod tidy`
2025-06-07 17:32:31 +02:00
Guilhem Lettron d48e582450 fix: order values in MultiResolutionResponse
Signed-off-by: Guilhem Lettron <glettron@akamai.com>
2025-06-07 16:31:32 +02:00
Nic Cope fcbcd1a315 Run go mod tidy
Signed-off-by: Nic Cope <nicc@rk0n.org>
2025-06-06 21:41:09 -07:00
Nic Cope 43879203d3
Merge pull request #826 from johngmyers/cobra
chore(deps): update module github.com/spf13/cobra to v1.9.1
2025-06-06 21:30:02 -07:00
Nic Cope 27adbc5d9e
Merge pull request #816 from crossplane/renovate/main-renovatebot-github-action-40.x
chore(deps): update renovatebot/github-action action to v40.3.6 (main)
2025-06-06 21:24:55 -07:00
Nic Cope e41983dddc
Merge pull request #817 from crossplane/renovate/main-github.com-google-go-cmp-0.x
fix(deps): update module github.com/google/go-cmp to v0.7.0 (main)
2025-06-06 21:23:27 -07:00
Nic Cope 739f7236b8
Merge pull request #814 from crossplane/renovate/main-actions-create-github-app-token-digest
chore(deps): update actions/create-github-app-token digest to d72941d (main)
2025-06-06 21:12:57 -07:00
nolancon da72350d65 Add deterministicExternalName to ReconcilerOptions
Signed-off-by: nolancon <cmsnolan@gmail.com>
2025-06-06 11:38:47 +00:00
nolancon 334eed1791 Revert "Add AnnotationKeyExternalNameIsDeterministic with helper"
This reverts commit fb7b837275.

Signed-off-by: nolancon <cmsnolan@gmail.com>
2025-06-06 11:37:38 +00:00
nolancon 386b31235c Revert "Only avoid re-queue for non-deterministically named resources"
This reverts commit 604c58f9eb.

Signed-off-by: nolancon <cmsnolan@gmail.com>
2025-06-06 11:37:13 +00:00
nolancon 604c58f9eb Only avoid re-queue for non-deterministically named resources
Signed-off-by: nolancon <cmsnolan@gmail.com>
2025-05-28 10:13:52 +00:00
nolancon fb7b837275 Add AnnotationKeyExternalNameIsDeterministic with helper
Signed-off-by: nolancon <cmsnolan@gmail.com>
2025-05-28 10:13:52 +00:00
Nic Cope 532a1c432f
Merge pull request #838 from chlunde/unfmt
Update Event recorder to not parse % format characters in error message
2025-05-21 14:26:55 -07:00
Nic Cope 15b6a5540c
Merge pull request #840 from n3wscott/fix-dupe-interface
fix lint issue on deprecated interfaces
2025-05-21 14:26:22 -07:00
Scott Nichols fa083bc2b6 fix lint issue on deprecated interfaces
Signed-off-by: Scott Nichols <n3wscott@upbound.io>
2025-05-21 14:08:14 -07:00
Jared Watts 0002011004
Merge pull request #839 from jbw976/base-branch-bump
build: remove release-1.17 from renovate baseBranches
2025-05-21 11:47:18 +01:00
Jared Watts c3c1a2262b
build: remove release-1.17 from renovate baseBranches
Signed-off-by: Jared Watts <jbw976@gmail.com>
2025-05-21 11:38:53 +01:00
Nic Cope 0812f329d5
Merge pull request #835 from n3wscott/fix-connector
Correct Connector and Disconnector typos, add aliases to prevent breakage.
2025-05-19 16:17:03 -07:00
Carl Henrik Lunde 3178304167 Update Event recorder to not parse % format characters in error message
*Eventf takes a format string. This change passes "%s" as the format string
and then the unsanitized string as an argument.

Fixes #837

Signed-off-by: Carl Henrik Lunde <chlunde@ifi.uio.no>
2025-05-19 20:43:35 +02:00
Jared Watts 54effc73f9
Merge pull request #836 from jbw976/base-branch-bump
chore: add release-1.20 to renovate baseBranches
2025-05-13 19:47:19 +01:00
Jared Watts dd5c50cbf1
chore: add release-1.20 to renovate baseBranches
Signed-off-by: Jared Watts <jbw976@gmail.com>
2025-05-13 19:15:12 +01:00
Scott Nichols bbe80d8327 Move func alias from var to func wrapper.
Signed-off-by: Scott Nichols <n3wscott@upbound.io>
2025-05-13 08:59:35 -07:00
Scott Nichols 6ac64baa77 Correct Connector and Disconnector typos, add aliases to prevent breakage.
Signed-off-by: Scott Nichols <n3wscott@upbound.io>
2025-05-12 16:15:45 -07:00
John Gardiner Myers 0c53fc3e34 chore(deps): update module github.com/spf13/cobra to v1.9.1
Removes one reason the linker can't do dead-code elimination

Signed-off-by: John Gardiner Myers <jgmyers@proofpoint.com>
2025-03-29 10:34:44 -07:00
crossplane-renovate[bot] daf7264750
chore(deps): update actions/create-github-app-token digest to d72941d 2025-03-28 08:05:10 +00:00
crossplane-renovate[bot] 521fee3cbb
fix(deps): update module github.com/google/go-cmp to v0.7.0 2025-03-02 08:05:46 +00:00
crossplane-renovate[bot] a123ea42e5
chore(deps): update renovatebot/github-action action to v40.3.6 2025-03-02 08:04:20 +00:00
125 changed files with 9435 additions and 6057 deletions

View File

@ -14,9 +14,9 @@
// PLEASE UPDATE THIS WHEN RELEASING.
"baseBranches": [
'main',
'release-1.17',
'release-1.18',
'release-1.19',
'release-1.20',
],
"ignorePaths": [
"design/**",

View File

@ -33,13 +33,13 @@ jobs:
- name: Get token
id: get-github-app-token
uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
with:
app-id: ${{ secrets.RENOVATE_GITHUB_APP_ID }}
private-key: ${{ secrets.RENOVATE_GITHUB_APP_PRIVATE_KEY }}
- name: Self-hosted Renovate
uses: renovatebot/github-action@063e0c946b9c1af35ef3450efc44114925d6e8e6 # v40.1.11
uses: renovatebot/github-action@0984fb80fc633b17e57f3e8b6c007fe0dc3e0d62 # v40.3.6
env:
RENOVATE_REPOSITORIES: ${{ github.repository }}
# Use GitHub API to create commits

View File

@ -1,21 +1,13 @@
run:
timeout: 10m
version: "2"
output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
formats:
- format: colored-line-number
text:
path: stderr
linters:
enable-all: true
fast: false
default: all
disable:
# These linters are all deprecated. We disable them explicitly to avoid the
# linter logging deprecation warnings.
- tenv
# These are linters we'd like to enable, but that will be labor intensive to
# make existing code compliant.
- wrapcheck
@ -23,6 +15,7 @@ linters:
- 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.
@ -97,94 +90,11 @@ linters:
# numbers, but we should not be strict about it.
- mnd
linters-settings:
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
govet:
# report about shadowed variables
disable:
- shadow
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
gci:
custom-order: true
sections:
- standard
- default
- prefix(github.com/crossplane/crossplane-runtime)
- prefix(github.com/crossplane/crossplane)
- blank
- dot
dupl:
# tokens count to trigger issue, 150 by default
threshold: 100
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 5
lll:
# tab width in spaces. Default to 1.
tab-width: 1
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
exported-is-used: true
exported-fields-are-used: true
unparam:
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 30
prealloc:
# XXX: we don't recommend using this linter before doing performance profiling.
# For most programs usage of prealloc will be a premature optimization.
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# True by default.
simple: true
range-loops: true # Report preallocation suggestions on range loops, true by default
for-loops: false # Report preallocation suggestions on for loops, false by default
gocritic:
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks.
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
enabled-tags:
- performance
settings: # settings passed to gocritic
captLocal: # must be valid enabled check name
paramsOnly: true
rangeValCopy:
sizeThreshold: 32
nolintlint:
require-explanation: true
require-specific: true
# 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:
@ -193,117 +103,163 @@ linters-settings:
- $test
deny:
- pkg: github.com/stretchr/testify
desc: "See https://go.dev/wiki/TestComments#assert-libraries"
desc: See https://go.dev/wiki/TestComments#assert-libraries
- pkg: github.com/onsi/ginkgo
desc: "See https://go.dev/wiki/TestComments#assert-libraries"
desc: See https://go.dev/wiki/TestComments#assert-libraries
- pkg: github.com/onsi/gomega
desc: "See https://go.dev/wiki/TestComments#assert-libraries"
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
recvcheck:
unparam:
check-exported: false
unused:
exported-fields-are-used: true
exclusions:
- "*.DeepCopy" # DeepCopy* methods are generated and always use a pointer receiver, which may conflict with other methods for a given type.
- "*.DeepCopyInto"
issues:
# Excluding generated files.
exclude-files:
- "zz_generated\\..+\\.go$"
- ".+\\.pb.go$"
# Excluding configuration per-path and per-linter.
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test(ing)?\.go
linters:
- gocognit
generated: lax
rules:
- linters:
- containedctx
- errcheck
- forcetypeassert
- gochecknoglobals
- gochecknoinits
- gocognit
- gosec
- scopelint
- unparam
- gochecknoinits
- gochecknoglobals
- containedctx
- forcetypeassert
- embeddedstructfieldcheck
path: _test(ing)?\.go
# Ease some gocritic warnings on test files.
- path: _test\.go
text: "(unnamedResult|exitAfterDefer)"
linters:
- linters:
- gocritic
path: _test\.go
text: (unnamedResult|exitAfterDefer)
# It's idiomatic to register Kubernetes types with a package scoped
# SchemeBuilder using an init function.
- path: apis/
linters:
- gochecknoinits
- 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.
- text: "(hugeParam|rangeValCopy):"
linters:
- 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.
- text: "SA3000:"
linters:
- linters:
- staticcheck
- text: "k8s.io/api/core/v1"
linters:
- goimports
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.
- text: "G101:"
linters:
- linters:
- gosec
- gas
text: 'G101:'
# This is an 'errors unhandled' warning that duplicates errcheck.
- text: "G104:"
linters:
- linters:
- gosec
- gas
text: 'G104:'
# This is about implicit memory aliasing in a range loop.
# This is a false positive with Go v1.22 and above.
- text: "G601:"
linters:
- linters:
- gosec
- gas
text: 'G601:'
# Some k8s dependencies do not have JSON tags on all fields in structs.
- path: k8s.io/
linters:
- linters:
- musttag
path: k8s.io/
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: false
# 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.'
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
# 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
paths:
- zz_generated\..+\.go$
- .+\.pb.go$
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
new: false
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
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$

View File

@ -3,7 +3,7 @@ VERSION --try --raw-output 0.8
PROJECT crossplane/crossplane-runtime
ARG --global GO_VERSION=1.23.7
ARG --global GO_VERSION=1.24.4
# reviewable checks that a branch is ready for review. Run it before opening a
# pull request. It will catch a lot of the things our CI workflow will catch.
@ -102,7 +102,7 @@ go-test:
# go-lint lints Go code.
go-lint:
ARG GOLANGCI_LINT_VERSION=v1.64.8
ARG GOLANGCI_LINT_VERSION=v2.2.1
FROM +go-modules
# This cache is private because golangci-lint doesn't support concurrent runs.
CACHE --id go-lint --sharing private /root/.cache/golangci-lint

View File

@ -12,7 +12,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc-gen-go v1.36.5
// protoc (unknown)
// source: changelogs/proto/v1alpha1/changelog.proto
@ -27,6 +27,7 @@ import (
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@ -92,22 +93,19 @@ func (OperationType) EnumDescriptor() ([]byte, []int) {
// SendChangeLogRequest represents a request to send a single change log entry.
type SendChangeLogRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -117,7 +115,7 @@ func (*SendChangeLogRequest) ProtoMessage() {}
func (x *SendChangeLogRequest) ProtoReflect() protoreflect.Message {
mi := &file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -142,10 +140,7 @@ func (x *SendChangeLogRequest) GetEntry() *ChangeLogEntry {
// ChangeLogEntry represents a single change log entry, with detailed information
// about the resource that was changed.
type ChangeLogEntry struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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
@ -170,17 +165,17 @@ type ChangeLogEntry struct {
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,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
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{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -190,7 +185,7 @@ func (*ChangeLogEntry) ProtoMessage() {}
func (x *ChangeLogEntry) ProtoReflect() protoreflect.Message {
mi := &file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -279,19 +274,17 @@ func (x *ChangeLogEntry) GetAdditionalDetails() map[string]string {
// 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
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SendChangeLogResponse) Reset() {
*x = SendChangeLogResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -301,7 +294,7 @@ func (*SendChangeLogResponse) ProtoMessage() {}
func (x *SendChangeLogResponse) ProtoReflect() protoreflect.Message {
mi := &file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -318,7 +311,7 @@ func (*SendChangeLogResponse) Descriptor() ([]byte, []int) {
var File_changelogs_proto_v1alpha1_changelog_proto protoreflect.FileDescriptor
var file_changelogs_proto_v1alpha1_changelog_proto_rawDesc = []byte{
var file_changelogs_proto_v1alpha1_changelog_proto_rawDesc = string([]byte{
0x0a, 0x29, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x63, 0x68, 0x61, 0x6e,
0x67, 0x65, 0x6c, 0x6f, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x63, 0x68, 0x61,
@ -393,16 +386,16 @@ var file_changelogs_proto_v1alpha1_changelog_proto_rawDesc = []byte{
0x2d, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x63, 0x68,
0x61, 0x6e, 0x67, 0x65, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76,
0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
})
var (
file_changelogs_proto_v1alpha1_changelog_proto_rawDescOnce sync.Once
file_changelogs_proto_v1alpha1_changelog_proto_rawDescData = file_changelogs_proto_v1alpha1_changelog_proto_rawDesc
file_changelogs_proto_v1alpha1_changelog_proto_rawDescData []byte
)
func file_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP() []byte {
file_changelogs_proto_v1alpha1_changelog_proto_rawDescOnce.Do(func() {
file_changelogs_proto_v1alpha1_changelog_proto_rawDescData = protoimpl.X.CompressGZIP(file_changelogs_proto_v1alpha1_changelog_proto_rawDescData)
file_changelogs_proto_v1alpha1_changelog_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_changelogs_proto_v1alpha1_changelog_proto_rawDesc), len(file_changelogs_proto_v1alpha1_changelog_proto_rawDesc)))
})
return file_changelogs_proto_v1alpha1_changelog_proto_rawDescData
}
@ -438,50 +431,12 @@ func file_changelogs_proto_v1alpha1_changelog_proto_init() {
if File_changelogs_proto_v1alpha1_changelog_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*SendChangeLogRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*ChangeLogEntry); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*SendChangeLogResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_changelogs_proto_v1alpha1_changelog_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_changelogs_proto_v1alpha1_changelog_proto_rawDesc), len(file_changelogs_proto_v1alpha1_changelog_proto_rawDesc)),
NumEnums: 1,
NumMessages: 4,
NumExtensions: 0,
@ -493,7 +448,6 @@ func file_changelogs_proto_v1alpha1_changelog_proto_init() {
MessageInfos: file_changelogs_proto_v1alpha1_changelog_proto_msgTypes,
}.Build()
File_changelogs_proto_v1alpha1_changelog_proto = out.File
file_changelogs_proto_v1alpha1_changelog_proto_rawDesc = nil
file_changelogs_proto_v1alpha1_changelog_proto_goTypes = nil
file_changelogs_proto_v1alpha1_changelog_proto_depIdxs = nil
}

307
apis/common/condition.go Normal file
View File

@ -0,0 +1,307 @@
/*
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 common
import (
"sort"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// A ConditionType represents a condition a resource could be in.
type ConditionType string
// Condition types.
const (
// TypeReady resources are believed to be ready to handle work.
TypeReady ConditionType = "Ready"
// TypeSynced resources are believed to be in sync with the
// Kubernetes resources that manage their lifecycle.
TypeSynced ConditionType = "Synced"
// TypeHealthy resources are believed to be in a healthy state and to have all
// of their child resources in a healthy state. For example, a claim is
// healthy when the claim is synced and the underlying composite resource is
// both synced and healthy. A composite resource is healthy when the composite
// resource is synced and all composed resources are synced and, if
// applicable, healthy (e.g., the composed resource is a composite resource).
// TODO: This condition is not yet implemented. It is currently just reserved
// as a system condition. See the tracking issue for more details
// https://github.com/crossplane/crossplane/issues/5643.
TypeHealthy ConditionType = "Healthy"
)
// A ConditionReason represents the reason a resource is in a condition.
type ConditionReason string
// Reasons a resource is or is not ready.
const (
ReasonAvailable ConditionReason = "Available"
ReasonUnavailable ConditionReason = "Unavailable"
ReasonCreating ConditionReason = "Creating"
ReasonDeleting ConditionReason = "Deleting"
)
// Reasons a resource is or is not synced.
const (
ReasonReconcileSuccess ConditionReason = "ReconcileSuccess"
ReasonReconcileError ConditionReason = "ReconcileError"
ReasonReconcilePaused ConditionReason = "ReconcilePaused"
)
// See https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
// A Condition that may apply to a resource.
type Condition struct { //nolint:recvcheck // False positive - only has non-pointer methods AFAICT.
// Type of this condition. At most one of each condition type may apply to
// a resource at any point in time.
Type ConditionType `json:"type"`
// Status of this condition; is it currently True, False, or Unknown?
Status corev1.ConditionStatus `json:"status"`
// LastTransitionTime is the last time this condition transitioned from one
// status to another.
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
// A Reason for this condition's last transition from one status to another.
Reason ConditionReason `json:"reason"`
// A Message containing details about this condition's last transition from
// one status to another, if any.
// +optional
Message string `json:"message,omitempty"`
// ObservedGeneration represents the .metadata.generation that the condition was set based upon.
// For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
// with respect to the current state of the instance.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
// Equal returns true if the condition is identical to the supplied condition,
// ignoring the LastTransitionTime.
func (c Condition) Equal(other Condition) bool {
return c.Type == other.Type &&
c.Status == other.Status &&
c.Reason == other.Reason &&
c.Message == other.Message &&
c.ObservedGeneration == other.ObservedGeneration
}
// WithMessage returns a condition by adding the provided message to existing
// condition.
func (c Condition) WithMessage(msg string) Condition {
c.Message = msg
return c
}
// WithObservedGeneration returns a condition by adding the provided observed generation
// to existing condition.
func (c Condition) WithObservedGeneration(gen int64) Condition {
c.ObservedGeneration = gen
return c
}
// IsSystemConditionType returns true if the condition is owned by the
// Crossplane system (e.g, Ready, Synced, Healthy).
func IsSystemConditionType(t ConditionType) bool {
switch t {
case TypeReady, TypeSynced, TypeHealthy:
return true
}
return false
}
// NOTE(negz): Conditions are implemented as a slice rather than a map to comply
// with Kubernetes API conventions. Ideally we'd comply by using a map that
// marshalled to a JSON array, but doing so confuses the CRD schema generator.
// https://github.com/kubernetes/community/blob/9bf8cd/contributors/devel/sig-architecture/api-conventions.md#lists-of-named-subobjects-preferred-over-maps
// NOTE(negz): Do not manipulate Conditions directly. Use the Set method.
// A ConditionedStatus reflects the observed status of a resource. Only
// one condition of each type may exist.
type ConditionedStatus struct {
// Conditions of the resource.
// +listType=map
// +listMapKey=type
// +optional
Conditions []Condition `json:"conditions,omitempty"`
}
// NewConditionedStatus returns a stat with the supplied conditions set.
func NewConditionedStatus(c ...Condition) *ConditionedStatus {
s := &ConditionedStatus{}
s.SetConditions(c...)
return s
}
// GetCondition returns the condition for the given ConditionType if exists,
// otherwise returns nil.
func (s *ConditionedStatus) GetCondition(ct ConditionType) Condition {
for _, c := range s.Conditions {
if c.Type == ct {
return c
}
}
return Condition{Type: ct, Status: corev1.ConditionUnknown}
}
// SetConditions sets the supplied conditions, replacing any existing conditions
// of the same type. This is a no-op if all supplied conditions are identical,
// ignoring the last transition time, to those already set.
func (s *ConditionedStatus) SetConditions(c ...Condition) {
for _, cond := range c {
exists := false
for i, existing := range s.Conditions {
if existing.Type != cond.Type {
continue
}
if existing.Equal(cond) {
exists = true
continue
}
s.Conditions[i] = cond
exists = true
}
if !exists {
s.Conditions = append(s.Conditions, cond)
}
}
}
// Equal returns true if the status is identical to the supplied status,
// ignoring the LastTransitionTimes and order of statuses.
func (s *ConditionedStatus) Equal(other *ConditionedStatus) bool {
if s == nil || other == nil {
return s == nil && other == nil
}
if len(other.Conditions) != len(s.Conditions) {
return false
}
sc := make([]Condition, len(s.Conditions))
copy(sc, s.Conditions)
oc := make([]Condition, len(other.Conditions))
copy(oc, other.Conditions)
// We should not have more than one condition of each type.
sort.Slice(sc, func(i, j int) bool { return sc[i].Type < sc[j].Type })
sort.Slice(oc, func(i, j int) bool { return oc[i].Type < oc[j].Type })
for i := range sc {
if !sc[i].Equal(oc[i]) {
return false
}
}
return true
}
// Creating returns a condition that indicates the resource is currently
// being created.
func Creating() Condition {
return Condition{
Type: TypeReady,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonCreating,
}
}
// Deleting returns a condition that indicates the resource is currently
// being deleted.
func Deleting() Condition {
return Condition{
Type: TypeReady,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonDeleting,
}
}
// Available returns a condition that indicates the resource is
// currently observed to be available for use.
func Available() Condition {
return Condition{
Type: TypeReady,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: ReasonAvailable,
}
}
// Unavailable returns a condition that indicates the resource is not
// currently available for use. Unavailable should be set only when Crossplane
// expects the resource to be available but knows it is not, for example
// because its API reports it is unhealthy.
func Unavailable() Condition {
return Condition{
Type: TypeReady,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonUnavailable,
}
}
// ReconcileSuccess returns a condition indicating that Crossplane successfully
// completed the most recent reconciliation of the resource.
func ReconcileSuccess() Condition {
return Condition{
Type: TypeSynced,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: ReasonReconcileSuccess,
}
}
// ReconcileError returns a condition indicating that Crossplane encountered an
// error while reconciling the resource. This could mean Crossplane was
// unable to update the resource to reflect its desired state, or that
// Crossplane was unable to determine the current actual state of the resource.
func ReconcileError(err error) Condition {
return Condition{
Type: TypeSynced,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonReconcileError,
Message: err.Error(),
}
}
// ReconcilePaused returns a condition that indicates reconciliation on
// the managed resource is paused via the pause annotation.
func ReconcilePaused() Condition {
return Condition{
Type: TypeSynced,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonReconcilePaused,
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
package common
import (
"testing"

3
apis/common/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package common contains core API types used by most Crossplane resources.
// +kubebuilder:object:generate=true
package common

54
apis/common/merge.go Normal file
View File

@ -0,0 +1,54 @@
/*
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 common
import (
"dario.cat/mergo"
)
// 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
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
package common
import (
"reflect"
@ -33,12 +33,15 @@ func (arr mergoOptArr) names() []string {
for i, opt := range arr {
names[i] = runtime.FuncForPC(reflect.ValueOf(opt).Pointer()).Name()
}
sort.Strings(names)
return names
}
func TestMergoConfiguration(t *testing.T) {
valTrue := true
tests := map[string]struct {
mo *MergeOptions
want mergoOptArr

View File

@ -0,0 +1,37 @@
/*
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 common
// ObservedStatus contains the recent reconciliation stats.
type ObservedStatus struct {
// ObservedGeneration is the latest metadata.generation
// which resulted in either a ready state, or stalled due to error
// it can not recover from without human intervention.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
// SetObservedGeneration sets the generation of the main resource
// during the last reconciliation.
func (s *ObservedStatus) SetObservedGeneration(generation int64) {
s.ObservedGeneration = generation
}
// GetObservedGeneration returns the last observed generation of the main resource.
func (s *ObservedStatus) GetObservedGeneration() int64 {
return s.ObservedGeneration
}

120
apis/common/policies.go Normal file
View File

@ -0,0 +1,120 @@
/*
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 common
// ManagementPolicies determine how should Crossplane controllers manage an
// external resource through an array of ManagementActions.
type ManagementPolicies []ManagementAction
// A ManagementAction represents an action that the Crossplane controllers
// can take on an external resource.
// +kubebuilder:validation:Enum=Observe;Create;Update;Delete;LateInitialize;*
type ManagementAction string
const (
// ManagementActionObserve means that the managed resource status.atProvider
// will be updated with the external resource state.
ManagementActionObserve ManagementAction = "Observe"
// ManagementActionCreate means that the external resource will be created
// using the managed resource spec.initProvider and spec.forProvider.
ManagementActionCreate ManagementAction = "Create"
// ManagementActionUpdate means that the external resource will be updated
// using the managed resource spec.forProvider.
ManagementActionUpdate ManagementAction = "Update"
// ManagementActionDelete means that the external resource will be deleted
// when the managed resource is deleted.
ManagementActionDelete ManagementAction = "Delete"
// ManagementActionLateInitialize means that unspecified fields of the managed
// resource spec.forProvider will be updated with the external resource state.
ManagementActionLateInitialize ManagementAction = "LateInitialize"
// ManagementActionAll means that all of the above actions will be taken
// by the Crossplane controllers.
ManagementActionAll ManagementAction = "*"
)
// A DeletionPolicy determines what should happen to the underlying external
// resource when a managed resource is deleted.
// +kubebuilder:validation:Enum=Orphan;Delete
type DeletionPolicy string
const (
// DeletionOrphan means the external resource will be orphaned when its
// managed resource is deleted.
DeletionOrphan DeletionPolicy = "Orphan"
// DeletionDelete means both the external resource will be deleted when its
// managed resource is deleted.
DeletionDelete DeletionPolicy = "Delete"
)
// A CompositeDeletePolicy determines how the composite resource should be deleted
// when the corresponding claim is deleted.
// +kubebuilder:validation:Enum=Background;Foreground
type CompositeDeletePolicy string
const (
// CompositeDeleteBackground means the composite resource will be deleted using
// the Background Propagation Policy when the claim is deleted.
CompositeDeleteBackground CompositeDeletePolicy = "Background"
// CompositeDeleteForeground means the composite resource will be deleted using
// the Foreground Propagation Policy when the claim is deleted.
CompositeDeleteForeground CompositeDeletePolicy = "Foreground"
)
// An UpdatePolicy determines how something should be updated - either
// automatically (without human intervention) or manually.
// +kubebuilder:validation:Enum=Automatic;Manual
type UpdatePolicy string
const (
// UpdateAutomatic means the resource should be updated automatically,
// without any human intervention.
UpdateAutomatic UpdatePolicy = "Automatic"
// UpdateManual means the resource requires human intervention to
// update.
UpdateManual UpdatePolicy = "Manual"
)
// ResolvePolicy is a type for resolve policy.
type ResolvePolicy string
// ResolutionPolicy is a type for resolution policy.
type ResolutionPolicy string
const (
// ResolvePolicyAlways is a resolve option.
// When the ResolvePolicy is set to ResolvePolicyAlways the reference will
// be tried to resolve for every reconcile loop.
ResolvePolicyAlways ResolvePolicy = "Always"
// ResolutionPolicyRequired is a resolution option.
// When the ResolutionPolicy is set to ResolutionPolicyRequired the execution
// could not continue even if the reference cannot be resolved.
ResolutionPolicyRequired ResolutionPolicy = "Required"
// ResolutionPolicyOptional is a resolution option.
// When the ReferenceResolutionPolicy is set to ReferencePolicyOptional the
// execution could continue even if the reference cannot be resolved.
ResolutionPolicyOptional ResolutionPolicy = "Optional"
)

328
apis/common/resource.go Normal file
View File

@ -0,0 +1,328 @@
/*
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 common
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
)
const (
// ResourceCredentialsSecretEndpointKey is the key inside a connection secret for the connection endpoint.
ResourceCredentialsSecretEndpointKey = "endpoint"
// ResourceCredentialsSecretPortKey is the key inside a connection secret for the connection port.
ResourceCredentialsSecretPortKey = "port"
// ResourceCredentialsSecretUserKey is the key inside a connection secret for the connection user.
ResourceCredentialsSecretUserKey = "username"
// ResourceCredentialsSecretPasswordKey is the key inside a connection secret for the connection password.
ResourceCredentialsSecretPasswordKey = "password"
// ResourceCredentialsSecretCAKey is the key inside a connection secret for the server CA certificate.
ResourceCredentialsSecretCAKey = "clusterCA"
// ResourceCredentialsSecretClientCertKey is the key inside a connection secret for the client certificate.
ResourceCredentialsSecretClientCertKey = "clientCert"
// ResourceCredentialsSecretClientKeyKey is the key inside a connection secret for the client key.
ResourceCredentialsSecretClientKeyKey = "clientKey"
// ResourceCredentialsSecretTokenKey is the key inside a connection secret for the bearer token value.
ResourceCredentialsSecretTokenKey = "token"
// ResourceCredentialsSecretKubeconfigKey is the key inside a connection secret for the raw kubeconfig yaml.
ResourceCredentialsSecretKubeconfigKey = "kubeconfig"
)
// LabelKeyProviderKind is added to ProviderConfigUsages to relate them to their
// ProviderConfig.
const LabelKeyProviderKind = "crossplane.io/provider-config-kind"
// LabelKeyProviderName is added to ProviderConfigUsages to relate them to their
// ProviderConfig.
const LabelKeyProviderName = "crossplane.io/provider-config"
// NOTE(negz): The below secret references differ from ObjectReference and
// LocalObjectReference in that they include only the fields Crossplane needs to
// reference a secret, and make those fields required. This reduces ambiguity in
// the API for resource authors.
// A LocalSecretReference is a reference to a secret in the same namespace as
// the referencer.
type LocalSecretReference struct {
// Name of the secret.
Name string `json:"name"`
}
// A SecretReference is a reference to a secret in an arbitrary namespace.
type SecretReference struct {
// Name of the secret.
Name string `json:"name"`
// Namespace of the secret.
Namespace string `json:"namespace"`
}
// A SecretKeySelector is a reference to a secret key in an arbitrary namespace.
type SecretKeySelector struct {
SecretReference `json:",inline"`
// The key to select.
Key string `json:"key"`
}
// A LocalSecretKeySelector is a reference to a secret key
// in the same namespace with the referencing object.
type LocalSecretKeySelector struct {
LocalSecretReference `json:",inline"`
Key string `json:"key"`
}
// ToSecretKeySelector is a convenience method for converting the
// LocalSecretKeySelector to a SecretKeySelector with the given namespace.
func (ls *LocalSecretKeySelector) ToSecretKeySelector(namespace string) *SecretKeySelector {
return &SecretKeySelector{
SecretReference: SecretReference{
Name: ls.Name,
Namespace: namespace,
},
Key: ls.Key,
}
}
// Policy represents the Resolve and Resolution policies of Reference instance.
type Policy struct {
// Resolve specifies when this reference should be resolved. The default
// is 'IfNotPresent', which will attempt to resolve the reference only when
// the corresponding field is not present. Use 'Always' to resolve the
// reference on every reconcile.
// +optional
// +kubebuilder:validation:Enum=Always;IfNotPresent
Resolve *ResolvePolicy `json:"resolve,omitempty"`
// Resolution specifies whether resolution of this reference is required.
// The default is 'Required', which means the reconcile will fail if the
// reference cannot be resolved. 'Optional' means this reference will be
// a no-op if it cannot be resolved.
// +optional
// +kubebuilder:default=Required
// +kubebuilder:validation:Enum=Required;Optional
Resolution *ResolutionPolicy `json:"resolution,omitempty"`
}
// IsResolutionPolicyOptional checks whether the resolution policy of relevant reference is Optional.
func (p *Policy) IsResolutionPolicyOptional() bool {
if p == nil || p.Resolution == nil {
return false
}
return *p.Resolution == ResolutionPolicyOptional
}
// IsResolvePolicyAlways checks whether the resolution policy of relevant reference is Always.
func (p *Policy) IsResolvePolicyAlways() bool {
if p == nil || p.Resolve == nil {
return false
}
return *p.Resolve == ResolvePolicyAlways
}
// A Reference to a named object.
type Reference struct {
// Name of the referenced object.
Name string `json:"name"`
// Policies for referencing.
// +optional
Policy *Policy `json:"policy,omitempty"`
}
// A NamespacedReference to a named object.
type NamespacedReference struct {
// Name of the referenced object.
Name string `json:"name"`
// Namespace of the referenced object
// +optional
Namespace string `json:"namespace,omitempty"`
// Policies for referencing.
// +optional
Policy *Policy `json:"policy,omitempty"`
}
// A TypedReference refers to an object by Name, Kind, and APIVersion. It is
// commonly used to reference cluster-scoped objects or objects where the
// namespace is already known.
type TypedReference struct {
// APIVersion of the referenced object.
APIVersion string `json:"apiVersion"`
// Kind of the referenced object.
Kind string `json:"kind"`
// Name of the referenced object.
Name string `json:"name"`
// UID of the referenced object.
// +optional
UID types.UID `json:"uid,omitempty"`
}
// A Selector selects an object.
type Selector struct {
// MatchLabels ensures an object with matching labels is selected.
MatchLabels map[string]string `json:"matchLabels,omitempty"`
// MatchControllerRef ensures an object with the same controller reference
// as the selecting object is selected.
MatchControllerRef *bool `json:"matchControllerRef,omitempty"`
// Policies for selection.
// +optional
Policy *Policy `json:"policy,omitempty"`
}
// NamespacedSelector selects a namespaced object.
type NamespacedSelector struct {
// MatchLabels ensures an object with matching labels is selected.
MatchLabels map[string]string `json:"matchLabels,omitempty"`
// MatchControllerRef ensures an object with the same controller reference
// as the selecting object is selected.
MatchControllerRef *bool `json:"matchControllerRef,omitempty"`
// Policies for selection.
// +optional
Policy *Policy `json:"policy,omitempty"`
// Namespace for the selector
// +optional
Namespace string `json:"namespace,omitempty"`
}
// ProviderConfigReference is a typed reference to a ProviderConfig
// object, with a known api group.
type ProviderConfigReference struct {
// Kind of the referenced object.
Kind string `json:"kind"`
// Name of the referenced object.
Name string `json:"name"`
}
// SetGroupVersionKind sets the Kind and APIVersion of a TypedReference.
func (obj *TypedReference) SetGroupVersionKind(gvk schema.GroupVersionKind) {
obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind()
}
// GroupVersionKind gets the GroupVersionKind of a TypedReference.
func (obj *TypedReference) GroupVersionKind() schema.GroupVersionKind {
return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind)
}
// GetObjectKind get the ObjectKind of a TypedReference.
func (obj *TypedReference) GetObjectKind() schema.ObjectKind { return obj }
// ResourceStatus represents the observed state of a managed resource.
type ResourceStatus struct {
ConditionedStatus `json:",inline"`
ObservedStatus `json:",inline"`
}
// A CredentialsSource is a source from which provider credentials may be
// acquired.
type CredentialsSource string
const (
// CredentialsSourceNone indicates that a provider does not require
// credentials.
CredentialsSourceNone CredentialsSource = "None"
// CredentialsSourceSecret indicates that a provider should acquire
// credentials from a secret.
CredentialsSourceSecret CredentialsSource = "Secret"
// CredentialsSourceInjectedIdentity indicates that a provider should use
// credentials via its (pod's) identity; i.e. via IRSA for AWS,
// Workload Identity for GCP, Pod Identity for Azure, or in-cluster
// authentication for the Kubernetes API.
CredentialsSourceInjectedIdentity CredentialsSource = "InjectedIdentity"
// CredentialsSourceEnvironment indicates that a provider should acquire
// credentials from an environment variable.
CredentialsSourceEnvironment CredentialsSource = "Environment"
// CredentialsSourceFilesystem indicates that a provider should acquire
// credentials from the filesystem.
CredentialsSourceFilesystem CredentialsSource = "Filesystem"
)
// CommonCredentialSelectors provides common selectors for extracting
// credentials.
//
//nolint:revive // preserve backward-compatibility
type CommonCredentialSelectors struct {
// Fs is a reference to a filesystem location that contains credentials that
// must be used to connect to the provider.
// +optional
Fs *FsSelector `json:"fs,omitempty"`
// Env is a reference to an environment variable that contains credentials
// that must be used to connect to the provider.
// +optional
Env *EnvSelector `json:"env,omitempty"`
// A SecretRef is a reference to a secret key that contains the credentials
// that must be used to connect to the provider.
// +optional
SecretRef *SecretKeySelector `json:"secretRef,omitempty"`
}
// EnvSelector selects an environment variable.
type EnvSelector struct {
// Name is the name of an environment variable.
Name string `json:"name"`
}
// FsSelector selects a filesystem location.
type FsSelector struct {
// Path is a filesystem path.
Path string `json:"path"`
}
// A ProviderConfigStatus defines the observed status of a ProviderConfig.
type ProviderConfigStatus struct {
ConditionedStatus `json:",inline"`
// Users of this provider configuration.
Users int64 `json:"users,omitempty"`
}
// A ProviderConfigUsage is a record that a particular managed resource is using
// a particular provider configuration.
type ProviderConfigUsage struct {
// ProviderConfigReference to the provider config being used.
ProviderConfigReference Reference `json:"providerConfigRef"`
// ResourceReference to the managed resource using the provider config.
ResourceReference TypedReference `json:"resourceRef"`
}
// A TypedProviderConfigUsage is a record that a particular managed resource is using
// a particular provider configuration.
type TypedProviderConfigUsage struct {
// ProviderConfigReference to the provider config being used.
ProviderConfigReference ProviderConfigReference `json:"providerConfigRef"`
// ResourceReference to the managed resource using the provider config.
ResourceReference TypedReference `json:"resourceRef"`
}

View File

@ -17,23 +17,20 @@ limitations under the License.
package v1
import (
"sort"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/crossplane/crossplane-runtime/apis/common"
)
// A ConditionType represents a condition a resource could be in.
type ConditionType string
type ConditionType = common.ConditionType
// Condition types.
const (
// TypeReady resources are believed to be ready to handle work.
TypeReady ConditionType = "Ready"
TypeReady ConditionType = common.TypeReady
// TypeSynced resources are believed to be in sync with the
// Kubernetes resources that manage their lifecycle.
TypeSynced ConditionType = "Synced"
TypeSynced ConditionType = common.TypeSynced
// TypeHealthy resources are believed to be in a healthy state and to have all
// of their child resources in a healthy state. For example, a claim is
@ -44,89 +41,36 @@ const (
// TODO: This condition is not yet implemented. It is currently just reserved
// as a system condition. See the tracking issue for more details
// https://github.com/crossplane/crossplane/issues/5643.
TypeHealthy ConditionType = "Healthy"
TypeHealthy ConditionType = common.TypeHealthy
)
// A ConditionReason represents the reason a resource is in a condition.
type ConditionReason string
type ConditionReason = common.ConditionReason
// Reasons a resource is or is not ready.
const (
ReasonAvailable ConditionReason = "Available"
ReasonUnavailable ConditionReason = "Unavailable"
ReasonCreating ConditionReason = "Creating"
ReasonDeleting ConditionReason = "Deleting"
ReasonAvailable = common.ReasonAvailable
ReasonUnavailable = common.ReasonUnavailable
ReasonCreating = common.ReasonCreating
ReasonDeleting = common.ReasonDeleting
)
// Reasons a resource is or is not synced.
const (
ReasonReconcileSuccess ConditionReason = "ReconcileSuccess"
ReasonReconcileError ConditionReason = "ReconcileError"
ReasonReconcilePaused ConditionReason = "ReconcilePaused"
ReasonReconcileSuccess = common.ReasonReconcileSuccess
ReasonReconcileError = common.ReasonReconcileError
ReasonReconcilePaused = common.ReasonReconcilePaused
)
// See https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
// A Condition that may apply to a resource.
type Condition struct {
// Type of this condition. At most one of each condition type may apply to
// a resource at any point in time.
Type ConditionType `json:"type"`
// Status of this condition; is it currently True, False, or Unknown?
Status corev1.ConditionStatus `json:"status"`
// LastTransitionTime is the last time this condition transitioned from one
// status to another.
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
// A Reason for this condition's last transition from one status to another.
Reason ConditionReason `json:"reason"`
// A Message containing details about this condition's last transition from
// one status to another, if any.
// +optional
Message string `json:"message,omitempty"`
// ObservedGeneration represents the .metadata.generation that the condition was set based upon.
// For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
// with respect to the current state of the instance.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
// Equal returns true if the condition is identical to the supplied condition,
// ignoring the LastTransitionTime.
func (c Condition) Equal(other Condition) bool {
return c.Type == other.Type &&
c.Status == other.Status &&
c.Reason == other.Reason &&
c.Message == other.Message &&
c.ObservedGeneration == other.ObservedGeneration
}
// WithMessage returns a condition by adding the provided message to existing
// condition.
func (c Condition) WithMessage(msg string) Condition {
c.Message = msg
return c
}
// WithObservedGeneration returns a condition by adding the provided observed generation
// to existing condition.
func (c Condition) WithObservedGeneration(gen int64) Condition {
c.ObservedGeneration = gen
return c
}
type Condition = common.Condition
// IsSystemConditionType returns true if the condition is owned by the
// Crossplane system (e.g, Ready, Synced, Healthy).
func IsSystemConditionType(t ConditionType) bool {
switch t {
case TypeReady, TypeSynced, TypeHealthy:
return true
}
return false
return common.IsSystemConditionType(t)
}
// NOTE(negz): Conditions are implemented as a slice rather than a map to comply
@ -138,119 +82,29 @@ func IsSystemConditionType(t ConditionType) bool {
// A ConditionedStatus reflects the observed status of a resource. Only
// one condition of each type may exist.
type ConditionedStatus struct {
// Conditions of the resource.
// +listType=map
// +listMapKey=type
// +optional
Conditions []Condition `json:"conditions,omitempty"`
}
type ConditionedStatus = common.ConditionedStatus
// NewConditionedStatus returns a stat with the supplied conditions set.
func NewConditionedStatus(c ...Condition) *ConditionedStatus {
s := &ConditionedStatus{}
s.SetConditions(c...)
return s
}
// GetCondition returns the condition for the given ConditionType if exists,
// otherwise returns nil.
func (s *ConditionedStatus) GetCondition(ct ConditionType) Condition {
for _, c := range s.Conditions {
if c.Type == ct {
return c
}
}
return Condition{Type: ct, Status: corev1.ConditionUnknown}
}
// SetConditions sets the supplied conditions, replacing any existing conditions
// of the same type. This is a no-op if all supplied conditions are identical,
// ignoring the last transition time, to those already set.
func (s *ConditionedStatus) SetConditions(c ...Condition) {
for _, cond := range c {
exists := false
for i, existing := range s.Conditions {
if existing.Type != cond.Type {
continue
}
if existing.Equal(cond) {
exists = true
continue
}
s.Conditions[i] = cond
exists = true
}
if !exists {
s.Conditions = append(s.Conditions, cond)
}
}
}
// Equal returns true if the status is identical to the supplied status,
// ignoring the LastTransitionTimes and order of statuses.
func (s *ConditionedStatus) Equal(other *ConditionedStatus) bool {
if s == nil || other == nil {
return s == nil && other == nil
}
if len(other.Conditions) != len(s.Conditions) {
return false
}
sc := make([]Condition, len(s.Conditions))
copy(sc, s.Conditions)
oc := make([]Condition, len(other.Conditions))
copy(oc, other.Conditions)
// We should not have more than one condition of each type.
sort.Slice(sc, func(i, j int) bool { return sc[i].Type < sc[j].Type })
sort.Slice(oc, func(i, j int) bool { return oc[i].Type < oc[j].Type })
for i := range sc {
if !sc[i].Equal(oc[i]) {
return false
}
}
return true
return common.NewConditionedStatus(c...)
}
// Creating returns a condition that indicates the resource is currently
// being created.
func Creating() Condition {
return Condition{
Type: TypeReady,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonCreating,
}
return common.Creating()
}
// Deleting returns a condition that indicates the resource is currently
// being deleted.
func Deleting() Condition {
return Condition{
Type: TypeReady,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonDeleting,
}
return common.Deleting()
}
// Available returns a condition that indicates the resource is
// currently observed to be available for use.
func Available() Condition {
return Condition{
Type: TypeReady,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: ReasonAvailable,
}
return common.Available()
}
// Unavailable returns a condition that indicates the resource is not
@ -258,23 +112,13 @@ func Available() Condition {
// expects the resource to be available but knows it is not, for example
// because its API reports it is unhealthy.
func Unavailable() Condition {
return Condition{
Type: TypeReady,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonUnavailable,
}
return common.Unavailable()
}
// ReconcileSuccess returns a condition indicating that Crossplane successfully
// completed the most recent reconciliation of the resource.
func ReconcileSuccess() Condition {
return Condition{
Type: TypeSynced,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: ReasonReconcileSuccess,
}
return common.ReconcileSuccess()
}
// ReconcileError returns a condition indicating that Crossplane encountered an
@ -282,22 +126,11 @@ func ReconcileSuccess() Condition {
// unable to update the resource to reflect its desired state, or that
// Crossplane was unable to determine the current actual state of the resource.
func ReconcileError(err error) Condition {
return Condition{
Type: TypeSynced,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonReconcileError,
Message: err.Error(),
}
return common.ReconcileError(err)
}
// ReconcilePaused returns a condition that indicates reconciliation on
// the managed resource is paused via the pause annotation.
func ReconcilePaused() Condition {
return Condition{
Type: TypeSynced,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonReconcilePaused,
}
return common.ReconcilePaused()
}

View File

@ -1,165 +0,0 @@
/*
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 v1
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
)
const (
// LabelKeyOwnerUID is the UID of the owner resource of a connection secret.
// Kubernetes provides owner/controller references to track ownership of
// resources including secrets, however, this would only work for in cluster
// k8s secrets. We opted to use a label for this purpose to be consistent
// across Secret Store implementations and expect all to support
// setting/getting labels.
LabelKeyOwnerUID = "secret.crossplane.io/owner-uid"
)
// PublishConnectionDetailsTo represents configuration of a connection secret.
type PublishConnectionDetailsTo struct {
// Name is the name of the connection secret.
Name string `json:"name"`
// Metadata is the metadata for connection secret.
// +optional
Metadata *ConnectionSecretMetadata `json:"metadata,omitempty"`
// SecretStoreConfigRef specifies which secret store config should be used
// for this ConnectionSecret.
// +optional
// +kubebuilder:default={"name": "default"}
SecretStoreConfigRef *Reference `json:"configRef,omitempty"`
}
// ConnectionSecretMetadata represents metadata of a connection secret.
// Labels are used to track ownership of connection secrets and has to be
// supported for any secret store implementation.
type ConnectionSecretMetadata struct {
// Labels are the labels/tags to be added to connection secret.
// - For Kubernetes secrets, this will be used as "metadata.labels".
// - It is up to Secret Store implementation for others store types.
// +optional
Labels map[string]string `json:"labels,omitempty"`
// Annotations are the annotations to be added to connection secret.
// - For Kubernetes secrets, this will be used as "metadata.annotations".
// - It is up to Secret Store implementation for others store types.
// +optional
Annotations map[string]string `json:"annotations,omitempty"`
// Type is the SecretType for the connection secret.
// - Only valid for Kubernetes Secret Stores.
// +optional
Type *corev1.SecretType `json:"type,omitempty"`
}
// SetOwnerUID sets owner object uid label.
func (in *ConnectionSecretMetadata) SetOwnerUID(uid types.UID) {
if in.Labels == nil {
in.Labels = map[string]string{}
}
in.Labels[LabelKeyOwnerUID] = string(uid)
}
// GetOwnerUID gets owner object uid.
func (in *ConnectionSecretMetadata) GetOwnerUID() string {
if u, ok := in.Labels[LabelKeyOwnerUID]; ok {
return u
}
return ""
}
// SecretStoreType represents a secret store type.
// +kubebuilder:validation:Enum=Kubernetes;Vault;Plugin
type SecretStoreType string
const (
// SecretStoreKubernetes indicates that secret store type is
// Kubernetes. In other words, connection secrets will be stored as K8s
// Secrets.
SecretStoreKubernetes SecretStoreType = "Kubernetes"
// SecretStorePlugin indicates that secret store type is Plugin and will be used with external secret stores.
SecretStorePlugin SecretStoreType = "Plugin"
)
// SecretStoreConfig represents configuration of a Secret Store.
type SecretStoreConfig struct {
// Type configures which secret store to be used. Only the configuration
// block for this store will be used and others will be ignored if provided.
// Default is Kubernetes.
// +optional
// +kubebuilder:default=Kubernetes
Type *SecretStoreType `json:"type,omitempty"`
// DefaultScope used for scoping secrets for "cluster-scoped" resources.
// If store type is "Kubernetes", this would mean the default namespace to
// store connection secrets for cluster scoped resources.
// In case of "Vault", this would be used as the default parent path.
// Typically, should be set as Crossplane installation namespace.
DefaultScope string `json:"defaultScope"`
// Kubernetes configures a Kubernetes secret store.
// If the "type" is "Kubernetes" but no config provided, in cluster config
// will be used.
// +optional
Kubernetes *KubernetesSecretStoreConfig `json:"kubernetes,omitempty"`
// Plugin configures External secret store as a plugin.
// +optional
Plugin *PluginStoreConfig `json:"plugin,omitempty"`
}
// PluginStoreConfig represents configuration of an External Secret Store.
type PluginStoreConfig struct {
// Endpoint is the endpoint of the gRPC server.
Endpoint string `json:"endpoint,omitempty"`
// ConfigRef contains store config reference info.
ConfigRef Config `json:"configRef,omitempty"`
}
// Config contains store config reference info.
type Config struct {
// APIVersion of the referenced config.
APIVersion string `json:"apiVersion"`
// Kind of the referenced config.
Kind string `json:"kind"`
// Name of the referenced config.
Name string `json:"name"`
}
// KubernetesAuthConfig required to authenticate to a K8s API. It expects
// a "kubeconfig" file to be provided.
type KubernetesAuthConfig struct {
// Source of the credentials.
// +kubebuilder:validation:Enum=None;Secret;Environment;Filesystem
Source CredentialsSource `json:"source"`
// CommonCredentialSelectors provides common selectors for extracting
// credentials.
CommonCredentialSelectors `json:",inline"`
}
// KubernetesSecretStoreConfig represents the required configuration
// for a Kubernetes secret store.
type KubernetesSecretStoreConfig struct {
// Credentials used to connect to the Kubernetes API.
Auth KubernetesAuthConfig `json:"auth"`
// TODO(turkenh): Support additional identities like
// https://github.com/crossplane-contrib/provider-kubernetes/blob/4d722ef914e6964e80e190317daca9872ae98738/apis/v1alpha1/types.go#L34
}

View File

@ -17,36 +17,8 @@ limitations under the License.
package v1
import (
"dario.cat/mergo"
"github.com/crossplane/crossplane-runtime/apis/common"
)
// 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
}
type MergeOptions = common.MergeOptions

View File

@ -16,22 +16,9 @@ limitations under the License.
package v1
import (
"github.com/crossplane/crossplane-runtime/apis/common"
)
// ObservedStatus contains the recent reconciliation stats.
type ObservedStatus struct {
// ObservedGeneration is the latest metadata.generation
// which resulted in either a ready state, or stalled due to error
// it can not recover from without human intervention.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
// SetObservedGeneration sets the generation of the main resource
// during the last reconciliation.
func (s *ObservedStatus) SetObservedGeneration(generation int64) {
s.ObservedGeneration = generation
}
// GetObservedGeneration returns the last observed generation of the main resource.
func (s *ObservedStatus) GetObservedGeneration() int64 {
return s.ObservedGeneration
}
type ObservedStatus = common.ObservedStatus

View File

@ -16,39 +16,42 @@ limitations under the License.
package v1
import (
"github.com/crossplane/crossplane-runtime/apis/common"
)
// ManagementPolicies determine how should Crossplane controllers manage an
// external resource through an array of ManagementActions.
type ManagementPolicies []ManagementAction
type ManagementPolicies = common.ManagementPolicies
// A ManagementAction represents an action that the Crossplane controllers
// can take on an external resource.
// +kubebuilder:validation:Enum=Observe;Create;Update;Delete;LateInitialize;*
type ManagementAction string
type ManagementAction = common.ManagementAction
const (
// ManagementActionObserve means that the managed resource status.atProvider
// will be updated with the external resource state.
ManagementActionObserve ManagementAction = "Observe"
ManagementActionObserve = common.ManagementActionObserve
// ManagementActionCreate means that the external resource will be created
// using the managed resource spec.initProvider and spec.forProvider.
ManagementActionCreate ManagementAction = "Create"
ManagementActionCreate = common.ManagementActionCreate
// ManagementActionUpdate means that the external resource will be updated
// using the managed resource spec.forProvider.
ManagementActionUpdate ManagementAction = "Update"
ManagementActionUpdate = common.ManagementActionUpdate
// ManagementActionDelete means that the external resource will be deleted
// when the managed resource is deleted.
ManagementActionDelete ManagementAction = "Delete"
ManagementActionDelete = common.ManagementActionDelete
// ManagementActionLateInitialize means that unspecified fields of the managed
// resource spec.forProvider will be updated with the external resource state.
ManagementActionLateInitialize ManagementAction = "LateInitialize"
ManagementActionLateInitialize = common.ManagementActionLateInitialize
// ManagementActionAll means that all of the above actions will be taken
// by the Crossplane controllers.
ManagementActionAll ManagementAction = "*"
ManagementActionAll = common.ManagementActionAll
)
// A DeletionPolicy determines what should happen to the underlying external
@ -68,53 +71,51 @@ const (
// A CompositeDeletePolicy determines how the composite resource should be deleted
// when the corresponding claim is deleted.
// +kubebuilder:validation:Enum=Background;Foreground
type CompositeDeletePolicy string
type CompositeDeletePolicy = common.CompositeDeletePolicy
const (
// CompositeDeleteBackground means the composite resource will be deleted using
// the Background Propagation Policy when the claim is deleted.
CompositeDeleteBackground CompositeDeletePolicy = "Background"
CompositeDeleteBackground = common.CompositeDeleteBackground
// CompositeDeleteForeground means the composite resource will be deleted using
// the Foreground Propagation Policy when the claim is deleted.
CompositeDeleteForeground CompositeDeletePolicy = "Foreground"
CompositeDeleteForeground = common.CompositeDeleteForeground
)
// An UpdatePolicy determines how something should be updated - either
// automatically (without human intervention) or manually.
// +kubebuilder:validation:Enum=Automatic;Manual
type UpdatePolicy string
type UpdatePolicy = common.UpdatePolicy
const (
// UpdateAutomatic means the resource should be updated automatically,
// without any human intervention.
UpdateAutomatic UpdatePolicy = "Automatic"
UpdateAutomatic = common.UpdateAutomatic
// UpdateManual means the resource requires human intervention to
// update.
UpdateManual UpdatePolicy = "Manual"
UpdateManual = common.UpdateManual
)
// ResolvePolicy is a type for resolve policy.
type ResolvePolicy string
type ResolvePolicy = common.ResolvePolicy
// ResolutionPolicy is a type for resolution policy.
type ResolutionPolicy string
type ResolutionPolicy = common.ResolutionPolicy
const (
// ResolvePolicyAlways is a resolve option.
// When the ResolvePolicy is set to ResolvePolicyAlways the reference will
// be tried to resolve for every reconcile loop.
ResolvePolicyAlways ResolvePolicy = "Always"
ResolvePolicyAlways = common.ResolvePolicyAlways
// ResolutionPolicyRequired is a resolution option.
// When the ResolutionPolicy is set to ResolutionPolicyRequired the execution
// could not continue even if the reference cannot be resolved.
ResolutionPolicyRequired ResolutionPolicy = "Required"
ResolutionPolicyRequired = common.ResolutionPolicyRequired
// ResolutionPolicyOptional is a resolution option.
// When the ReferenceResolutionPolicy is set to ReferencePolicyOptional the
// execution could continue even if the reference cannot be resolved.
ResolutionPolicyOptional ResolutionPolicy = "Optional"
ResolutionPolicyOptional = common.ResolutionPolicyOptional
)

View File

@ -18,8 +18,8 @@ package v1
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"github.com/crossplane/crossplane-runtime/apis/common"
)
const (
@ -43,9 +43,13 @@ const (
ResourceCredentialsSecretKubeconfigKey = "kubeconfig"
)
// LabelKeyProviderKind is added to ProviderConfigUsages to relate them to their
// ProviderConfig.
const LabelKeyProviderKind = common.LabelKeyProviderKind
// LabelKeyProviderName is added to ProviderConfigUsages to relate them to their
// ProviderConfig.
const LabelKeyProviderName = "crossplane.io/provider-config"
const LabelKeyProviderName = common.LabelKeyProviderName
// NOTE(negz): The below secret references differ from ObjectReference and
// LocalObjectReference in that they include only the fields Crossplane needs to
@ -54,118 +58,41 @@ const LabelKeyProviderName = "crossplane.io/provider-config"
// A LocalSecretReference is a reference to a secret in the same namespace as
// the referencer.
type LocalSecretReference struct {
// Name of the secret.
Name string `json:"name"`
}
type LocalSecretReference = common.LocalSecretReference
// A SecretReference is a reference to a secret in an arbitrary namespace.
type SecretReference struct {
// Name of the secret.
Name string `json:"name"`
// Namespace of the secret.
Namespace string `json:"namespace"`
}
type SecretReference = common.SecretReference
// A SecretKeySelector is a reference to a secret key in an arbitrary namespace.
type SecretKeySelector struct {
SecretReference `json:",inline"`
type SecretKeySelector = common.SecretKeySelector
// The key to select.
Key string `json:"key"`
}
// A LocalSecretKeySelector is a reference to a secret key
// in the same namespace with the referencing object.
type LocalSecretKeySelector = common.LocalSecretKeySelector
// Policy represents the Resolve and Resolution policies of Reference instance.
type Policy struct {
// Resolve specifies when this reference should be resolved. The default
// is 'IfNotPresent', which will attempt to resolve the reference only when
// the corresponding field is not present. Use 'Always' to resolve the
// reference on every reconcile.
// +optional
// +kubebuilder:validation:Enum=Always;IfNotPresent
Resolve *ResolvePolicy `json:"resolve,omitempty"`
// Resolution specifies whether resolution of this reference is required.
// The default is 'Required', which means the reconcile will fail if the
// reference cannot be resolved. 'Optional' means this reference will be
// a no-op if it cannot be resolved.
// +optional
// +kubebuilder:default=Required
// +kubebuilder:validation:Enum=Required;Optional
Resolution *ResolutionPolicy `json:"resolution,omitempty"`
}
// IsResolutionPolicyOptional checks whether the resolution policy of relevant reference is Optional.
func (p *Policy) IsResolutionPolicyOptional() bool {
if p == nil || p.Resolution == nil {
return false
}
return *p.Resolution == ResolutionPolicyOptional
}
// IsResolvePolicyAlways checks whether the resolution policy of relevant reference is Always.
func (p *Policy) IsResolvePolicyAlways() bool {
if p == nil || p.Resolve == nil {
return false
}
return *p.Resolve == ResolvePolicyAlways
}
type Policy = common.Policy
// A Reference to a named object.
type Reference struct {
// Name of the referenced object.
Name string `json:"name"`
type Reference = common.Reference
// Policies for referencing.
// +optional
Policy *Policy `json:"policy,omitempty"`
}
// A NamespacedReference to a named object.
type NamespacedReference = common.NamespacedReference
// A TypedReference refers to an object by Name, Kind, and APIVersion. It is
// commonly used to reference cluster-scoped objects or objects where the
// namespace is already known.
type TypedReference struct {
// APIVersion of the referenced object.
APIVersion string `json:"apiVersion"`
// Kind of the referenced object.
Kind string `json:"kind"`
// Name of the referenced object.
Name string `json:"name"`
// UID of the referenced object.
// +optional
UID types.UID `json:"uid,omitempty"`
}
type TypedReference = common.TypedReference
// A Selector selects an object.
type Selector struct {
// MatchLabels ensures an object with matching labels is selected.
MatchLabels map[string]string `json:"matchLabels,omitempty"`
type Selector = common.Selector
// MatchControllerRef ensures an object with the same controller reference
// as the selecting object is selected.
MatchControllerRef *bool `json:"matchControllerRef,omitempty"`
// NamespacedSelector selects a namespaced object.
type NamespacedSelector = common.NamespacedSelector
// Policies for selection.
// +optional
Policy *Policy `json:"policy,omitempty"`
}
// SetGroupVersionKind sets the Kind and APIVersion of a TypedReference.
func (obj *TypedReference) SetGroupVersionKind(gvk schema.GroupVersionKind) {
obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind()
}
// GroupVersionKind gets the GroupVersionKind of a TypedReference.
func (obj *TypedReference) GroupVersionKind() schema.GroupVersionKind {
return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind)
}
// GetObjectKind get the ObjectKind of a TypedReference.
func (obj *TypedReference) GetObjectKind() schema.ObjectKind { return obj }
// ProviderConfigReference is a typed reference to a ProviderConfig
// object, with a known api group.
type ProviderConfigReference = common.ProviderConfigReference
// TODO(negz): Rename Resource* to Managed* to clarify that they enable the
// resource.Managed interface.
@ -176,21 +103,9 @@ type ResourceSpec struct {
// Secret to which any connection details for this managed resource should
// be written. Connection details frequently include the endpoint, username,
// and password required to connect to the managed resource.
// This field is planned to be replaced in a future release in favor of
// PublishConnectionDetailsTo. Currently, both could be set independently
// and connection details would be published to both without affecting
// each other.
// +optional
WriteConnectionSecretToReference *SecretReference `json:"writeConnectionSecretToRef,omitempty"`
// PublishConnectionDetailsTo specifies the connection secret config which
// contains a name, metadata and a reference to secret store config to
// which any connection details for this managed resource should be written.
// Connection details frequently include the endpoint, username,
// and password required to connect to the managed resource.
// +optional
PublishConnectionDetailsTo *PublishConnectionDetailsTo `json:"publishConnectionDetailsTo,omitempty"`
// ProviderConfigReference specifies how the provider that will be used to
// create, observe, update, and delete this managed resource should be
// configured.
@ -231,80 +146,48 @@ type ResourceStatus struct {
// A CredentialsSource is a source from which provider credentials may be
// acquired.
type CredentialsSource string
type CredentialsSource = common.CredentialsSource
const (
// CredentialsSourceNone indicates that a provider does not require
// credentials.
CredentialsSourceNone CredentialsSource = "None"
CredentialsSourceNone = common.CredentialsSourceNone
// CredentialsSourceSecret indicates that a provider should acquire
// credentials from a secret.
CredentialsSourceSecret CredentialsSource = "Secret"
CredentialsSourceSecret = common.CredentialsSourceSecret
// CredentialsSourceInjectedIdentity indicates that a provider should use
// credentials via its (pod's) identity; i.e. via IRSA for AWS,
// Workload Identity for GCP, Pod Identity for Azure, or in-cluster
// authentication for the Kubernetes API.
CredentialsSourceInjectedIdentity CredentialsSource = "InjectedIdentity"
CredentialsSourceInjectedIdentity = common.CredentialsSourceInjectedIdentity
// CredentialsSourceEnvironment indicates that a provider should acquire
// credentials from an environment variable.
CredentialsSourceEnvironment CredentialsSource = "Environment"
CredentialsSourceEnvironment = common.CredentialsSourceEnvironment
// CredentialsSourceFilesystem indicates that a provider should acquire
// credentials from the filesystem.
CredentialsSourceFilesystem CredentialsSource = "Filesystem"
CredentialsSourceFilesystem = common.CredentialsSourceFilesystem
)
// CommonCredentialSelectors provides common selectors for extracting
// credentials.
type CommonCredentialSelectors struct {
// Fs is a reference to a filesystem location that contains credentials that
// must be used to connect to the provider.
// +optional
Fs *FsSelector `json:"fs,omitempty"`
// Env is a reference to an environment variable that contains credentials
// that must be used to connect to the provider.
// +optional
Env *EnvSelector `json:"env,omitempty"`
// A SecretRef is a reference to a secret key that contains the credentials
// that must be used to connect to the provider.
// +optional
SecretRef *SecretKeySelector `json:"secretRef,omitempty"`
}
type CommonCredentialSelectors = common.CommonCredentialSelectors
// EnvSelector selects an environment variable.
type EnvSelector struct {
// Name is the name of an environment variable.
Name string `json:"name"`
}
type EnvSelector = common.EnvSelector
// FsSelector selects a filesystem location.
type FsSelector struct {
// Path is a filesystem path.
Path string `json:"path"`
}
type FsSelector = common.FsSelector
// A ProviderConfigStatus defines the observed status of a ProviderConfig.
type ProviderConfigStatus struct {
ConditionedStatus `json:",inline"`
// Users of this provider configuration.
Users int64 `json:"users,omitempty"`
}
type ProviderConfigStatus = common.ProviderConfigStatus
// A ProviderConfigUsage is a record that a particular managed resource is using
// a particular provider configuration.
type ProviderConfigUsage struct {
// ProviderConfigReference to the provider config being used.
ProviderConfigReference Reference `json:"providerConfigRef"`
// ResourceReference to the managed resource using the provider config.
ResourceReference TypedReference `json:"resourceRef"`
}
type ProviderConfigUsage = common.ProviderConfigUsage
// A TargetSpec defines the common fields of objects used for exposing
// infrastructure to workloads that can be scheduled to.

View File

@ -24,378 +24,6 @@ import (
corev1 "k8s.io/api/core/v1"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommonCredentialSelectors) DeepCopyInto(out *CommonCredentialSelectors) {
*out = *in
if in.Fs != nil {
in, out := &in.Fs, &out.Fs
*out = new(FsSelector)
**out = **in
}
if in.Env != nil {
in, out := &in.Env, &out.Env
*out = new(EnvSelector)
**out = **in
}
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(SecretKeySelector)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonCredentialSelectors.
func (in *CommonCredentialSelectors) DeepCopy() *CommonCredentialSelectors {
if in == nil {
return nil
}
out := new(CommonCredentialSelectors)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Condition) DeepCopyInto(out *Condition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
func (in *Condition) DeepCopy() *Condition {
if in == nil {
return nil
}
out := new(Condition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ConditionedStatus) DeepCopyInto(out *ConditionedStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionedStatus.
func (in *ConditionedStatus) DeepCopy() *ConditionedStatus {
if in == nil {
return nil
}
out := new(ConditionedStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Config) DeepCopyInto(out *Config) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config.
func (in *Config) DeepCopy() *Config {
if in == nil {
return nil
}
out := new(Config)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ConnectionSecretMetadata) DeepCopyInto(out *ConnectionSecretMetadata) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Type != nil {
in, out := &in.Type, &out.Type
*out = new(corev1.SecretType)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionSecretMetadata.
func (in *ConnectionSecretMetadata) DeepCopy() *ConnectionSecretMetadata {
if in == nil {
return nil
}
out := new(ConnectionSecretMetadata)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EnvSelector) DeepCopyInto(out *EnvSelector) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvSelector.
func (in *EnvSelector) DeepCopy() *EnvSelector {
if in == nil {
return nil
}
out := new(EnvSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FsSelector) DeepCopyInto(out *FsSelector) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FsSelector.
func (in *FsSelector) DeepCopy() *FsSelector {
if in == nil {
return nil
}
out := new(FsSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesAuthConfig) DeepCopyInto(out *KubernetesAuthConfig) {
*out = *in
in.CommonCredentialSelectors.DeepCopyInto(&out.CommonCredentialSelectors)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesAuthConfig.
func (in *KubernetesAuthConfig) DeepCopy() *KubernetesAuthConfig {
if in == nil {
return nil
}
out := new(KubernetesAuthConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesSecretStoreConfig) DeepCopyInto(out *KubernetesSecretStoreConfig) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesSecretStoreConfig.
func (in *KubernetesSecretStoreConfig) DeepCopy() *KubernetesSecretStoreConfig {
if in == nil {
return nil
}
out := new(KubernetesSecretStoreConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalSecretReference.
func (in *LocalSecretReference) DeepCopy() *LocalSecretReference {
if in == nil {
return nil
}
out := new(LocalSecretReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ManagementPolicies) DeepCopyInto(out *ManagementPolicies) {
{
in := &in
*out = make(ManagementPolicies, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementPolicies.
func (in ManagementPolicies) DeepCopy() ManagementPolicies {
if in == nil {
return nil
}
out := new(ManagementPolicies)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MergeOptions) DeepCopyInto(out *MergeOptions) {
*out = *in
if in.KeepMapValues != nil {
in, out := &in.KeepMapValues, &out.KeepMapValues
*out = new(bool)
**out = **in
}
if in.AppendSlice != nil {
in, out := &in.AppendSlice, &out.AppendSlice
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeOptions.
func (in *MergeOptions) DeepCopy() *MergeOptions {
if in == nil {
return nil
}
out := new(MergeOptions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ObservedStatus) DeepCopyInto(out *ObservedStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservedStatus.
func (in *ObservedStatus) DeepCopy() *ObservedStatus {
if in == nil {
return nil
}
out := new(ObservedStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PluginStoreConfig) DeepCopyInto(out *PluginStoreConfig) {
*out = *in
out.ConfigRef = in.ConfigRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginStoreConfig.
func (in *PluginStoreConfig) DeepCopy() *PluginStoreConfig {
if in == nil {
return nil
}
out := new(PluginStoreConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Policy) DeepCopyInto(out *Policy) {
*out = *in
if in.Resolve != nil {
in, out := &in.Resolve, &out.Resolve
*out = new(ResolvePolicy)
**out = **in
}
if in.Resolution != nil {
in, out := &in.Resolution, &out.Resolution
*out = new(ResolutionPolicy)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy.
func (in *Policy) DeepCopy() *Policy {
if in == nil {
return nil
}
out := new(Policy)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderConfigStatus) DeepCopyInto(out *ProviderConfigStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigStatus.
func (in *ProviderConfigStatus) DeepCopy() *ProviderConfigStatus {
if in == nil {
return nil
}
out := new(ProviderConfigStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderConfigUsage) DeepCopyInto(out *ProviderConfigUsage) {
*out = *in
in.ProviderConfigReference.DeepCopyInto(&out.ProviderConfigReference)
out.ResourceReference = in.ResourceReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigUsage.
func (in *ProviderConfigUsage) DeepCopy() *ProviderConfigUsage {
if in == nil {
return nil
}
out := new(ProviderConfigUsage)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PublishConnectionDetailsTo) DeepCopyInto(out *PublishConnectionDetailsTo) {
*out = *in
if in.Metadata != nil {
in, out := &in.Metadata, &out.Metadata
*out = new(ConnectionSecretMetadata)
(*in).DeepCopyInto(*out)
}
if in.SecretStoreConfigRef != nil {
in, out := &in.SecretStoreConfigRef, &out.SecretStoreConfigRef
*out = new(Reference)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublishConnectionDetailsTo.
func (in *PublishConnectionDetailsTo) DeepCopy() *PublishConnectionDetailsTo {
if in == nil {
return nil
}
out := new(PublishConnectionDetailsTo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Reference) DeepCopyInto(out *Reference) {
*out = *in
if in.Policy != nil {
in, out := &in.Policy, &out.Policy
*out = new(Policy)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Reference.
func (in *Reference) DeepCopy() *Reference {
if in == nil {
return nil
}
out := new(Reference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) {
*out = *in
@ -404,11 +32,6 @@ func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) {
*out = new(SecretReference)
**out = **in
}
if in.PublishConnectionDetailsTo != nil {
in, out := &in.PublishConnectionDetailsTo, &out.PublishConnectionDetailsTo
*out = new(PublishConnectionDetailsTo)
(*in).DeepCopyInto(*out)
}
if in.ProviderConfigReference != nil {
in, out := &in.ProviderConfigReference, &out.ProviderConfigReference
*out = new(Reference)
@ -435,7 +58,7 @@ func (in *ResourceSpec) DeepCopy() *ResourceSpec {
func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
out.ObservedStatus = in.ObservedStatus
in.ObservedStatus.DeepCopyInto(&out.ObservedStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceStatus.
@ -448,99 +71,6 @@ func (in *ResourceStatus) DeepCopy() *ResourceStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) {
*out = *in
out.SecretReference = in.SecretReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector.
func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
if in == nil {
return nil
}
out := new(SecretKeySelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretReference) DeepCopyInto(out *SecretReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference.
func (in *SecretReference) DeepCopy() *SecretReference {
if in == nil {
return nil
}
out := new(SecretReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretStoreConfig) DeepCopyInto(out *SecretStoreConfig) {
*out = *in
if in.Type != nil {
in, out := &in.Type, &out.Type
*out = new(SecretStoreType)
**out = **in
}
if in.Kubernetes != nil {
in, out := &in.Kubernetes, &out.Kubernetes
*out = new(KubernetesSecretStoreConfig)
(*in).DeepCopyInto(*out)
}
if in.Plugin != nil {
in, out := &in.Plugin, &out.Plugin
*out = new(PluginStoreConfig)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreConfig.
func (in *SecretStoreConfig) DeepCopy() *SecretStoreConfig {
if in == nil {
return nil
}
out := new(SecretStoreConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Selector) DeepCopyInto(out *Selector) {
*out = *in
if in.MatchLabels != nil {
in, out := &in.MatchLabels, &out.MatchLabels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.MatchControllerRef != nil {
in, out := &in.MatchControllerRef, &out.MatchControllerRef
*out = new(bool)
**out = **in
}
if in.Policy != nil {
in, out := &in.Policy, &out.Policy
*out = new(Policy)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Selector.
func (in *Selector) DeepCopy() *Selector {
if in == nil {
return nil
}
out := new(Selector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TargetSpec) DeepCopyInto(out *TargetSpec) {
*out = *in
@ -581,18 +111,3 @@ func (in *TargetStatus) DeepCopy() *TargetStatus {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TypedReference) DeepCopyInto(out *TypedReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TypedReference.
func (in *TypedReference) DeepCopy() *TypedReference {
if in == nil {
return nil
}
out := new(TypedReference)
in.DeepCopyInto(out)
return out
}

19
apis/common/v2/doc.go Normal file
View File

@ -0,0 +1,19 @@
/*
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 v2 contains core API types used by most Crossplane resources.
// +kubebuilder:object:generate=true
package v2

View File

@ -0,0 +1,51 @@
/*
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 v2
import (
"github.com/crossplane/crossplane-runtime/apis/common"
)
// A ManagedResourceSpec defines the desired state of a managed resource.
type ManagedResourceSpec struct {
// WriteConnectionSecretToReference specifies the namespace and name of a
// Secret to which any connection details for this managed resource should
// be written. Connection details frequently include the endpoint, username,
// and password required to connect to the managed resource.
// +optional
WriteConnectionSecretToReference *common.LocalSecretReference `json:"writeConnectionSecretToRef,omitempty"`
// ProviderConfigReference specifies how the provider that will be used to
// create, observe, update, and delete this managed resource should be
// configured.
// +kubebuilder:default={"kind": "ClusterProviderConfig", "name": "default"}
ProviderConfigReference *common.ProviderConfigReference `json:"providerConfigRef,omitempty"`
// THIS IS A BETA FIELD. It is on by default but can be opted out
// through a Crossplane feature flag.
// ManagementPolicies specify the array of actions Crossplane is allowed to
// take on the managed and external resources.
// See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223
// and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md
// +optional
// +kubebuilder:default={"*"}
ManagementPolicies common.ManagementPolicies `json:"managementPolicies,omitempty"`
}
// A TypedProviderConfigUsage is a record that a particular managed resource is using
// a particular provider configuration.
type TypedProviderConfigUsage = common.TypedProviderConfigUsage

View File

@ -0,0 +1,55 @@
//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 v2
import (
"github.com/crossplane/crossplane-runtime/apis/common"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ManagedResourceSpec) DeepCopyInto(out *ManagedResourceSpec) {
*out = *in
if in.WriteConnectionSecretToReference != nil {
in, out := &in.WriteConnectionSecretToReference, &out.WriteConnectionSecretToReference
*out = new(common.LocalSecretReference)
**out = **in
}
if in.ProviderConfigReference != nil {
in, out := &in.ProviderConfigReference, &out.ProviderConfigReference
*out = new(common.ProviderConfigReference)
**out = **in
}
if in.ManagementPolicies != nil {
in, out := &in.ManagementPolicies, &out.ManagementPolicies
*out = make(common.ManagementPolicies, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedResourceSpec.
func (in *ManagedResourceSpec) DeepCopy() *ManagedResourceSpec {
if in == nil {
return nil
}
out := new(ManagedResourceSpec)
in.DeepCopyInto(out)
return out
}

View File

@ -0,0 +1,468 @@
//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 common
import ()
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommonCredentialSelectors) DeepCopyInto(out *CommonCredentialSelectors) {
*out = *in
if in.Fs != nil {
in, out := &in.Fs, &out.Fs
*out = new(FsSelector)
**out = **in
}
if in.Env != nil {
in, out := &in.Env, &out.Env
*out = new(EnvSelector)
**out = **in
}
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(SecretKeySelector)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonCredentialSelectors.
func (in *CommonCredentialSelectors) DeepCopy() *CommonCredentialSelectors {
if in == nil {
return nil
}
out := new(CommonCredentialSelectors)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Condition) DeepCopyInto(out *Condition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
func (in *Condition) DeepCopy() *Condition {
if in == nil {
return nil
}
out := new(Condition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ConditionedStatus) DeepCopyInto(out *ConditionedStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionedStatus.
func (in *ConditionedStatus) DeepCopy() *ConditionedStatus {
if in == nil {
return nil
}
out := new(ConditionedStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EnvSelector) DeepCopyInto(out *EnvSelector) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvSelector.
func (in *EnvSelector) DeepCopy() *EnvSelector {
if in == nil {
return nil
}
out := new(EnvSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FsSelector) DeepCopyInto(out *FsSelector) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FsSelector.
func (in *FsSelector) DeepCopy() *FsSelector {
if in == nil {
return nil
}
out := new(FsSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LocalSecretKeySelector) DeepCopyInto(out *LocalSecretKeySelector) {
*out = *in
out.LocalSecretReference = in.LocalSecretReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalSecretKeySelector.
func (in *LocalSecretKeySelector) DeepCopy() *LocalSecretKeySelector {
if in == nil {
return nil
}
out := new(LocalSecretKeySelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalSecretReference.
func (in *LocalSecretReference) DeepCopy() *LocalSecretReference {
if in == nil {
return nil
}
out := new(LocalSecretReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ManagementPolicies) DeepCopyInto(out *ManagementPolicies) {
{
in := &in
*out = make(ManagementPolicies, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementPolicies.
func (in ManagementPolicies) DeepCopy() ManagementPolicies {
if in == nil {
return nil
}
out := new(ManagementPolicies)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MergeOptions) DeepCopyInto(out *MergeOptions) {
*out = *in
if in.KeepMapValues != nil {
in, out := &in.KeepMapValues, &out.KeepMapValues
*out = new(bool)
**out = **in
}
if in.AppendSlice != nil {
in, out := &in.AppendSlice, &out.AppendSlice
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeOptions.
func (in *MergeOptions) DeepCopy() *MergeOptions {
if in == nil {
return nil
}
out := new(MergeOptions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespacedReference) DeepCopyInto(out *NamespacedReference) {
*out = *in
if in.Policy != nil {
in, out := &in.Policy, &out.Policy
*out = new(Policy)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedReference.
func (in *NamespacedReference) DeepCopy() *NamespacedReference {
if in == nil {
return nil
}
out := new(NamespacedReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespacedSelector) DeepCopyInto(out *NamespacedSelector) {
*out = *in
if in.MatchLabels != nil {
in, out := &in.MatchLabels, &out.MatchLabels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.MatchControllerRef != nil {
in, out := &in.MatchControllerRef, &out.MatchControllerRef
*out = new(bool)
**out = **in
}
if in.Policy != nil {
in, out := &in.Policy, &out.Policy
*out = new(Policy)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedSelector.
func (in *NamespacedSelector) DeepCopy() *NamespacedSelector {
if in == nil {
return nil
}
out := new(NamespacedSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ObservedStatus) DeepCopyInto(out *ObservedStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservedStatus.
func (in *ObservedStatus) DeepCopy() *ObservedStatus {
if in == nil {
return nil
}
out := new(ObservedStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Policy) DeepCopyInto(out *Policy) {
*out = *in
if in.Resolve != nil {
in, out := &in.Resolve, &out.Resolve
*out = new(ResolvePolicy)
**out = **in
}
if in.Resolution != nil {
in, out := &in.Resolution, &out.Resolution
*out = new(ResolutionPolicy)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy.
func (in *Policy) DeepCopy() *Policy {
if in == nil {
return nil
}
out := new(Policy)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderConfigReference) DeepCopyInto(out *ProviderConfigReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigReference.
func (in *ProviderConfigReference) DeepCopy() *ProviderConfigReference {
if in == nil {
return nil
}
out := new(ProviderConfigReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderConfigStatus) DeepCopyInto(out *ProviderConfigStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigStatus.
func (in *ProviderConfigStatus) DeepCopy() *ProviderConfigStatus {
if in == nil {
return nil
}
out := new(ProviderConfigStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderConfigUsage) DeepCopyInto(out *ProviderConfigUsage) {
*out = *in
in.ProviderConfigReference.DeepCopyInto(&out.ProviderConfigReference)
out.ResourceReference = in.ResourceReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigUsage.
func (in *ProviderConfigUsage) DeepCopy() *ProviderConfigUsage {
if in == nil {
return nil
}
out := new(ProviderConfigUsage)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Reference) DeepCopyInto(out *Reference) {
*out = *in
if in.Policy != nil {
in, out := &in.Policy, &out.Policy
*out = new(Policy)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Reference.
func (in *Reference) DeepCopy() *Reference {
if in == nil {
return nil
}
out := new(Reference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
out.ObservedStatus = in.ObservedStatus
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceStatus.
func (in *ResourceStatus) DeepCopy() *ResourceStatus {
if in == nil {
return nil
}
out := new(ResourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) {
*out = *in
out.SecretReference = in.SecretReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector.
func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
if in == nil {
return nil
}
out := new(SecretKeySelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretReference) DeepCopyInto(out *SecretReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference.
func (in *SecretReference) DeepCopy() *SecretReference {
if in == nil {
return nil
}
out := new(SecretReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Selector) DeepCopyInto(out *Selector) {
*out = *in
if in.MatchLabels != nil {
in, out := &in.MatchLabels, &out.MatchLabels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.MatchControllerRef != nil {
in, out := &in.MatchControllerRef, &out.MatchControllerRef
*out = new(bool)
**out = **in
}
if in.Policy != nil {
in, out := &in.Policy, &out.Policy
*out = new(Policy)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Selector.
func (in *Selector) DeepCopy() *Selector {
if in == nil {
return nil
}
out := new(Selector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TypedProviderConfigUsage) DeepCopyInto(out *TypedProviderConfigUsage) {
*out = *in
out.ProviderConfigReference = in.ProviderConfigReference
out.ResourceReference = in.ResourceReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TypedProviderConfigUsage.
func (in *TypedProviderConfigUsage) DeepCopy() *TypedProviderConfigUsage {
if in == nil {
return nil
}
out := new(TypedProviderConfigUsage)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TypedReference) DeepCopyInto(out *TypedReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TypedReference.
func (in *TypedReference) DeepCopy() *TypedReference {
if in == nil {
return nil
}
out := new(TypedReference)
in.DeepCopyInto(out)
return out
}

View File

@ -12,7 +12,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc-gen-go v1.36.5
// protoc (unknown)
// source: proto/v1alpha1/ess.proto
@ -25,6 +25,7 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@ -36,23 +37,20 @@ const (
// ConfigReference is used to refer a StoreConfig object.
type ConfigReference struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -62,7 +60,7 @@ func (*ConfigReference) ProtoMessage() {}
func (x *ConfigReference) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1alpha1_ess_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -100,23 +98,20 @@ func (x *ConfigReference) GetName() string {
// Secret defines the structure of a secret.
type Secret struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Data map[string][]byte `protobuf:"bytes,3,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
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{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -126,7 +121,7 @@ func (*Secret) ProtoMessage() {}
func (x *Secret) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1alpha1_ess_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -164,22 +159,19 @@ func (x *Secret) GetData() map[string][]byte {
// GetSecretRequest requests secret from the secret store.
type GetSecretRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -189,7 +181,7 @@ func (*GetSecretRequest) ProtoMessage() {}
func (x *GetSecretRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1alpha1_ess_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -220,21 +212,18 @@ func (x *GetSecretRequest) GetSecret() *Secret {
// GetSecretResponse returns the secret from the secret store.
type GetSecretResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -244,7 +233,7 @@ func (*GetSecretResponse) ProtoMessage() {}
func (x *GetSecretResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1alpha1_ess_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -268,22 +257,19 @@ func (x *GetSecretResponse) GetSecret() *Secret {
// ApplySecretRequest applies the secret data update to the secret store.
type ApplySecretRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -293,7 +279,7 @@ func (*ApplySecretRequest) ProtoMessage() {}
func (x *ApplySecretRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1alpha1_ess_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -324,21 +310,18 @@ func (x *ApplySecretRequest) GetSecret() *Secret {
// ApplySecretResponse returns if the secret is changed or not.
type ApplySecretResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -348,7 +331,7 @@ func (*ApplySecretResponse) ProtoMessage() {}
func (x *ApplySecretResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1alpha1_ess_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -372,22 +355,19 @@ func (x *ApplySecretResponse) GetChanged() bool {
// DeleteKeysRequest deletes the secret from the secret store.
type DeleteKeysRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -397,7 +377,7 @@ func (*DeleteKeysRequest) ProtoMessage() {}
func (x *DeleteKeysRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1alpha1_ess_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -428,19 +408,17 @@ func (x *DeleteKeysRequest) GetSecret() *Secret {
// DeleteKeysResponse is returned if the secret is deleted.
type DeleteKeysResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteKeysResponse) Reset() {
*x = DeleteKeysResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_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)
@ -450,7 +428,7 @@ func (*DeleteKeysResponse) ProtoMessage() {}
func (x *DeleteKeysResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1alpha1_ess_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -467,7 +445,7 @@ func (*DeleteKeysResponse) Descriptor() ([]byte, []int) {
var File_proto_v1alpha1_ess_proto protoreflect.FileDescriptor
var file_proto_v1alpha1_ess_proto_rawDesc = []byte{
var file_proto_v1alpha1_ess_proto_rawDesc = string([]byte{
0x0a, 0x18, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
0x2f, 0x65, 0x73, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x65, 0x73, 0x73, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x22, 0x5a,
@ -554,16 +532,16 @@ var file_proto_v1alpha1_ess_proto_rawDesc = []byte{
0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2d, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f,
0x61, 0x70, 0x69, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70,
0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
})
var (
file_proto_v1alpha1_ess_proto_rawDescOnce sync.Once
file_proto_v1alpha1_ess_proto_rawDescData = file_proto_v1alpha1_ess_proto_rawDesc
file_proto_v1alpha1_ess_proto_rawDescData []byte
)
func file_proto_v1alpha1_ess_proto_rawDescGZIP() []byte {
file_proto_v1alpha1_ess_proto_rawDescOnce.Do(func() {
file_proto_v1alpha1_ess_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_v1alpha1_ess_proto_rawDescData)
file_proto_v1alpha1_ess_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_v1alpha1_ess_proto_rawDesc), len(file_proto_v1alpha1_ess_proto_rawDesc)))
})
return file_proto_v1alpha1_ess_proto_rawDescData
}
@ -609,109 +587,11 @@ func file_proto_v1alpha1_ess_proto_init() {
if File_proto_v1alpha1_ess_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_v1alpha1_ess_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*ConfigReference); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_v1alpha1_ess_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*Secret); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_v1alpha1_ess_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*GetSecretRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_v1alpha1_ess_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*GetSecretResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_v1alpha1_ess_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*ApplySecretRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_v1alpha1_ess_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*ApplySecretResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_v1alpha1_ess_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*DeleteKeysRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_v1alpha1_ess_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*DeleteKeysResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_v1alpha1_ess_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v1alpha1_ess_proto_rawDesc), len(file_proto_v1alpha1_ess_proto_rawDesc)),
NumEnums: 0,
NumMessages: 10,
NumExtensions: 0,
@ -722,7 +602,6 @@ func file_proto_v1alpha1_ess_proto_init() {
MessageInfos: file_proto_v1alpha1_ess_proto_msgTypes,
}.Build()
File_proto_v1alpha1_ess_proto = out.File
file_proto_v1alpha1_ess_proto_rawDesc = nil
file_proto_v1alpha1_ess_proto_goTypes = nil
file_proto_v1alpha1_ess_proto_depIdxs = nil
}

77
go.mod
View File

@ -1,29 +1,27 @@
module github.com/crossplane/crossplane-runtime
go 1.23.0
toolchain go1.23.7
go 1.24.0
require (
dario.cat/mergo v1.0.1
github.com/evanphx/json-patch v5.9.11+incompatible
github.com/go-logr/logr v1.4.2
github.com/google/go-cmp v0.6.0
github.com/prometheus/client_golang v1.19.1
github.com/google/go-cmp v0.7.0
github.com/prometheus/client_golang v1.22.0
github.com/spf13/afero v1.11.0
golang.org/x/time v0.5.0
google.golang.org/grpc v1.65.0
golang.org/x/time v0.9.0
google.golang.org/grpc v1.68.1
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
google.golang.org/protobuf v1.34.2
k8s.io/api v0.31.0
k8s.io/apiextensions-apiserver v0.31.0
k8s.io/apimachinery v0.31.0
k8s.io/client-go v0.31.0
k8s.io/component-base v0.31.0
google.golang.org/protobuf v1.36.5
k8s.io/api v0.33.0
k8s.io/apiextensions-apiserver v0.33.0
k8s.io/apimachinery v0.33.0
k8s.io/client-go v0.33.0
k8s.io/component-base v0.33.0
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/controller-runtime v0.19.0
sigs.k8s.io/controller-tools v0.16.0
sigs.k8s.io/controller-tools v0.18.0
sigs.k8s.io/yaml v1.4.0
)
@ -34,20 +32,16 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gobuffalo/flect v1.0.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gobuffalo/flect v1.0.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@ -59,27 +53,32 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/zap v1.27.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.24.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.32.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.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/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
k8s.io/code-generator v0.33.0 // indirect
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
)

170
go.sum
View File

@ -6,7 +6,7 @@ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
@ -18,8 +18,8 @@ github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb
github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
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/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@ -28,37 +28,34 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
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/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA=
github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/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-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM=
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
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/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@ -67,6 +64,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -74,6 +73,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -92,45 +93,50 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
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.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
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=
@ -144,56 +150,56 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
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=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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=
@ -203,35 +209,41 @@ 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.8/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=
k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk=
k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk=
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs=
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
k8s.io/code-generator v0.33.0 h1:B212FVl6EFqNmlgdOZYWNi77yBv+ed3QgQsMR8YQCw4=
k8s.io/code-generator v0.33.0/go.mod h1:KnJRokGxjvbBQkSJkbVuBbu6z4B0rC7ynkpY5Aw6m9o=
k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk=
k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU=
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 h1:2OX19X59HxDprNCVrWi6jb7LW1PoqTlYqEq5H2oetog=
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
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-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
sigs.k8s.io/controller-tools v0.16.0 h1:EJPB+a5Bve861SPBPPWRbP6bbKyNxqK12oYT5zEns9s=
sigs.k8s.io/controller-tools v0.16.0/go.mod h1:0I0xqjR65YTfoO12iR+mZR6s6UAVcUARgXRlsu0ljB0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/controller-tools v0.18.0 h1:rGxGZCZTV2wJreeRgqVoWab/mfcumTMmSwKzoM9xrsE=
sigs.k8s.io/controller-tools v0.18.0/go.mod h1:gLKoiGBriyNh+x1rWtUQnakUYEujErjXs9pf+x/8n1U=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
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/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@ -36,12 +36,14 @@ const (
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)

View File

@ -27,10 +27,12 @@ func TestLoad(t *testing.T) {
certsFolderPath string
requireClientValidation bool
}
type want struct {
err error
out *tls.Config
}
cases := map[string]struct {
reason string
args
@ -92,16 +94,16 @@ func TestLoad(t *testing.T) {
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
certsFolderPath := tc.args.certsFolderPath
requireClient := tc.args.requireClientValidation
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.want.err, err, test.EquateErrors()); diff != "" {
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.want.out.ClientAuth, cfg.ClientAuth); diff != "" {
if diff := cmp.Diff(tc.out.ClientAuth, cfg.ClientAuth); diff != "" {
t.Errorf("\n%s\nLoad(...): -want, +got:\n%s", tc.reason, diff)
}
}

View File

@ -68,5 +68,6 @@ func (c *observedGenerationPropagationConditionSet) MarkConditions(condition ...
for i := range condition {
condition[i].ObservedGeneration = c.o.GetGeneration()
}
c.o.SetConditions(condition...)
}

View File

@ -80,6 +80,7 @@ func TestOGConditionSetMark(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)
}
@ -129,5 +130,6 @@ func newManaged(generation int64, conditions ...xpv1.Condition) *fake.Managed {
mg := &fake.Managed{}
mg.Generation = generation
mg.SetConditions(conditions...)
return mg
}

View File

@ -1,83 +0,0 @@
/*
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 fake implements a fake secret store.
//
//nolint:musttag // We only use JSON to round-trip convert these mocks.
package fake
import (
"context"
"encoding/json"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
)
// SecretStore is a fake SecretStore.
type SecretStore struct {
ReadKeyValuesFn func(ctx context.Context, n store.ScopedName, s *store.Secret) error
WriteKeyValuesFn func(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error)
DeleteKeyValuesFn func(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error
}
// ReadKeyValues reads key values.
func (ss *SecretStore) ReadKeyValues(ctx context.Context, n store.ScopedName, s *store.Secret) error {
return ss.ReadKeyValuesFn(ctx, n, s)
}
// WriteKeyValues writes key values.
func (ss *SecretStore) WriteKeyValues(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) {
return ss.WriteKeyValuesFn(ctx, s, wo...)
}
// DeleteKeyValues deletes key values.
func (ss *SecretStore) DeleteKeyValues(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error {
return ss.DeleteKeyValuesFn(ctx, s, do...)
}
// StoreConfig is a mock implementation of the StoreConfig interface.
type StoreConfig struct {
metav1.ObjectMeta
Config v1.SecretStoreConfig
v1.ConditionedStatus
}
// GetStoreConfig returns SecretStoreConfig.
func (s *StoreConfig) GetStoreConfig() v1.SecretStoreConfig {
return s.Config
}
// GetObjectKind returns schema.ObjectKind.
func (s *StoreConfig) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}
// DeepCopyObject returns a copy of the object as runtime.Object.
func (s *StoreConfig) DeepCopyObject() runtime.Object {
out := &StoreConfig{}
j, err := json.Marshal(s)
if err != nil {
panic(err)
}
_ = json.Unmarshal(j, out)
return out
}

View File

@ -1,39 +0,0 @@
/*
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 connection
import (
"context"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
// A StoreConfig configures a connection store.
type StoreConfig interface {
resource.Object
GetStoreConfig() v1.SecretStoreConfig
}
// A Store stores sensitive key values in Secret.
type Store interface {
ReadKeyValues(ctx context.Context, n store.ScopedName, s *store.Secret) error
WriteKeyValues(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (changed bool, err error)
DeleteKeyValues(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error
}

View File

@ -1,221 +0,0 @@
/*
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 connection provides utilities for working with connection details.
package connection
import (
"context"
"crypto/tls"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
// Error strings.
const (
errConnectStore = "cannot connect to secret store"
errWriteStore = "cannot write to secret store"
errReadStore = "cannot read from secret store"
errDeleteFromStore = "cannot delete from secret store"
errGetStoreConfig = "cannot get store config"
errSecretConflict = "cannot establish control of existing connection secret"
errFmtNotOwnedBy = "existing secret is not owned by UID %q"
)
// StoreBuilderFn is a function that builds and returns a Store with a given
// store config.
type StoreBuilderFn func(ctx context.Context, local client.Client, tcfg *tls.Config, cfg v1.SecretStoreConfig) (Store, error)
// A DetailsManagerOption configures a DetailsManager.
type DetailsManagerOption func(*DetailsManager)
// WithStoreBuilder configures the StoreBuilder to use.
func WithStoreBuilder(sb StoreBuilderFn) DetailsManagerOption {
return func(m *DetailsManager) {
m.storeBuilder = sb
}
}
// WithTLSConfig configures the TLS config to use.
func WithTLSConfig(tcfg *tls.Config) DetailsManagerOption {
return func(m *DetailsManager) {
m.tcfg = tcfg
}
}
// DetailsManager is a connection details manager that satisfies the required
// interfaces to work with connection details by managing interaction with
// different store implementations.
type DetailsManager struct {
client client.Client
newConfig func() StoreConfig
storeBuilder StoreBuilderFn
tcfg *tls.Config
}
// NewDetailsManager returns a new connection DetailsManager.
func NewDetailsManager(c client.Client, of schema.GroupVersionKind, o ...DetailsManagerOption) *DetailsManager {
nc := func() StoreConfig {
//nolint:forcetypeassert // If the supplied type isn't a StoreConfig it's a programming error. We want to panic.
return resource.MustCreateObject(of, c.Scheme()).(StoreConfig)
}
// Panic early if we've been asked to reconcile a resource kind that has not
// been registered with our controller manager's scheme.
_ = nc()
m := &DetailsManager{
client: c,
newConfig: nc,
storeBuilder: RuntimeStoreBuilder,
}
for _, mo := range o {
mo(m)
}
return m
}
// PublishConnection publishes the supplied ConnectionDetails to a secret on
// the configured connection Store.
func (m *DetailsManager) PublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, conn managed.ConnectionDetails) (bool, error) {
// This resource does not want to expose a connection secret.
p := so.GetPublishConnectionDetailsTo()
if p == nil {
return false, nil
}
ss, err := m.connectStore(ctx, p)
if err != nil {
return false, errors.Wrap(err, errConnectStore)
}
changed, err := ss.WriteKeyValues(ctx, store.NewSecret(so, store.KeyValues(conn)), SecretToWriteMustBeOwnedBy(so))
return changed, errors.Wrap(err, errWriteStore)
}
// UnpublishConnection deletes connection details secret to the configured
// connection Store.
func (m *DetailsManager) UnpublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, conn managed.ConnectionDetails) error {
// This resource didn't expose a connection secret.
p := so.GetPublishConnectionDetailsTo()
if p == nil {
return nil
}
ss, err := m.connectStore(ctx, p)
if err != nil {
return errors.Wrap(err, errConnectStore)
}
return errors.Wrap(ss.DeleteKeyValues(ctx, store.NewSecret(so, store.KeyValues(conn)), SecretToDeleteMustBeOwnedBy(so)), errDeleteFromStore)
}
// FetchConnection fetches connection details of a given ConnectionSecretOwner.
func (m *DetailsManager) FetchConnection(ctx context.Context, so resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
// This resource does not want to expose a connection secret.
p := so.GetPublishConnectionDetailsTo()
if p == nil {
return nil, nil
}
ss, err := m.connectStore(ctx, p)
if err != nil {
return nil, errors.Wrap(err, errConnectStore)
}
s := &store.Secret{}
return managed.ConnectionDetails(s.Data), errors.Wrap(ss.ReadKeyValues(ctx, store.ScopedName{Name: p.Name, Scope: so.GetNamespace()}, s), errReadStore)
}
// PropagateConnection propagate connection details from one resource to another.
func (m *DetailsManager) PropagateConnection(ctx context.Context, to resource.LocalConnectionSecretOwner, from resource.ConnectionSecretOwner) (propagated bool, err error) {
// Either from does not expose a connection secret, or to does not want one.
if from.GetPublishConnectionDetailsTo() == nil || to.GetPublishConnectionDetailsTo() == nil {
return false, nil
}
ssFrom, err := m.connectStore(ctx, from.GetPublishConnectionDetailsTo())
if err != nil {
return false, errors.Wrap(err, errConnectStore)
}
sFrom := &store.Secret{}
if err = ssFrom.ReadKeyValues(ctx, store.ScopedName{
Name: from.GetPublishConnectionDetailsTo().Name,
Scope: from.GetNamespace(),
}, sFrom); err != nil {
return false, errors.Wrap(err, errReadStore)
}
// Make sure 'from' is the controller of the connection secret it references
// before we propagate it. This ensures a resource cannot use Crossplane to
// circumvent RBAC by propagating a secret it does not own.
if sFrom.GetOwner() != string(from.GetUID()) {
return false, errors.New(errSecretConflict)
}
ssTo, err := m.connectStore(ctx, to.GetPublishConnectionDetailsTo())
if err != nil {
return false, errors.Wrap(err, errConnectStore)
}
changed, err := ssTo.WriteKeyValues(ctx, store.NewSecret(to, sFrom.Data), SecretToWriteMustBeOwnedBy(to))
return changed, errors.Wrap(err, errWriteStore)
}
func (m *DetailsManager) connectStore(ctx context.Context, p *v1.PublishConnectionDetailsTo) (Store, error) {
sc := m.newConfig()
if err := m.client.Get(ctx, types.NamespacedName{Name: p.SecretStoreConfigRef.Name}, sc); err != nil {
return nil, errors.Wrap(err, errGetStoreConfig)
}
return m.storeBuilder(ctx, m.client, m.tcfg, sc.GetStoreConfig())
}
// SecretToWriteMustBeOwnedBy requires that the current object is a
// connection secret that is owned by an object with the supplied UID.
func SecretToWriteMustBeOwnedBy(so metav1.Object) store.WriteOption {
return func(_ context.Context, current, _ *store.Secret) error {
return secretMustBeOwnedBy(so, current)
}
}
// SecretToDeleteMustBeOwnedBy requires that the current secret is owned by
// an object with the supplied UID.
func SecretToDeleteMustBeOwnedBy(so metav1.Object) store.DeleteOption {
return func(_ context.Context, secret *store.Secret) error {
return secretMustBeOwnedBy(so, secret)
}
}
func secretMustBeOwnedBy(so metav1.Object, secret *store.Secret) error {
if secret.Metadata == nil || secret.Metadata.GetOwnerUID() != string(so.GetUID()) {
return errors.Errorf(errFmtNotOwnedBy, string(so.GetUID()))
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,256 +0,0 @@
/*
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 kubernetes implements a secret store backed by Kubernetes Secrets.
package kubernetes
import (
"context"
"crypto/tls"
"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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
// Error strings.
const (
errGetSecret = "cannot get secret"
errDeleteSecret = "cannot delete secret"
errUpdateSecret = "cannot update secret"
errApplySecret = "cannot apply secret"
errExtractKubernetesAuthCreds = "cannot extract kubernetes auth credentials"
errBuildRestConfig = "cannot build rest config kubeconfig"
errBuildClient = "cannot build Kubernetes client"
)
// SecretStore is a Kubernetes Secret Store.
type SecretStore struct {
client resource.ClientApplicator
defaultNamespace string
}
// NewSecretStore returns a new Kubernetes SecretStore.
func NewSecretStore(ctx context.Context, local client.Client, _ *tls.Config, cfg v1.SecretStoreConfig) (*SecretStore, error) {
kube, err := buildClient(ctx, local, cfg)
if err != nil {
return nil, errors.Wrap(err, errBuildClient)
}
return &SecretStore{
client: resource.ClientApplicator{
Client: kube,
Applicator: resource.NewApplicatorWithRetry(resource.NewAPIPatchingApplicator(kube), resource.IsAPIErrorWrapped, nil),
},
defaultNamespace: cfg.DefaultScope,
}, nil
}
func buildClient(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (client.Client, error) {
if cfg.Kubernetes == nil {
// No KubernetesSecretStoreConfig provided, local API Server will be
// used as Secret Store.
return local, nil
}
// Configure client for an external API server with a given Kubeconfig.
kfg, err := resource.CommonCredentialExtractor(ctx, cfg.Kubernetes.Auth.Source, local, cfg.Kubernetes.Auth.CommonCredentialSelectors)
if err != nil {
return nil, errors.Wrap(err, errExtractKubernetesAuthCreds)
}
config, err := clientcmd.RESTConfigFromKubeConfig(kfg)
if err != nil {
return nil, errors.Wrap(err, errBuildRestConfig)
}
return client.New(config, client.Options{})
}
// ReadKeyValues reads and returns key value pairs for a given Kubernetes Secret.
func (ss *SecretStore) ReadKeyValues(ctx context.Context, n store.ScopedName, s *store.Secret) error {
ks := &corev1.Secret{}
if err := ss.client.Get(ctx, types.NamespacedName{Name: n.Name, Namespace: ss.namespaceForSecret(n)}, ks); resource.IgnoreNotFound(err) != nil {
return errors.Wrap(err, errGetSecret)
}
s.Data = ks.Data
s.Metadata = &v1.ConnectionSecretMetadata{
Labels: ks.Labels,
Annotations: ks.Annotations,
Type: &ks.Type,
}
return nil
}
// WriteKeyValues writes key value pairs to a given Kubernetes Secret.
func (ss *SecretStore) WriteKeyValues(ctx context.Context, s *store.Secret, wo ...store.WriteOption) (bool, error) {
ks := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: s.Name,
Namespace: ss.namespaceForSecret(s.ScopedName),
},
Type: resource.SecretTypeConnection,
Data: s.Data,
}
if s.Metadata != nil {
ks.Labels = s.Metadata.Labels
ks.Annotations = s.Metadata.Annotations
if s.Metadata.Type != nil {
ks.Type = *s.Metadata.Type
}
}
ao := applyOptions(wo...)
ao = append(ao, 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.
return !cmp.Equal(current.(*corev1.Secret).Data, desired.(*corev1.Secret).Data, cmpopts.EquateEmpty()) //nolint:forcetypeassert // Will always be a secret.
}))
err := ss.client.Apply(ctx, ks, ao...)
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, errApplySecret)
}
return true, nil
}
// DeleteKeyValues delete key value pairs from a given Kubernetes Secret.
// If no kv specified, the whole secret instance is deleted.
// If kv specified, those would be deleted and secret instance will be deleted
// only if there is no data left.
func (ss *SecretStore) DeleteKeyValues(ctx context.Context, s *store.Secret, do ...store.DeleteOption) error {
// NOTE(turkenh): DeleteKeyValues method wouldn't need to do anything if we
// have used owner references similar to existing implementation. However,
// this wouldn't work if the K8s API is not the same as where ConnectionSecretOwner
// object lives, i.e. a remote cluster.
// Considering there is not much additional value with deletion via garbage
// collection in this specific case other than one less API call during
// deletion, I opted for unifying both instead of adding conditional logic
// like add owner references if not remote and not call delete etc.
ks := &corev1.Secret{}
err := ss.client.Get(ctx, types.NamespacedName{Name: s.Name, Namespace: ss.namespaceForSecret(s.ScopedName)}, ks)
if kerrors.IsNotFound(err) {
// Secret already deleted, nothing to do.
return nil
}
if err != nil {
return errors.Wrap(err, errGetSecret)
}
for _, o := range do {
if err = o(ctx, s); err != nil {
return err
}
}
// Delete all supplied keys from secret data
for k := range s.Data {
delete(ks.Data, k)
}
if len(s.Data) == 0 || len(ks.Data) == 0 {
// Secret is deleted only if:
// - No kv to delete specified as input
// - No data left in the secret
return errors.Wrapf(ss.client.Delete(ctx, ks), errDeleteSecret)
}
// If there are still keys left, update the secret with the remaining.
return errors.Wrapf(ss.client.Update(ctx, ks), errUpdateSecret)
}
func (ss *SecretStore) namespaceForSecret(n store.ScopedName) string {
if n.Scope == "" {
return ss.defaultNamespace
}
return n.Scope
}
func applyOptions(wo ...store.WriteOption) []resource.ApplyOption {
ao := make([]resource.ApplyOption, len(wo))
for i := range wo {
o := wo[i]
ao[i] = func(ctx context.Context, current, desired runtime.Object) error {
currentSecret := current.(*corev1.Secret) //nolint:forcetypeassert // Will always be a secret.
desiredSecret := desired.(*corev1.Secret) //nolint:forcetypeassert // Will always be a secret.
cs := &store.Secret{
ScopedName: store.ScopedName{
Name: currentSecret.Name,
Scope: currentSecret.Namespace,
},
Metadata: &v1.ConnectionSecretMetadata{
Labels: currentSecret.Labels,
Annotations: currentSecret.Annotations,
Type: &currentSecret.Type,
},
Data: currentSecret.Data,
}
// NOTE(turkenh): With External Secret Stores, we are using a special label/tag with key
// "secret.crossplane.io/owner-uid" to track the owner of the connection secret. However, different from
// other Secret Store implementations, Kubernetes Store uses metadata.OwnerReferences for this purpose and
// we don't want it to appear in the labels of the secret additionally.
// Here we are adding the owner label to the internal representation of the current secret as part of
// converting store.WriteOption's to k8s resource.ApplyOption's, so that our generic store.WriteOptions
// checking secret owner could work as expected.
// Fixes: https://github.com/crossplane/crossplane/issues/3520
if len(currentSecret.GetOwnerReferences()) > 0 {
cs.Metadata.SetOwnerUID(currentSecret.GetOwnerReferences()[0].UID)
}
ds := &store.Secret{
ScopedName: store.ScopedName{
Name: desiredSecret.Name,
Scope: desiredSecret.Namespace,
},
Metadata: &v1.ConnectionSecretMetadata{
Labels: desiredSecret.Labels,
Annotations: desiredSecret.Annotations,
Type: &desiredSecret.Type,
},
Data: desiredSecret.Data,
}
if err := o(ctx, cs, ds); err != nil {
return err
}
desiredSecret.Data = ds.Data
desiredSecret.Labels = ds.Metadata.Labels
desiredSecret.Annotations = ds.Metadata.Annotations
if ds.Metadata.Type != nil {
desiredSecret.Type = *ds.Metadata.Type
}
return nil
}
}
return ao
}

View File

@ -1,897 +0,0 @@
/*
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 kubernetes
import (
"context"
"testing"
"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/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var (
errBoom = errors.New("boom")
fakeSecretName = "fake"
fakeSecretNamespace = "fake-namespace"
fakeOwnerID = "00000000-0000-0000-0000-000000000000"
storeTypeKubernetes = v1.SecretStoreKubernetes
)
func fakeKV() map[string][]byte {
return map[string][]byte{
"key1": []byte("value1"),
"key2": []byte("value2"),
"key3": []byte("value3"),
}
}
func fakeLabels() map[string]string {
return map[string]string{
"environment": "unit-test",
"reason": "testing",
}
}
func fakeAnnotations() map[string]string {
return map[string]string{
"some-annotation-key": "some-annotation-value",
}
}
func TestSecretStoreReadKeyValues(t *testing.T) {
type args struct {
client resource.ClientApplicator
n store.ScopedName
}
type want struct {
result store.KeyValues
err error
}
cases := map[string]struct {
reason string
args
want
}{
"CannotGetSecret": {
reason: "Should return a proper error if cannot get the secret",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(errBoom),
},
},
n: store.ScopedName{
Name: fakeSecretName,
},
},
want: want{
err: errors.Wrap(errBoom, errGetSecret),
},
},
"SuccessfulRead": {
reason: "Should return all key values after a success read",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if key.Name != fakeSecretName || key.Namespace != fakeSecretNamespace {
return errors.New("unexpected secret name or namespace to get the secret")
}
*obj.(*corev1.Secret) = corev1.Secret{
Data: fakeKV(),
}
return nil
},
},
},
n: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
},
want: want{
result: store.KeyValues(fakeKV()),
},
},
"SecretNotFound": {
reason: "Should return nil as an error if secret is not found",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
},
},
},
want: want{
err: nil,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ss := &SecretStore{
client: tc.args.client,
}
s := &store.Secret{}
s.ScopedName = tc.args.n
err := ss.ReadKeyValues(context.Background(), tc.args.n, s)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nss.ReadKeyValues(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.result, s.Data); diff != "" {
t.Errorf("\n%s\nss.ReadKeyValues(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestSecretStoreWriteKeyValues(t *testing.T) {
secretTypeOpaque := corev1.SecretTypeOpaque
type args struct {
client resource.ClientApplicator
defaultNamespace string
secret *store.Secret
wo []store.WriteOption
}
type want struct {
changed bool
err error
}
cases := map[string]struct {
reason string
args
want
}{
"ApplyFailed": {
reason: "Should return a proper error when cannot apply.",
args: args{
client: resource.ClientApplicator{
Applicator: resource.ApplyFn(func(_ context.Context, _ client.Object, _ ...resource.ApplyOption) error {
return errBoom
}),
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Data: store.KeyValues(fakeKV()),
},
},
want: want{
err: errors.Wrap(errBoom, errApplySecret),
},
},
"FailedWriteOption": {
reason: "Should return a proper error if supplied write option fails",
args: args{
client: resource.ClientApplicator{
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
for _, fn := range option {
if err := fn(ctx, fakeConnectionSecret(withData(fakeKV())), obj); err != nil {
return err
}
}
return nil
}),
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Data: store.KeyValues(fakeKV()),
},
wo: []store.WriteOption{
func(_ context.Context, _, _ *store.Secret) error {
return errBoom
},
},
},
want: want{
err: errors.Wrap(errBoom, errApplySecret),
},
},
"SuccessfulWriteOption": {
reason: "Should return a proper error if supplied write option fails",
args: args{
client: resource.ClientApplicator{
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
for _, fn := range option {
if err := fn(ctx, fakeConnectionSecret(withData(fakeKV())), obj); err != nil {
return err
}
}
return nil
}),
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Data: store.KeyValues(fakeKV()),
},
wo: []store.WriteOption{
func(_ context.Context, _, desired *store.Secret) error {
desired.Data["customkey"] = []byte("customval")
desired.Metadata = &v1.ConnectionSecretMetadata{
Labels: map[string]string{
"foo": "baz",
},
}
return nil
},
},
},
want: want{
changed: true,
},
},
"SecretAlreadyUpToDate": {
reason: "Should not change secret if already up to date.",
args: args{
client: resource.ClientApplicator{
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
for _, fn := range option {
if err := fn(ctx, fakeConnectionSecret(withData(fakeKV())), obj); err != nil {
return err
}
}
return nil
}),
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Data: store.KeyValues(fakeKV()),
},
},
},
"SecretUpdatedWithNewValue": {
reason: "Should update value for an existing key if changed.",
args: args{
client: resource.ClientApplicator{
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{
"existing-key": []byte("new-value"),
})), obj.(*corev1.Secret)); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
for _, fn := range option {
if err := fn(ctx, fakeConnectionSecret(withData(map[string][]byte{
"existing-key": []byte("old-value"),
})), obj); err != nil {
return err
}
}
return nil
}),
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Data: store.KeyValues(map[string][]byte{
"existing-key": []byte("new-value"),
}),
},
},
want: want{
changed: true,
},
},
"SecretHasExpectedOwner": {
reason: "Should correctly check the owner of the secret.",
args: args{
client: resource.ClientApplicator{
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{
"existing-key": []byte("new-value"),
})), obj.(*corev1.Secret)); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
for _, fn := range option {
if err := fn(ctx, fakeConnectionSecret(withData(map[string][]byte{
"existing-key": []byte("old-value"),
}), withOwnerID(fakeOwnerID)), obj); err != nil {
return err
}
}
return nil
}),
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Data: store.KeyValues(map[string][]byte{
"existing-key": []byte("new-value"),
}),
},
wo: []store.WriteOption{func(_ context.Context, current, _ *store.Secret) error {
if current.Metadata == nil || current.Metadata.GetOwnerUID() != fakeOwnerID {
return errors.Errorf("secret not owned by %s", fakeOwnerID)
}
return nil
}},
},
want: want{
changed: true,
},
},
"SecretUpdatedWithNewKey": {
reason: "Should update existing secret additively if a new key added",
args: args{
client: resource.ClientApplicator{
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{
"new-key": []byte("new-value"),
})), obj.(*corev1.Secret)); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
for _, fn := range option {
if err := fn(ctx, fakeConnectionSecret(withData(map[string][]byte{
"existing-key": []byte("existing-value"),
})), obj); err != nil {
return err
}
}
return nil
}),
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Data: store.KeyValues(map[string][]byte{
"new-key": []byte("new-value"),
}),
},
},
want: want{
changed: true,
},
},
"SecretCreatedWithData": {
reason: "Should create a secret with all key values with default type.",
args: args{
client: resource.ClientApplicator{
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
if diff := cmp.Diff(fakeConnectionSecret(withData(fakeKV())), obj.(*corev1.Secret)); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
for _, fn := range option {
if err := fn(ctx, &corev1.Secret{}, obj); err != nil {
return err
}
}
return nil
}),
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Data: store.KeyValues(fakeKV()),
},
},
want: want{
changed: true,
},
},
"SecretCreatedWithDataAndMetadata": {
reason: "Should create a secret with all key values and provided metadata data.",
args: args{
client: resource.ClientApplicator{
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
if diff := cmp.Diff(fakeConnectionSecret(
withData(fakeKV()),
withType(corev1.SecretTypeOpaque),
withLabels(fakeLabels()),
withAnnotations(fakeAnnotations())), obj.(*corev1.Secret)); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
for _, fn := range option {
if err := fn(ctx, &corev1.Secret{}, obj); err != nil {
return err
}
}
return nil
}),
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Metadata: &v1.ConnectionSecretMetadata{
Labels: map[string]string{
"environment": "unit-test",
"reason": "testing",
},
Annotations: map[string]string{
"some-annotation-key": "some-annotation-value",
},
Type: &secretTypeOpaque,
},
Data: store.KeyValues(fakeKV()),
},
},
want: want{
changed: true,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ss := &SecretStore{
client: tc.args.client,
defaultNamespace: tc.args.defaultNamespace,
}
changed, err := ss.WriteKeyValues(context.Background(), tc.args.secret, tc.args.wo...)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nss.WriteKeyValues(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.changed, changed); diff != "" {
t.Errorf("\n%s\nss.WriteKeyValues(...): -want changed, +got changed:\n%s", tc.reason, diff)
}
})
}
}
func TestSecretStoreDeleteKeyValues(t *testing.T) {
type args struct {
client resource.ClientApplicator
defaultNamespace string
secret *store.Secret
do []store.DeleteOption
}
type want struct {
err error
}
cases := map[string]struct {
reason string
args
want
}{
"CannotGetSecret": {
reason: "Should return a proper error when it fails to get secret.",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(errBoom),
},
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
},
},
want: want{
err: errors.Wrap(errBoom, errGetSecret),
},
},
"SecretUpdatedWithRemainingKeys": {
reason: "Should remove supplied keys from secret and update with remaining.",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
*obj.(*corev1.Secret) = *fakeConnectionSecret(withData(fakeKV()))
return nil
}),
MockUpdate: func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{"key3": []byte("value3")})), obj.(*corev1.Secret)); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return nil
},
},
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
Data: store.KeyValues(map[string][]byte{
"key1": []byte("value1"),
"key2": []byte("value2"),
}),
},
},
want: want{
err: nil,
},
},
"CannotDeleteSecret": {
reason: "Should return a proper error when it fails to delete secret.",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
*obj.(*corev1.Secret) = *fakeConnectionSecret()
return nil
}),
MockDelete: test.NewMockDeleteFn(errBoom),
},
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
},
},
want: want{
err: errors.Wrap(errBoom, errDeleteSecret),
},
},
"SecretAlreadyDeleted": {
reason: "Should not return error if secret already deleted.",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(_ client.Object) error {
return kerrors.NewNotFound(schema.GroupResource{}, "")
}),
},
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
},
},
want: want{
err: nil,
},
},
"FailedDeleteOption": {
reason: "Should return a proper error if provided delete option fails.",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
*obj.(*corev1.Secret) = *fakeConnectionSecret(withData(fakeKV()))
return nil
}),
MockDelete: func(_ context.Context, _ client.Object, _ ...client.DeleteOption) error {
return nil
},
},
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
},
do: []store.DeleteOption{
func(_ context.Context, _ *store.Secret) error {
return errBoom
},
},
},
want: want{
err: errBoom,
},
},
"SecretDeletedNoKVSupplied": {
reason: "Should delete the whole secret if no kv supplied as parameter.",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
*obj.(*corev1.Secret) = *fakeConnectionSecret(withData(fakeKV()))
return nil
}),
MockDelete: func(_ context.Context, _ client.Object, _ ...client.DeleteOption) error {
return nil
},
},
},
secret: &store.Secret{
ScopedName: store.ScopedName{
Name: fakeSecretName,
Scope: fakeSecretNamespace,
},
},
do: []store.DeleteOption{
func(_ context.Context, _ *store.Secret) error {
return nil
},
},
},
want: want{
err: nil,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ss := &SecretStore{
client: tc.args.client,
defaultNamespace: tc.args.defaultNamespace,
}
err := ss.DeleteKeyValues(context.Background(), tc.args.secret, tc.args.do...)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nss.DeleteKeyValues(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}
func TestNewSecretStore(t *testing.T) {
type args struct {
client resource.ClientApplicator
cfg v1.SecretStoreConfig
}
type want struct {
err error
}
cases := map[string]struct {
reason string
args
want
}{
"SuccessfulLocal": {
reason: "Should return no error after successfully building local Kubernetes secret store",
args: args{
client: resource.ClientApplicator{},
cfg: v1.SecretStoreConfig{
Type: &storeTypeKubernetes,
DefaultScope: "test-ns",
},
},
want: want{
err: nil,
},
},
"NoSecretWithRemoteKubeconfig": {
reason: "Should fail properly if configured kubeconfig secret does not exist",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(_ client.Object) error {
return kerrors.NewNotFound(schema.GroupResource{}, "kube-conn")
}),
},
},
cfg: v1.SecretStoreConfig{
Type: &storeTypeKubernetes,
DefaultScope: "test-ns",
Kubernetes: &v1.KubernetesSecretStoreConfig{
Auth: v1.KubernetesAuthConfig{
Source: v1.CredentialsSourceSecret,
CommonCredentialSelectors: v1.CommonCredentialSelectors{
SecretRef: &v1.SecretKeySelector{
SecretReference: v1.SecretReference{
Name: "kube-conn",
Namespace: "test-ns",
},
Key: "kubeconfig",
},
},
},
},
},
},
want: want{
err: errors.Wrap(errors.Wrap(errors.Wrap(kerrors.NewNotFound(schema.GroupResource{}, "kube-conn"), "cannot get credentials secret"), errExtractKubernetesAuthCreds), errBuildClient),
},
},
"InvalidRestConfigForRemote": {
reason: "Should fetch the configured kubeconfig and fail if it is not valid",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
*obj.(*corev1.Secret) = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "kube-conn",
Namespace: "test-ns",
},
Data: map[string][]byte{
"kubeconfig": []byte(`
apiVersion: v1
kind: Config
malformed
`),
},
}
return nil
}),
},
},
cfg: v1.SecretStoreConfig{
Type: &storeTypeKubernetes,
DefaultScope: "test-ns",
Kubernetes: &v1.KubernetesSecretStoreConfig{
Auth: v1.KubernetesAuthConfig{
Source: v1.CredentialsSourceSecret,
CommonCredentialSelectors: v1.CommonCredentialSelectors{
SecretRef: &v1.SecretKeySelector{
SecretReference: v1.SecretReference{
Name: "kube-conn",
Namespace: "test-ns",
},
Key: "kubeconfig",
},
},
},
},
},
},
want: want{
err: errors.Wrap(errors.Wrap(errors.New("yaml: line 5: could not find expected ':'"), errBuildRestConfig), errBuildClient),
},
},
"InvalidKubeconfigForRemote": {
reason: "Should fetch the configured kubeconfig and fail if it is not valid",
args: args{
client: resource.ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
*obj.(*corev1.Secret) = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "kube-conn",
Namespace: "test-ns",
},
Data: map[string][]byte{
"kubeconfig": []byte(`
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: TEST
server: https://127.0.0.1:64695
name: kind-kind
contexts:
- context:
cluster: kind-kind
namespace: crossplane-system
user: kind-kind
name: kind-kind
current-context: kind-kind
kind: Config
users:
- name: kind-kind
user: {}
`),
},
}
return nil
}),
},
},
cfg: v1.SecretStoreConfig{
Type: &storeTypeKubernetes,
DefaultScope: "test-ns",
Kubernetes: &v1.KubernetesSecretStoreConfig{
Auth: v1.KubernetesAuthConfig{
Source: v1.CredentialsSourceSecret,
CommonCredentialSelectors: v1.CommonCredentialSelectors{
SecretRef: &v1.SecretKeySelector{
SecretReference: v1.SecretReference{
Name: "kube-conn",
Namespace: "test-ns",
},
Key: "kubeconfig",
},
},
},
},
},
},
want: want{
err: errors.Wrap(errors.New("unable to load root certificates: unable to parse bytes as PEM block"), errBuildClient),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
_, err := NewSecretStore(context.Background(), tc.args.client, nil, tc.args.cfg)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nNewSecretStore(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}
type secretOption func(*corev1.Secret)
func withType(t corev1.SecretType) secretOption {
return func(s *corev1.Secret) {
s.Type = t
}
}
func withData(d map[string][]byte) secretOption {
return func(s *corev1.Secret) {
s.Data = d
}
}
func withLabels(l map[string]string) secretOption {
return func(s *corev1.Secret) {
s.Labels = l
}
}
func withAnnotations(a map[string]string) secretOption {
return func(s *corev1.Secret) {
s.Annotations = a
}
}
func withOwnerID(id string) secretOption {
return func(s *corev1.Secret) {
s.SetOwnerReferences([]metav1.OwnerReference{
{
UID: types.UID(id),
},
})
}
}
func fakeConnectionSecret(opts ...secretOption) *corev1.Secret {
s := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: fakeSecretName,
Namespace: fakeSecretNamespace,
},
Type: resource.SecretTypeConnection,
}
for _, o := range opts {
o(s)
}
return s
}

View File

@ -1,46 +0,0 @@
/*
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 fake is a fake ExternalSecretStorePluginServiceClient.
package fake
import (
"context"
"google.golang.org/grpc"
ess "github.com/crossplane/crossplane-runtime/apis/proto/v1alpha1"
)
// ExternalSecretStorePluginServiceClient is a fake ExternalSecretStorePluginServiceClient.
type ExternalSecretStorePluginServiceClient struct {
GetSecretFn func(context.Context, *ess.GetSecretRequest, ...grpc.CallOption) (*ess.GetSecretResponse, error)
ApplySecretFn func(context.Context, *ess.ApplySecretRequest, ...grpc.CallOption) (*ess.ApplySecretResponse, error)
DeleteKeysFn func(context.Context, *ess.DeleteKeysRequest, ...grpc.CallOption) (*ess.DeleteKeysResponse, error)
*ess.UnimplementedExternalSecretStorePluginServiceServer
}
// GetSecret returns the secret.
func (e *ExternalSecretStorePluginServiceClient) GetSecret(ctx context.Context, req *ess.GetSecretRequest, _ ...grpc.CallOption) (*ess.GetSecretResponse, error) {
return e.GetSecretFn(ctx, req)
}
// ApplySecret applies the secret.
func (e *ExternalSecretStorePluginServiceClient) ApplySecret(ctx context.Context, req *ess.ApplySecretRequest, _ ...grpc.CallOption) (*ess.ApplySecretResponse, error) {
return e.ApplySecretFn(ctx, req)
}
// DeleteKeys deletes the secret keys.
func (e *ExternalSecretStorePluginServiceClient) DeleteKeys(ctx context.Context, req *ess.DeleteKeysRequest, _ ...grpc.CallOption) (*ess.DeleteKeysResponse, error) {
return e.DeleteKeysFn(ctx, req)
}

View File

@ -1,145 +0,0 @@
/*
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 plugin implements a gRPC client for external secret store plugins.
package plugin
import (
"context"
"crypto/tls"
"path/filepath"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
essproto "github.com/crossplane/crossplane-runtime/apis/proto/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
"github.com/crossplane/crossplane-runtime/pkg/errors"
)
// Error strings.
const (
errGet = "cannot get secret"
errApply = "cannot apply secret"
errDelete = "cannot delete secret"
errFmtCannotDial = "cannot dial to the endpoint: %s"
)
// SecretStore is an External Secret Store.
type SecretStore struct {
client essproto.ExternalSecretStorePluginServiceClient
kubeClient client.Client
config *v1.Config
defaultScope string
}
// NewSecretStore returns a new External SecretStore.
func NewSecretStore(_ context.Context, kube client.Client, tcfg *tls.Config, cfg v1.SecretStoreConfig) (*SecretStore, error) {
creds := credentials.NewTLS(tcfg)
conn, err := grpc.NewClient(cfg.Plugin.Endpoint, grpc.WithTransportCredentials(creds))
if err != nil {
return nil, errors.Wrapf(err, errFmtCannotDial, cfg.Plugin.Endpoint)
}
return &SecretStore{
kubeClient: kube,
client: essproto.NewExternalSecretStorePluginServiceClient(conn),
config: &cfg.Plugin.ConfigRef,
defaultScope: cfg.DefaultScope,
}, nil
}
// ReadKeyValues reads and returns key value pairs for a given Secret.
func (ss *SecretStore) ReadKeyValues(ctx context.Context, n store.ScopedName, s *store.Secret) error {
resp, err := ss.client.GetSecret(ctx, &essproto.GetSecretRequest{Secret: &essproto.Secret{ScopedName: ss.getScopedName(n)}, Config: ss.getConfigReference()})
if err != nil {
return errors.Wrap(err, errGet)
}
s.ScopedName = n
respSecret := resp.GetSecret()
if respSecret == nil {
return nil
}
respSecretData := respSecret.GetData()
s.Data = make(map[string][]byte, len(respSecretData))
for d := range respSecretData {
s.Data[d] = respSecretData[d]
}
respSecretMetadata := respSecret.GetMetadata()
if len(respSecretMetadata) != 0 {
s.Metadata = new(v1.ConnectionSecretMetadata)
s.Metadata.Labels = make(map[string]string, len(respSecretMetadata))
for k, v := range respSecretMetadata {
s.Metadata.Labels[k] = v
}
}
return nil
}
// WriteKeyValues writes key value pairs to a given Secret.
func (ss *SecretStore) WriteKeyValues(ctx context.Context, s *store.Secret, _ ...store.WriteOption) (changed bool, err error) {
sec := &essproto.Secret{}
sec.ScopedName = ss.getScopedName(s.ScopedName)
sec.Data = make(map[string][]byte, len(s.Data))
for k, v := range s.Data {
sec.Data[k] = v
}
if s.Metadata != nil && len(s.Metadata.Labels) != 0 {
sec.Metadata = make(map[string]string, len(s.Metadata.Labels))
for k, v := range s.Metadata.Labels {
sec.Metadata[k] = v
}
}
resp, err := ss.client.ApplySecret(ctx, &essproto.ApplySecretRequest{Secret: sec, Config: ss.getConfigReference()})
if err != nil {
return false, errors.Wrap(err, errApply)
}
return resp.GetChanged(), nil
}
// DeleteKeyValues delete key value pairs from a given Secret.
func (ss *SecretStore) DeleteKeyValues(ctx context.Context, s *store.Secret, _ ...store.DeleteOption) error {
_, err := ss.client.DeleteKeys(ctx, &essproto.DeleteKeysRequest{Secret: &essproto.Secret{ScopedName: ss.getScopedName(s.ScopedName)}, Config: ss.getConfigReference()})
return errors.Wrap(err, errDelete)
}
func (ss *SecretStore) getConfigReference() *essproto.ConfigReference {
return &essproto.ConfigReference{
ApiVersion: ss.config.APIVersion,
Kind: ss.config.Kind,
Name: ss.config.Name,
}
}
func (ss *SecretStore) getScopedName(n store.ScopedName) string {
if n.Scope == "" {
n.Scope = ss.defaultScope
}
return filepath.Join(n.Scope, n.Name)
}

View File

@ -1,279 +0,0 @@
/*
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 plugin
import (
"context"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
ess "github.com/crossplane/crossplane-runtime/apis/proto/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
"github.com/crossplane/crossplane-runtime/pkg/connection/store/plugin/fake"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
const (
parentPath = "crossplane-system"
secretName = "ess-test-secret"
)
var errBoom = errors.New("boom")
func TestReadKeyValues(t *testing.T) {
type args struct {
sn store.ScopedName
client ess.ExternalSecretStorePluginServiceClient
}
type want struct {
out *store.Secret
err error
}
cases := map[string]struct {
reason string
args
want
}{
"ErrorWhileGetting": {
reason: "Should return a proper error if secret cannot be obtained",
args: args{
client: &fake.ExternalSecretStorePluginServiceClient{
GetSecretFn: func(_ context.Context, _ *ess.GetSecretRequest, _ ...grpc.CallOption) (*ess.GetSecretResponse, error) {
return nil, errBoom
},
},
},
want: want{
out: &store.Secret{},
err: errors.Wrap(errBoom, errGet),
},
},
"SuccessfulGet": {
reason: "Should return key values from a secret with scope",
args: args{
sn: store.ScopedName{
Name: secretName,
Scope: parentPath,
},
client: &fake.ExternalSecretStorePluginServiceClient{
GetSecretFn: func(_ context.Context, req *ess.GetSecretRequest, _ ...grpc.CallOption) (*ess.GetSecretResponse, error) {
if diff := cmp.Diff(filepath.Join(parentPath, secretName), req.GetSecret().GetScopedName()); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
sec := &ess.Secret{
ScopedName: req.GetSecret().GetScopedName(),
Data: map[string][]byte{
"data1": []byte("val1"),
"data2": []byte("val2"),
},
Metadata: map[string]string{
"meta1": "val1",
"meta2": "val2",
},
}
res := &ess.GetSecretResponse{
Secret: sec,
}
return res, nil
},
},
},
want: want{
out: &store.Secret{
ScopedName: store.ScopedName{
Name: secretName,
Scope: parentPath,
},
Data: map[string][]byte{
"data1": []byte("val1"),
"data2": []byte("val2"),
},
Metadata: &v1.ConnectionSecretMetadata{
Labels: map[string]string{
"meta1": "val1",
"meta2": "val2",
},
},
},
err: nil,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
ss := &SecretStore{
client: tc.args.client,
config: &v1.Config{
APIVersion: "v1alpha1",
Kind: "VaultConfig",
Name: "ess-test",
},
}
s := &store.Secret{}
err := ss.ReadKeyValues(ctx, tc.args.sn, s)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nss.ReadKeyValues(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.out, s); diff != "" {
t.Errorf("\n%s\nss.ReadKeyValues(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestWriteKeyValues(t *testing.T) {
type args struct {
client ess.ExternalSecretStorePluginServiceClient
}
type want struct {
isChanged bool
err error
}
cases := map[string]struct {
reason string
args
want
}{
"ErrorWhileWriting": {
reason: "Should return a proper error if secret cannot be applied",
args: args{
client: &fake.ExternalSecretStorePluginServiceClient{
ApplySecretFn: func(_ context.Context, _ *ess.ApplySecretRequest, _ ...grpc.CallOption) (*ess.ApplySecretResponse, error) {
return nil, errBoom
},
},
},
want: want{
isChanged: false,
err: errors.Wrap(errBoom, errApply),
},
},
"SuccessfulWrite": {
reason: "Should return isChanged true",
args: args{
client: &fake.ExternalSecretStorePluginServiceClient{
ApplySecretFn: func(_ context.Context, _ *ess.ApplySecretRequest, _ ...grpc.CallOption) (*ess.ApplySecretResponse, error) {
resp := &ess.ApplySecretResponse{
Changed: true,
}
return resp, nil
},
},
},
want: want{
isChanged: true,
err: nil,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
ss := &SecretStore{
client: tc.args.client,
config: &v1.Config{
APIVersion: "v1alpha1",
Kind: "VaultConfig",
Name: "ess-test",
},
}
s := &store.Secret{}
isChanged, err := ss.WriteKeyValues(ctx, s)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nss.WriteKeyValues(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.isChanged, isChanged); diff != "" {
t.Errorf("\n%s\nss.WriteKeyValues(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestDeleteKeyValues(t *testing.T) {
type args struct {
client ess.ExternalSecretStorePluginServiceClient
}
type want struct {
err error
}
cases := map[string]struct {
reason string
args
want
}{
"ErrorWhileDeleting": {
reason: "Should return a proper error if key values cannot be deleted",
args: args{
client: &fake.ExternalSecretStorePluginServiceClient{
DeleteKeysFn: func(_ context.Context, _ *ess.DeleteKeysRequest, _ ...grpc.CallOption) (*ess.DeleteKeysResponse, error) {
return nil, errBoom
},
},
},
want: want{
err: errors.Wrap(errBoom, errDelete),
},
},
"SuccessfulDelete": {
reason: "Should not return error",
args: args{
client: &fake.ExternalSecretStorePluginServiceClient{
DeleteKeysFn: func(_ context.Context, _ *ess.DeleteKeysRequest, _ ...grpc.CallOption) (*ess.DeleteKeysResponse, error) {
return nil, nil
},
},
},
want: want{
err: nil,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
ss := &SecretStore{
client: tc.args.client,
config: &v1.Config{
APIVersion: "v1alpha1",
Kind: "VaultConfig",
Name: "ess-test",
},
}
s := &store.Secret{}
err := ss.DeleteKeyValues(ctx, s)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nss.DeletKeyValues(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}

View File

@ -1,92 +0,0 @@
/*
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 store implements secret stores.
package store
import (
"context"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
// SecretOwner owns a Secret.
type SecretOwner interface {
resource.Object
resource.ConnectionDetailsPublisherTo
}
// KeyValues is a map with sensitive values.
type KeyValues map[string][]byte
// ScopedName is scoped name of a secret.
type ScopedName struct {
Name string
Scope string
}
// A Secret is an entity representing a set of sensitive Key Values.
type Secret struct {
ScopedName
Metadata *v1.ConnectionSecretMetadata
Data KeyValues
}
// NewSecret returns a new Secret owned by supplied SecretOwner and with
// supplied data.
func NewSecret(so SecretOwner, data KeyValues) *Secret {
if so.GetPublishConnectionDetailsTo() == nil {
return nil
}
p := so.GetPublishConnectionDetailsTo()
if p.Metadata == nil {
p.Metadata = &v1.ConnectionSecretMetadata{}
}
p.Metadata.SetOwnerUID(so.GetUID())
return &Secret{
ScopedName: ScopedName{
Name: p.Name,
Scope: so.GetNamespace(),
},
Metadata: p.Metadata,
Data: data,
}
}
// GetOwner returns the UID of the owner of secret.
func (s *Secret) GetOwner() string {
if s.Metadata == nil {
return ""
}
return s.Metadata.GetOwnerUID()
}
// GetLabels returns the labels of the secret.
func (s *Secret) GetLabels() map[string]string {
if s.Metadata == nil {
return nil
}
return s.Metadata.Labels
}
// A WriteOption is called before writing the desired secret over the
// current object.
type WriteOption func(ctx context.Context, current, desired *Secret) error
// An DeleteOption is called before deleting the secret.
type DeleteOption func(ctx context.Context, secret *Secret) error

View File

@ -1,47 +0,0 @@
/*
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 connection
import (
"context"
"crypto/tls"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/connection/store/kubernetes"
"github.com/crossplane/crossplane-runtime/pkg/connection/store/plugin"
"github.com/crossplane/crossplane-runtime/pkg/errors"
)
const (
errFmtUnknownSecretStore = "unknown secret store type: %q"
)
// RuntimeStoreBuilder builds and returns a Store for any supported Store type
// in a given config.
//
// All in-tree connection Store implementations needs to be registered here.
func RuntimeStoreBuilder(ctx context.Context, local client.Client, tcfg *tls.Config, cfg v1.SecretStoreConfig) (Store, error) {
switch *cfg.Type {
case v1.SecretStoreKubernetes:
return kubernetes.NewSecretStore(ctx, local, nil, cfg)
case v1.SecretStorePlugin:
return plugin.NewSecretStore(ctx, local, tcfg, cfg)
}
return nil, errors.Errorf(errFmtUnknownSecretStore, *cfg.Type)
}

15
pkg/controller/gate.go Normal file
View File

@ -0,0 +1,15 @@
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
}

View File

@ -69,6 +69,9 @@ type Options struct {
// ChangeLogOptions for recording change logs.
ChangeLogOptions *ChangeLogOptions
// Gate implements a gated function callback pattern.
Gate Gate
}
// ForControllerRuntime extracts options for controller-runtime.

View File

@ -85,6 +85,7 @@ func WithMessage(err error, message string) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", message, err)
}
@ -94,6 +95,7 @@ func WithMessagef(err error, format string, args ...any) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}
@ -120,6 +122,7 @@ func Cause(err error) error {
if !ok {
return err
}
err = w.Unwrap()
}
@ -146,6 +149,7 @@ func Join(errs ...error) MultiError {
if err == nil {
return nil
}
return multiError{aggregate: err}
}

View File

@ -29,6 +29,7 @@ func TestWrap(t *testing.T) {
err error
message string
}
cases := map[string]struct {
args args
want error
@ -65,6 +66,7 @@ func TestWrapf(t *testing.T) {
message string
args []any
}
cases := map[string]struct {
args args
want error

View File

@ -30,6 +30,7 @@ func SilentlyRequeueOnConflict(result reconcile.Result, err error) (reconcile.Re
if kerrors.IsConflict(Cause(err)) {
return reconcile.Result{Requeue: true}, nil
}
return result, err
}

View File

@ -33,10 +33,12 @@ func TestSilentlyRequeueOnConflict(t *testing.T) {
result reconcile.Result
err error
}
type want struct {
result reconcile.Result
err error
}
tests := []struct {
reason string
args args
@ -91,6 +93,7 @@ func TestSilentlyRequeueOnConflict(t *testing.T) {
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)
}

View File

@ -52,6 +52,7 @@ func Normal(r Reason, message string, keysAndValues ...string) Event {
Annotations: map[string]string{},
}
sliceMap(keysAndValues, e.Annotations)
return e
}
@ -64,6 +65,7 @@ func Warning(r Reason, err error, keysAndValues ...string) Event {
Annotations: map[string]string{},
}
sliceMap(keysAndValues, e.Annotations)
return e
}
@ -77,17 +79,27 @@ type Recorder interface {
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) *APIRecorder {
return &APIRecorder{kube: r, annotations: map[string]string{}}
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) {
r.kube.AnnotatedEventf(obj, r.annotations, string(e.Type), string(e.Reason), e.Message)
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
@ -97,7 +109,9 @@ func (r *APIRecorder) WithAnnotations(keysAndValues ...string) Recorder {
for k, v := range r.annotations {
ar.annotations[k] = v
}
sliceMap(keysAndValues, ar.annotations)
return ar
}

View File

@ -28,6 +28,7 @@ func TestSliceMap(t *testing.T) {
from []string
to map[string]string
}
cases := map[string]struct {
reason string
args args

View File

@ -33,9 +33,11 @@ type Flags struct {
// 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()
}
@ -45,7 +47,9 @@ func (fs *Flags) Enabled(f Flag) bool {
if fs == nil {
return false
}
fs.m.RLock()
defer fs.m.RUnlock()
return fs.enabled[f]
}

View File

@ -82,6 +82,7 @@ func (sg Segments) String() string {
b.WriteString(fmt.Sprintf("[%s]", s.Field))
continue
}
b.WriteString(fmt.Sprintf(".%s", s.Field))
case SegmentIndex:
b.WriteString(fmt.Sprintf("[%d]", s.Index))
@ -120,6 +121,7 @@ func Parse(path string) (Segments, error) {
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:
@ -130,6 +132,7 @@ func Parse(path string) (Segments, error) {
return nil, errors.Errorf("%s at position %d", i.val, i.pos)
}
}
return segments, nil
}
@ -174,6 +177,7 @@ func (l *lexer) run() {
for state := lexField; state != nil; {
state = state(l)
}
close(l.items)
}
@ -182,7 +186,9 @@ func (l *lexer) emit(t itemType) {
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
}
@ -202,12 +208,14 @@ func lexField(l *lexer) stateFn {
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
}
}
@ -216,6 +224,7 @@ func lexField(l *lexer) stateFn {
l.pos = len(l.input)
l.emit(itemField)
l.emit(itemEOL)
return nil
}
@ -234,6 +243,7 @@ func lexPeriod(l *lexer) stateFn {
if r == period {
return l.errorf(l.pos, "unexpected %q", period)
}
if r == leftBracket {
return l.errorf(l.pos, "unexpected %q", leftBracket)
}
@ -249,6 +259,7 @@ func lexLeftBracket(l *lexer) stateFn {
l.pos += utf8.RuneLen(leftBracket)
l.emit(itemLeftBracket)
return lexFieldOrIndex
}
@ -272,11 +283,13 @@ func lexFieldOrIndex(l *lexer) stateFn {
// 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
}

View File

@ -299,6 +299,7 @@ func TestParse(t *testing.T) {
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)
}

View File

@ -57,6 +57,7 @@ func merge(dst, src any, mergeOptions *xpv1.MergeOptions) (any, error) {
// 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,
@ -77,6 +78,7 @@ func merge(dst, src any, mergeOptions *xpv1.MergeOptions) (any, error) {
if err := mergo.Merge(&mDst, argWrap(src), mergeOptions.MergoConfiguration()...); err != nil {
return nil, errors.Wrap(err, errInvalidMerge)
}
return mDst[keyArg], nil
}
@ -85,9 +87,11 @@ func removeSourceDuplicates(dst, src any) any {
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
}
@ -95,6 +99,7 @@ func removeSourceDuplicates(dst, src any) any {
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
@ -102,10 +107,12 @@ func removeSourceDuplicates(dst, src any) any {
found = true
}
}
if !found {
// then put src item into result
result = reflect.Append(result, itemSrc)
}
}
return result.Interface()
}

View File

@ -36,6 +36,7 @@ func TestMergeValue(t *testing.T) {
valSrc2 = "e1-from-source-2"
valDst = "e1-from-destination"
)
formatArr := func(arr []string) string {
return fmt.Sprintf(`{"%s": ["%s"]}`, pathTest, strings.Join(arr, `", "`))
}
@ -59,15 +60,18 @@ func TestMergeValue(t *testing.T) {
type fields struct {
object map[string]any
}
type args struct {
path string
value any
mo *xpv1.MergeOptions
}
type want struct {
serialized string
err error
}
tests := map[string]struct {
reason string
fields fields
@ -199,11 +203,13 @@ func TestMergeValue(t *testing.T) {
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)

View File

@ -44,6 +44,7 @@ func IsNotFound(err error) bool {
_, ok := cause.(interface {
IsNotFound() bool
})
return ok
}
@ -100,6 +101,7 @@ func (p *Paved) UnstructuredContent() map[string]any {
if p.object == nil {
return make(map[string]any)
}
return p.object
}
@ -115,18 +117,22 @@ func (p *Paved) getValue(s Segments) (any, error) {
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) {
@ -135,9 +141,11 @@ func getValueFromInterface(it any, s Segments) (any, error) {
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])}
@ -165,14 +173,17 @@ func (p *Paved) ExpandWildcards(path string) ([]string, error) {
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
}
@ -180,7 +191,9 @@ func expandWildcards(data any, segments Segments) ([]Segments, error) { //nolint
// 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 {
@ -190,10 +203,12 @@ func expandWildcards(data any, segments Segments) ([]Segments, error) { //nolint
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:
@ -201,10 +216,12 @@ func expandWildcards(data any, segments Segments) ([]Segments, error) { //nolint
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:
@ -212,17 +229,22 @@ func expandWildcards(data any, segments Segments) ([]Segments, error) { //nolint
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
}
@ -242,10 +264,12 @@ func (p *Paved) GetValueInto(path string, out any) error {
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")
}
@ -260,6 +284,7 @@ func (p *Paved) GetString(path string) (string, error) {
if !ok {
return "", errors.Errorf("%s: not a string", path)
}
return s, nil
}
@ -281,6 +306,7 @@ func (p *Paved) GetStringArray(path string) ([]string, error) {
if !ok {
return nil, errors.Errorf("%s: not an array of strings", path)
}
sa[i] = s
}
@ -305,6 +331,7 @@ func (p *Paved) GetStringObject(path string) (map[string]string, error) {
if !ok {
return nil, errors.Errorf("%s: not an object with string field values", path)
}
so[k] = s
}
@ -322,6 +349,7 @@ func (p *Paved) GetBool(path string) (bool, error) {
if !ok {
return false, errors.Errorf("%s: not a bool", path)
}
return b, nil
}
@ -336,6 +364,7 @@ func (p *Paved) GetInteger(path string) (int64, error) {
if !ok {
return 0, errors.Errorf("%s: not a (int64) number", path)
}
return f, nil
}
@ -354,6 +383,7 @@ func (p *Paved) setValue(s Segments, value any) error {
}
var in any = p.object
for i, current := range s {
final := i == len(s)-1
@ -393,13 +423,16 @@ func (p *Paved) setValue(s Segments, value any) error {
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
}
@ -413,6 +446,7 @@ func prepareElement(array []any, current, next Segment) {
case SegmentField:
array[current.Index] = make(map[string]any)
}
return
}
@ -444,6 +478,7 @@ func prepareField(object map[string]any, current, next Segment) {
case SegmentField:
object[current.Field] = make(map[string]any)
}
return
}
@ -471,6 +506,7 @@ func (p *Paved) SetValue(path string, value any) error {
if err != nil {
return errors.Wrapf(err, "cannot parse path %q", path)
}
return p.setValue(segments, value)
}
@ -478,11 +514,13 @@ 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
}
@ -511,6 +549,7 @@ func (p *Paved) DeleteField(path string) error {
if err != nil {
return errors.Wrapf(err, "cannot parse path %q", path)
}
return p.delete(segments)
}
@ -522,10 +561,14 @@ func (p *Paved) delete(segments Segments) error { //nolint:gocognit // See note
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
@ -535,6 +578,7 @@ func (p *Paved) delete(segments Segments) error { //nolint:gocognit // See note
// 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)
@ -552,7 +596,9 @@ func (p *Paved) delete(segments Segments) error { //nolint:gocognit // See note
if err != nil {
return errors.Wrapf(err, "cannot delete %s", segments)
}
array[current.Index] = o
return nil
}
@ -573,13 +619,16 @@ func (p *Paved) delete(segments Segments) error { //nolint:gocognit // See note
if err != nil {
return errors.Wrapf(err, "cannot delete %s", segments)
}
object[current.Field] = o
return nil
}
in = object[current.Field]
}
}
return nil
}
@ -593,20 +642,26 @@ func deleteField(obj any, s Segment) (any, error) {
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
}

View File

@ -68,6 +68,7 @@ func TestGetValue(t *testing.T) {
value any
err error
}
cases := map[string]struct {
reason string
path string
@ -181,6 +182,7 @@ func TestGetValue(t *testing.T) {
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)
}
@ -201,10 +203,12 @@ func TestGetValueInto(t *testing.T) {
path string
out any
}
type want struct {
out any
err error
}
cases := map[string]struct {
reason string
data []byte
@ -257,6 +261,7 @@ func TestGetValueInto(t *testing.T) {
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)
}
@ -269,6 +274,7 @@ func TestGetString(t *testing.T) {
value string
err error
}
cases := map[string]struct {
reason string
path string
@ -310,6 +316,7 @@ func TestGetString(t *testing.T) {
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)
}
@ -322,6 +329,7 @@ func TestGetStringArray(t *testing.T) {
value []string
err error
}
cases := map[string]struct {
reason string
path string
@ -371,6 +379,7 @@ func TestGetStringArray(t *testing.T) {
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)
}
@ -383,6 +392,7 @@ func TestGetStringObject(t *testing.T) {
value map[string]string
err error
}
cases := map[string]struct {
reason string
path string
@ -432,6 +442,7 @@ func TestGetStringObject(t *testing.T) {
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)
}
@ -444,6 +455,7 @@ func TestGetBool(t *testing.T) {
value bool
err error
}
cases := map[string]struct {
reason string
path string
@ -485,6 +497,7 @@ func TestGetBool(t *testing.T) {
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)
}
@ -497,6 +510,7 @@ func TestGetInteger(t *testing.T) {
value int64
err error
}
cases := map[string]struct {
reason string
path string
@ -538,6 +552,7 @@ func TestGetInteger(t *testing.T) {
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)
}
@ -551,10 +566,12 @@ func TestSetValue(t *testing.T) {
value any
opts []PavedOption
}
type want struct {
object map[string]any
err error
}
cases := map[string]struct {
reason string
data []byte
@ -814,6 +831,7 @@ func TestSetValue(t *testing.T) {
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)
}
@ -826,6 +844,7 @@ func TestExpandWildcards(t *testing.T) {
expanded []string
err error
}
cases := map[string]struct {
reason string
path string
@ -979,6 +998,7 @@ func TestExpandWildcards(t *testing.T) {
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 != "" {
@ -992,10 +1012,12 @@ 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
@ -1267,6 +1289,7 @@ func TestDeleteField(t *testing.T) {
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)
}

106
pkg/gate/gate.go Normal file
View File

@ -0,0 +1,106 @@
/*
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
})
}

299
pkg/gate/gate_test.go Normal file
View File

@ -0,0 +1,299 @@
/*
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/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.Add(1)
go func() {
defer wg.Done()
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")
}
}

View File

@ -49,6 +49,7 @@ func (l *requestThrottlingFilter) klogToLogrLevel(klogLvl int) int {
if klogLvl >= 3 {
return klogLvl - 3
}
return 0
}

View File

@ -70,6 +70,7 @@ const (
// 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,
@ -83,6 +84,7 @@ func ReferenceTo(o metav1.Object, of schema.GroupVersionKind) *corev1.ObjectRefe
// presumed to be of the supplied group, version, and kind.
func TypedReferenceTo(o metav1.Object, of schema.GroupVersionKind) *xpv1.TypedReference {
v, k := of.ToAPIVersionAndKind()
return &xpv1.TypedReference{
APIVersion: v,
Kind: k,
@ -108,6 +110,7 @@ func AsController(r *xpv1.TypedReference) metav1.OwnerReference {
ref := AsOwner(r)
ref.Controller = &t
ref.BlockOwnerDeletion = &t
return ref
}
@ -139,9 +142,11 @@ func AddOwnerReference(o metav1.Object, r metav1.OwnerReference) {
if refs[i].UID == r.UID {
refs[i] = r
o.SetOwnerReferences(refs)
return
}
}
o.SetOwnerReferences(append(refs, r))
}
@ -154,6 +159,7 @@ func AddControllerReference(o metav1.Object, r metav1.OwnerReference) error {
}
AddOwnerReference(o, r)
return nil
}
@ -165,6 +171,7 @@ func AddFinalizer(o metav1.Object, finalizer string) {
return
}
}
o.SetFinalizers(append(f, finalizer))
}
@ -176,6 +183,7 @@ func RemoveFinalizer(o metav1.Object, finalizer string) {
f = append(f[:i], f[i+1:]...)
}
}
o.SetFinalizers(f)
}
@ -187,6 +195,7 @@ func FinalizerExists(o metav1.Object, finalizer string) bool {
return true
}
}
return false
}
@ -197,9 +206,11 @@ func AddLabels(o metav1.Object, labels map[string]string) {
o.SetLabels(labels)
return
}
for k, v := range labels {
l[k] = v
}
o.SetLabels(l)
}
@ -209,9 +220,11 @@ func RemoveLabels(o metav1.Object, labels ...string) {
if l == nil {
return
}
for _, k := range labels {
delete(l, k)
}
o.SetLabels(l)
}
@ -222,9 +235,11 @@ func AddAnnotations(o metav1.Object, annotations map[string]string) {
o.SetAnnotations(annotations)
return
}
for k, v := range annotations {
a[k] = v
}
o.SetAnnotations(a)
}
@ -234,9 +249,11 @@ func RemoveAnnotations(o metav1.Object, annotations ...string) {
if a == nil {
return
}
for _, k := range annotations {
delete(a, k)
}
o.SetAnnotations(a)
}
@ -267,10 +284,12 @@ func SetExternalName(o metav1.Object, name string) {
// 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
}
@ -284,10 +303,12 @@ func SetExternalCreatePending(o metav1.Object, t time.Time) {
// 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
}
@ -301,10 +322,12 @@ func SetExternalCreateSucceeded(o metav1.Object, t time.Time) {
// 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
}
@ -344,6 +367,7 @@ func ExternalCreateSucceededDuring(o metav1.Object, d time.Duration) bool {
if t.IsZero() {
return false
}
return time.Since(t) < d
}

View File

@ -46,6 +46,7 @@ func TestReferenceTo(t *testing.T) {
o metav1.Object
of schema.GroupVersionKind
}
tests := map[string]struct {
args
want *corev1.ObjectReference
@ -77,7 +78,7 @@ func TestReferenceTo(t *testing.T) {
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := ReferenceTo(tc.args.o, tc.args.of)
got := ReferenceTo(tc.o, tc.of)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("ReferenceTo(): -want, +got:\n%s", diff)
}
@ -90,6 +91,7 @@ func TestTypedReferenceTo(t *testing.T) {
o metav1.Object
of schema.GroupVersionKind
}
tests := map[string]struct {
args
want *xpv1.TypedReference
@ -120,7 +122,7 @@ func TestTypedReferenceTo(t *testing.T) {
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := TypedReferenceTo(tc.args.o, tc.args.of)
got := TypedReferenceTo(tc.o, tc.of)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("TypedReferenceTo(): -want, +got:\n%s", diff)
}
@ -895,6 +897,7 @@ func TestSetExternalName(t *testing.T) {
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)
}
@ -947,6 +950,7 @@ func TestSetExternalCreatePending(t *testing.T) {
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)
}
@ -999,6 +1003,7 @@ func TestSetExternalCreateSucceeded(t *testing.T) {
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)
}
@ -1051,6 +1056,7 @@ func TestSetExternalCreateFailed(t *testing.T) {
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)
}

View File

@ -62,6 +62,7 @@ func SkipDirs() FilterFn {
if info.IsDir() {
return true, nil
}
return false, nil
}
}
@ -79,6 +80,7 @@ func SkipNotYAML() FilterFn {
if filepath.Ext(path) != ".yaml" && filepath.Ext(path) != ".yml" {
return true, nil
}
return false, nil
}
}
@ -92,18 +94,23 @@ func NewFsReadCloser(fs afero.Fs, dir string, fns ...FilterFn) (*FsReadCloser, e
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,
@ -121,24 +128,31 @@ func (r *FsReadCloser) Read(p []byte) (n int, err error) {
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
}
@ -155,6 +169,7 @@ func (r *FsReadCloser) Annotate() any {
if index == len(r.paths) {
index--
}
return FsReadCloserAnnotation{
path: r.paths[index],
position: r.position,

View File

@ -78,6 +78,7 @@ func (l *PackageLinter) Lint(pkg Lintable) error {
return err
}
}
for _, o := range pkg.GetMeta() {
for _, fn := range l.perMeta {
if err := fn(o); err != nil {
@ -85,6 +86,7 @@ func (l *PackageLinter) Lint(pkg Lintable) error {
}
}
}
for _, o := range pkg.GetObjects() {
for _, fn := range l.perObject {
if err := fn(o); err != nil {
@ -92,6 +94,7 @@ func (l *PackageLinter) Lint(pkg Lintable) error {
}
}
}
return nil
}
@ -100,16 +103,20 @@ func (l *PackageLinter) Lint(pkg Lintable) 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, ", "))
}
}

View File

@ -119,7 +119,6 @@ func TestLinter(t *testing.T) {
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)
}
@ -175,7 +174,6 @@ func TestOr(t *testing.T) {
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)
}

View File

@ -105,21 +105,27 @@ func (p *PackageParser) Parse(_ context.Context, reader io.ReadCloser) (*Package
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
@ -127,15 +133,20 @@ func (p *PackageParser) Parse(_ context.Context, reader io.ReadCloser) (*Package
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
}
@ -151,6 +162,7 @@ func isEmptyYAML(y []byte) bool {
return false
}
}
return true
}
@ -159,6 +171,7 @@ func annotateErr(err error, reader io.ReadCloser) error {
if anno, ok := reader.(AnnotatedReadCloser); ok {
return errors.Wrapf(err, "%+v", anno.Annotate())
}
return err
}
@ -184,6 +197,7 @@ func NewPodLogBackend(bo ...BackendOption) *PodLogBackend {
for _, o := range bo {
o(p)
}
return p
}
@ -192,11 +206,14 @@ func (p *PodLogBackend) Init(ctx context.Context, bo ...BackendOption) (io.ReadC
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
}
@ -207,6 +224,7 @@ func PodName(name string) BackendOption {
if !ok {
return
}
pl.name = name
}
}
@ -218,6 +236,7 @@ func PodNamespace(namespace string) BackendOption {
if !ok {
return
}
pl.namespace = namespace
}
}
@ -229,6 +248,7 @@ func PodClient(client kubernetes.Interface) BackendOption {
if !ok {
return
}
pl.client = client
}
}
@ -261,6 +281,7 @@ func NewFsBackend(fs afero.Fs, bo ...BackendOption) *FsBackend {
for _, o := range bo {
o(f)
}
return f
}
@ -269,6 +290,7 @@ func (p *FsBackend) Init(_ context.Context, bo ...BackendOption) (io.ReadCloser,
for _, o := range bo {
o(p)
}
return NewFsReadCloser(p.fs, p.dir, p.skips...)
}
@ -279,6 +301,7 @@ func FsDir(dir string) BackendOption {
if !ok {
return
}
f.dir = dir
}
}
@ -290,6 +313,7 @@ func FsFilters(skips ...FilterFn) BackendOption {
if !ok {
return
}
f.skips = skips
}
}
@ -311,5 +335,6 @@ func (p *EchoBackend) Init(_ context.Context, bo ...BackendOption) (io.ReadClose
for _, o := range bo {
o(p)
}
return io.NopCloser(strings.NewReader(p.echo)), nil
}

View File

@ -195,18 +195,22 @@ func TestParser(t *testing.T) {
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 != "" {
@ -220,9 +224,11 @@ func TestCleanYAML(t *testing.T) {
type args struct {
in []byte
}
type want struct {
out bool
}
cases := map[string]struct {
reason string
args args

View File

@ -53,7 +53,9 @@ func (s Settings) Generate() (string, error) {
if err != nil {
return "", err
}
pw[i] = s.CharacterSet[n.Int64()]
}
return string(pw), nil
}

View File

@ -8,12 +8,13 @@ import (
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)
}

View File

@ -52,5 +52,6 @@ func LimitRESTConfig(cfg *rest.Config, rps int) *rest.Config {
out := rest.CopyConfig(cfg)
out.QPS = float32(rps * 5)
out.Burst = rps * 10
return out
}

View File

@ -56,7 +56,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
if d := r.when(req); d > 0 {
return reconcile.Result{RequeueAfter: d}, nil
}
r.limit.Forget(item)
return r.inner.Reconcile(ctx, req)
}
@ -78,6 +80,7 @@ func (r *Reconciler) when(req reconcile.Request) time.Duration {
r.limitedL.Lock()
delete(r.limited, item)
r.limitedL.Unlock()
return 0
}

View File

@ -41,6 +41,7 @@ func TestReconcile(t *testing.T) {
ctx context.Context
req reconcile.Request
}
type want struct {
res reconcile.Result
err error
@ -101,6 +102,7 @@ func TestReconcile(t *testing.T) {
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)
}

View File

@ -0,0 +1,85 @@
/*
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/pkg/controller"
"github.com/crossplane/crossplane-runtime/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
}

View File

@ -0,0 +1,557 @@
/*
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/pkg/logging"
"github.com/crossplane/crossplane-runtime/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 {
slices.SortFunc(tc.want.trueCalls, func(a, b schema.GroupVersionKind) int {
return strings.Compare(a.Kind, b.Kind)
})
slices.SortFunc(tc.fields.gate.TrueCalls, func(a, b schema.GroupVersionKind) int {
return strings.Compare(a.Kind, b.Kind)
})
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, func(a, b schema.GroupVersionKind) int {
return strings.Compare(a.Kind, b.Kind)
})
slices.SortFunc(tc.fields.gate.FalseCalls, func(a, b schema.GroupVersionKind) int {
return strings.Compare(a.Kind, b.Kind)
})
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)
}
}
})
}
}

View File

@ -0,0 +1,46 @@
/*
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/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))
}

View File

@ -70,7 +70,9 @@ func (a *NameAsExternalName) Initialize(ctx context.Context, mg resource.Managed
if meta.GetExternalName(mg) != "" {
return nil
}
meta.SetExternalName(mg, mg.GetName())
return errors.Wrap(a.client.Update(ctx, mg), errUpdateManaged)
}
@ -103,6 +105,7 @@ func (a *APISecretPublisher) PublishConnection(ctx context.Context, o resource.C
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 {
@ -116,6 +119,7 @@ func (a *APISecretPublisher) PublishConnection(ctx context.Context, o resource.C
// The update was not allowed because it was a no-op.
return false, nil
}
if err != nil {
return false, errors.Wrap(err, errCreateOrUpdateSecret)
}
@ -130,6 +134,65 @@ func (a *APISecretPublisher) UnpublishConnection(_ context.Context, _ resource.C
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.
@ -153,15 +216,19 @@ func prepareJSONMerge(existing, resolved runtime.Object) ([]byte, error) {
// 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)
}
@ -177,6 +244,7 @@ func (a *APISimpleReferenceResolver) ResolveReferences(ctx context.Context, mg r
}
existing := mg.DeepCopyObject()
if err := rr.ResolveReferences(ctx, a.client); err != nil {
return errors.Wrap(err, errResolveReferences)
}
@ -190,6 +258,7 @@ func (a *APISimpleReferenceResolver) ResolveReferences(ctx context.Context, mg r
if err != nil {
return err
}
return errors.Wrap(a.client.Patch(ctx, mg, client.RawPatch(types.ApplyPatchType, patch), client.FieldOwner(fieldOwnerAPISimpleRefResolver), client.ForceOwnership), errPatchManaged)
}
@ -221,9 +290,12 @@ func (u *RetryingCriticalAnnotationUpdater) UpdateCriticalAnnotations(ctx contex
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)
}

View File

@ -35,7 +35,11 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var _ Initializer = &NameAsExternalName{}
var (
_ Initializer = &NameAsExternalName{}
_ ConnectionPublisher = &APISecretPublisher{}
_ LocalConnectionPublisher = &APILocalSecretPublisher{}
)
func TestNameAsExternalName(t *testing.T) {
type args struct {
@ -61,11 +65,11 @@ func TestNameAsExternalName(t *testing.T) {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(errBoom)},
args: args{
ctx: context.Background(),
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{Name: testExternalName}},
mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{Name: testExternalName}},
},
want: want{
err: errors.Wrap(errBoom, errUpdateManaged),
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{
Name: testExternalName,
Annotations: map[string]string{meta.AnnotationKeyExternalName: testExternalName},
}},
@ -75,11 +79,11 @@ func TestNameAsExternalName(t *testing.T) {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)},
args: args{
ctx: context.Background(),
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{Name: testExternalName}},
mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{Name: testExternalName}},
},
want: want{
err: nil,
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{
Name: testExternalName,
Annotations: map[string]string{meta.AnnotationKeyExternalName: testExternalName},
}},
@ -88,14 +92,14 @@ func TestNameAsExternalName(t *testing.T) {
"UpdateNotNeeded": {
args: args{
ctx: context.Background(),
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{
Name: testExternalName,
Annotations: map[string]string{meta.AnnotationKeyExternalName: "some-name"},
}},
},
want: want{
err: nil,
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
mg: &fake.LegacyManaged{ObjectMeta: metav1.ObjectMeta{
Name: testExternalName,
Annotations: map[string]string{meta.AnnotationKeyExternalName: "some-name"},
}},
@ -106,10 +110,12 @@ func TestNameAsExternalName(t *testing.T) {
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)
}
@ -120,7 +126,7 @@ func TestNameAsExternalName(t *testing.T) {
func TestAPISecretPublisher(t *testing.T) {
errBoom := errors.New("boom")
mg := &fake.Managed{
mg := &fake.LegacyManaged{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: &xpv1.SecretReference{
Namespace: "coolnamespace",
Name: "coolsecret",
@ -136,7 +142,7 @@ func TestAPISecretPublisher(t *testing.T) {
type args struct {
ctx context.Context
mg resource.Managed
mg resource.LegacyManaged
c ConnectionDetails
}
@ -144,6 +150,7 @@ func TestAPISecretPublisher(t *testing.T) {
err error
published bool
}
cases := map[string]struct {
reason string
fields fields
@ -154,14 +161,14 @@ func TestAPISecretPublisher(t *testing.T) {
reason: "A managed resource with a nil GetWriteConnectionSecretToReference should not publish a secret",
args: args{
ctx: context.Background(),
mg: &fake.Managed{},
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.Managed{}),
typer: fake.SchemeWith(&fake.LegacyManaged{}),
},
args: args{
ctx: context.Background(),
@ -184,7 +191,7 @@ func TestAPISecretPublisher(t *testing.T) {
}
return nil
}),
typer: fake.SchemeWith(&fake.Managed{}),
typer: fake.SchemeWith(&fake.LegacyManaged{}),
},
args: args{
ctx: context.Background(),
@ -207,7 +214,7 @@ func TestAPISecretPublisher(t *testing.T) {
}
return nil
}),
typer: fake.SchemeWith(&fake.Managed{}),
typer: fake.SchemeWith(&fake.LegacyManaged{}),
},
args: args{
ctx: context.Background(),
@ -223,10 +230,131 @@ func TestAPISecretPublisher(t *testing.T) {
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: &xpv1.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)
}
@ -255,7 +383,7 @@ func (r *mockSimpleReferencer) Equal(s *mockSimpleReferencer) bool {
func TestResolveReferences(t *testing.T) {
errBoom := errors.New("boom")
different := &fake.Managed{}
different := &fake.LegacyManaged{}
type args struct {
ctx context.Context
@ -272,7 +400,7 @@ func TestResolveReferences(t *testing.T) {
reason: "Should return early without error when the managed resource has no references.",
args: args{
ctx: context.Background(),
mg: &fake.Managed{},
mg: &fake.LegacyManaged{},
},
want: nil,
},
@ -284,7 +412,7 @@ func TestResolveReferences(t *testing.T) {
args: args{
ctx: context.Background(),
mg: &mockSimpleReferencer{
Managed: &fake.Managed{},
Managed: &fake.LegacyManaged{},
MockResolveReferences: func(context.Context, client.Reader) error {
return errBoom
},
@ -300,7 +428,7 @@ func TestResolveReferences(t *testing.T) {
args: args{
ctx: context.Background(),
mg: &mockSimpleReferencer{
Managed: &fake.Managed{},
Managed: &fake.LegacyManaged{},
MockResolveReferences: func(context.Context, client.Reader) error {
return nil
},
@ -347,6 +475,7 @@ func TestResolveReferences(t *testing.T) {
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)
@ -360,6 +489,7 @@ func TestPrepareJSONMerge(t *testing.T) {
existing runtime.Object
resolved runtime.Object
}
type want struct {
patch string
err error
@ -373,8 +503,8 @@ func TestPrepareJSONMerge(t *testing.T) {
"SuccessfulPatch": {
reason: "Should successfully compute the JSON merge patch document.",
args: args{
existing: &fake.Managed{},
resolved: &fake.Managed{
existing: &fake.LegacyManaged{},
resolved: &fake.LegacyManaged{
ObjectMeta: metav1.ObjectMeta{
Name: "resolved",
},
@ -392,6 +522,7 @@ func TestPrepareJSONMerge(t *testing.T) {
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)
}
@ -406,6 +537,7 @@ func TestRetryingCriticalAnnotationUpdater(t *testing.T) {
ctx context.Context
o client.Object
}
type want struct {
err error
o client.Object
@ -415,7 +547,7 @@ func TestRetryingCriticalAnnotationUpdater(t *testing.T) {
obj.SetLabels(map[string]string{"getcalled": "true"})
return nil
}
objectReturnedByGet := &fake.Managed{}
objectReturnedByGet := &fake.LegacyManaged{}
setLabels(objectReturnedByGet)
cases := map[string]struct {
@ -434,7 +566,7 @@ func TestRetryingCriticalAnnotationUpdater(t *testing.T) {
}, "abc", errBoom)),
},
args: args{
o: &fake.Managed{},
o: &fake.LegacyManaged{},
},
want: want{
err: errors.Wrap(errBoom, errUpdateCriticalAnnotations),
@ -448,11 +580,11 @@ func TestRetryingCriticalAnnotationUpdater(t *testing.T) {
MockUpdate: test.NewMockUpdateFn(errBoom),
},
args: args{
o: &fake.Managed{},
o: &fake.LegacyManaged{},
},
want: want{
err: errors.Wrap(errBoom, errUpdateCriticalAnnotations),
o: &fake.Managed{},
o: &fake.LegacyManaged{},
},
},
"SuccessfulGetAfterAConflict": {
@ -465,7 +597,7 @@ func TestRetryingCriticalAnnotationUpdater(t *testing.T) {
}, "abc", errBoom)),
},
args: args{
o: &fake.Managed{},
o: &fake.LegacyManaged{},
},
want: want{
err: errors.Wrap(kerrors.NewConflict(schema.GroupResource{
@ -482,11 +614,11 @@ func TestRetryingCriticalAnnotationUpdater(t *testing.T) {
MockUpdate: test.NewMockUpdateFn(errBoom),
},
args: args{
o: &fake.Managed{},
o: &fake.LegacyManaged{},
},
want: want{
err: errors.Wrap(errBoom, errUpdateCriticalAnnotations),
o: &fake.Managed{},
o: &fake.LegacyManaged{},
},
},
}
@ -494,10 +626,12 @@ func TestRetryingCriticalAnnotationUpdater(t *testing.T) {
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)
}

View File

@ -119,6 +119,7 @@ func (g *GRPCChangeLogger) Log(ctx context.Context, managed resource.Managed, op
// 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")
}

View File

@ -52,6 +52,7 @@ func (c *changeLogServiceClient) SendChangeLog(ctx context.Context, in *v1alpha1
if c.sendFn != nil {
return c.sendFn(ctx, in, opts...)
}
return nil, nil
}
@ -167,6 +168,7 @@ func mustObjectAsProtobufStruct(o runtime.Object) *structpb.Struct {
if err != nil {
panic(err)
}
return s
}
@ -183,6 +185,7 @@ func equateApproxTimepb(margin time.Duration) []cmp.Option {
a, b := p.Last().Values()
return msgIsTimestamp(a) && msgIsTimestamp(b)
}
return false
},
cmp.Transformer("timestamppb", func(t protocmp.Message) time.Time {

View File

@ -116,10 +116,12 @@ func (r *MRMetricRecorder) recordFirstTimeReconciled(managed resource.Managed) {
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
@ -142,6 +144,7 @@ func (r *MRMetricRecorder) recordFirstTimeReady(managed resource.Managed) {
if !ok {
return
}
r.mrFirstTimeReady.With(getLabels(managed)).Observe(time.Since(managed.GetCreationTimestamp().Time).Seconds())
r.firstObservation.Delete(managed.GetName())
}

View File

@ -30,6 +30,15 @@ type ManagementPoliciesResolver struct {
enabled bool
supportedPolicies []sets.Set[xpv1.ManagementAction]
managementPolicies sets.Set[xpv1.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 xpv1.DeletionPolicy
}
@ -103,12 +112,11 @@ func defaultSupportedManagementPolicies() []sets.Set[xpv1.ManagementAction] {
// NewManagementPoliciesResolver returns an ManagementPolicyChecker based
// on the management policies and if the management policies feature
// is enabled.
func NewManagementPoliciesResolver(managementPolicyEnabled bool, managementPolicy xpv1.ManagementPolicies, deletionPolicy xpv1.DeletionPolicy, o ...ManagementPoliciesResolverOption) ManagementPoliciesChecker {
func NewManagementPoliciesResolver(managementPolicyEnabled bool, managementPolicy xpv1.ManagementPolicies, o ...ManagementPoliciesResolverOption) ManagementPoliciesChecker {
r := &ManagementPoliciesResolver{
enabled: managementPolicyEnabled,
supportedPolicies: defaultSupportedManagementPolicies(),
managementPolicies: sets.New[xpv1.ManagementAction](managementPolicy...),
deletionPolicy: deletionPolicy,
}
for _, ro := range o {
@ -118,6 +126,25 @@ func NewManagementPoliciesResolver(managementPolicyEnabled bool, managementPolic
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 xpv1.ManagementPolicies, deletionPolicy xpv1.DeletionPolicy, o ...ManagementPoliciesResolverOption) ManagementPoliciesChecker {
r := &ManagementPoliciesResolver{
enabled: managementPolicyEnabled,
supportedPolicies: defaultSupportedManagementPolicies(),
managementPolicies: sets.New[xpv1.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.
@ -139,6 +166,7 @@ func (m *ManagementPoliciesResolver) Validate() error {
return nil
}
}
return fmt.Errorf(errFmtManagementPolicyNotSupported, m.managementPolicies.UnsortedList())
}
@ -148,6 +176,7 @@ func (m *ManagementPoliciesResolver) IsPaused() bool {
if !m.enabled {
return false
}
return m.managementPolicies.Len() == 0
}
@ -157,6 +186,7 @@ func (m *ManagementPoliciesResolver) ShouldCreate() bool {
if !m.enabled {
return true
}
return m.managementPolicies.HasAny(xpv1.ManagementActionCreate, xpv1.ManagementActionAll)
}
@ -166,6 +196,7 @@ func (m *ManagementPoliciesResolver) ShouldUpdate() bool {
if !m.enabled {
return true
}
return m.managementPolicies.HasAny(xpv1.ManagementActionUpdate, xpv1.ManagementActionAll)
}
@ -175,6 +206,7 @@ func (m *ManagementPoliciesResolver) ShouldLateInitialize() bool {
if !m.enabled {
return true
}
return m.managementPolicies.HasAny(xpv1.ManagementActionLateInitialize, xpv1.ManagementActionAll)
}
@ -185,9 +217,22 @@ func (m *ManagementPoliciesResolver) ShouldOnlyObserve() bool {
if !m.enabled {
return false
}
return m.managementPolicies.Equal(sets.New[xpv1.ManagementAction](xpv1.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(xpv1.ManagementActionDelete, xpv1.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
@ -196,7 +241,7 @@ func (m *ManagementPoliciesResolver) ShouldOnlyObserve() bool {
// 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 *ManagementPoliciesResolver) ShouldDelete() bool {
func (m *LegacyManagementPoliciesResolver) ShouldDelete() bool {
if !m.enabled {
return m.deletionPolicy != xpv1.DeletionOrphan
}

View File

@ -1,78 +0,0 @@
/*
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"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
const errSecretStoreDisabled = "cannot publish to secret store, feature is not enabled"
// A PublisherChain chains multiple ManagedPublishers.
type PublisherChain []ConnectionPublisher
// PublishConnection calls each ConnectionPublisher.PublishConnection serially. It returns the first error it
// encounters, if any.
func (pc PublisherChain) PublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) {
published := false
for _, p := range pc {
pb, err := p.PublishConnection(ctx, o, c)
if err != nil {
return published, err
}
if pb {
published = true
}
}
return published, nil
}
// UnpublishConnection calls each ConnectionPublisher.UnpublishConnection serially. It returns the first error it
// encounters, if any.
func (pc PublisherChain) UnpublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error {
for _, p := range pc {
if err := p.UnpublishConnection(ctx, o, c); err != nil {
return err
}
}
return nil
}
// DisabledSecretStoreManager is a connection details manager that returns a proper
// error when API used but feature not enabled.
type DisabledSecretStoreManager struct{}
// PublishConnection returns a proper error when API used but the feature was
// not enabled.
func (m *DisabledSecretStoreManager) PublishConnection(_ context.Context, so resource.ConnectionSecretOwner, _ ConnectionDetails) (bool, error) {
if so.GetPublishConnectionDetailsTo() != nil {
return false, errors.New(errSecretStoreDisabled)
}
return false, nil
}
// UnpublishConnection returns a proper error when API used but the feature was
// not enabled.
func (m *DisabledSecretStoreManager) UnpublishConnection(_ context.Context, so resource.ConnectionSecretOwner, _ ConnectionDetails) error {
if so.GetPublishConnectionDetailsTo() != nil {
return errors.New(errSecretStoreDisabled)
}
return nil
}

View File

@ -1,205 +0,0 @@
/*
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"
"github.com/google/go-cmp/cmp"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var (
_ ConnectionPublisher = &APISecretPublisher{}
_ ConnectionPublisher = PublisherChain{}
)
func TestPublisherChain(t *testing.T) {
type args struct {
ctx context.Context
mg resource.Managed
c ConnectionDetails
}
type want struct {
err error
published bool
}
errBoom := errors.New("boom")
cases := map[string]struct {
p ConnectionPublisher
args args
want want
}{
"EmptyChain": {
p: PublisherChain{},
args: args{
ctx: context.Background(),
mg: &fake.Managed{},
c: ConnectionDetails{},
},
},
"SuccessfulPublisher": {
p: PublisherChain{
ConnectionPublisherFns{
PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) (bool, error) {
return true, nil
},
UnpublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) error {
return nil
},
},
},
args: args{
ctx: context.Background(),
mg: &fake.Managed{},
c: ConnectionDetails{},
},
want: want{
published: true,
},
},
"PublisherReturnsError": {
p: PublisherChain{
ConnectionPublisherFns{
PublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) (bool, error) {
return false, errBoom
},
UnpublishConnectionFn: func(_ context.Context, _ resource.ConnectionSecretOwner, _ ConnectionDetails) error {
return nil
},
},
},
args: args{
ctx: context.Background(),
mg: &fake.Managed{},
c: ConnectionDetails{},
},
want: want{
err: errBoom,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, gotErr := tc.p.PublishConnection(tc.args.ctx, tc.args.mg, tc.args.c)
if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("Publish(...): -want, +got:\n%s", diff)
}
if diff := cmp.Diff(tc.want.published, got); diff != "" {
t.Errorf("Publish(...): -wantPublished, +gotPublished:\n%s", diff)
}
})
}
}
func TestDisabledSecretStorePublish(t *testing.T) {
type args struct {
mg resource.Managed
}
type want struct {
published bool
err error
}
cases := map[string]struct {
args args
want want
}{
"APINotUsedNoError": {
args: args{
mg: &fake.Managed{},
},
},
"APIUsedProperError": {
args: args{
mg: &fake.Managed{
ConnectionDetailsPublisherTo: fake.ConnectionDetailsPublisherTo{
To: &xpv1.PublishConnectionDetailsTo{},
},
},
},
want: want{
err: errors.New(errSecretStoreDisabled),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ss := &DisabledSecretStoreManager{}
got, gotErr := ss.PublishConnection(context.Background(), tc.args.mg, nil)
if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("Publish(...): -want, +got:\n%s", diff)
}
if diff := cmp.Diff(tc.want.published, got); diff != "" {
t.Errorf("Publish(...): -wantPublished, +gotPublished:\n%s", diff)
}
})
}
}
func TestDisabledSecretStoreUnpublish(t *testing.T) {
type args struct {
mg resource.Managed
}
type want struct {
err error
}
cases := map[string]struct {
args args
want want
}{
"APINotUsedNoError": {
args: args{
mg: &fake.Managed{},
},
},
"APIUsedProperError": {
args: args{
mg: &fake.Managed{
ConnectionDetailsPublisherTo: fake.ConnectionDetailsPublisherTo{
To: &xpv1.PublishConnectionDetailsTo{},
},
},
},
want: want{
err: errors.New(errSecretStoreDisabled),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ss := &DisabledSecretStoreManager{}
gotErr := ss.UnpublishConnection(context.Background(), tc.args.mg, nil)
if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("Publish(...): -want, +got:\n%s", diff)
}
})
}
}

View File

@ -68,6 +68,8 @@ const (
errRecordChangeLog = "cannot record change log entry"
errExternalResourceNotExist = "external resource does not exist"
errManagedNotImplemented = "managed resource does not implement connection details"
)
// Event reasons.
@ -175,6 +177,35 @@ func (fn ConnectionPublisherFns) UnpublishConnection(ctx context.Context, o reso
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 {
@ -199,6 +230,7 @@ func (cc InitializerChain) Initialize(ctx context.Context, mg resource.Managed)
return err
}
}
return nil
}
@ -229,76 +261,67 @@ func (m ReferenceResolverFn) ResolveReferences(ctx context.Context, mg resource.
return m(ctx, mg)
}
// An ExternalConnecter produces a new ExternalClient given the supplied
// An ExternalConnector produces a new ExternalClient given the supplied
// Managed resource.
type ExternalConnecter = TypedExternalConnecter[resource.Managed]
type ExternalConnector = TypedExternalConnector[resource.Managed]
// A TypedExternalConnecter produces a new ExternalClient given the supplied
// A TypedExternalConnector produces a new ExternalClient given the supplied
// Managed resource.
type TypedExternalConnecter[managed resource.Managed] interface {
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)
}
// An ExternalDisconnecter disconnects from a provider.
//
// Deprecated: Please use Disconnect() on the ExternalClient for disconnecting
// from the provider.
type ExternalDisconnecter interface {
// Disconnect from the provider and close the ExternalClient.
Disconnect(ctx context.Context) 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]
}
// A NopDisconnecter converts an ExternalConnecter into an
// ExternalConnectDisconnecter with a no-op Disconnect method.
type NopDisconnecter = TypedNopDisconnecter[resource.Managed]
// A TypedNopDisconnecter converts an ExternalConnecter into an
// ExternalConnectDisconnecter with a no-op Disconnect method.
type TypedNopDisconnecter[managed resource.Managed] struct {
c TypedExternalConnecter[managed]
}
// Connect calls the underlying ExternalConnecter's Connect method.
func (c *TypedNopDisconnecter[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) {
// 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 *TypedNopDisconnecter[managed]) Disconnect(_ context.Context) error {
func (c *TypedNopDisconnector[managed]) Disconnect(_ context.Context) error {
return nil
}
// NewNopDisconnecter converts an ExternalConnecter into an
// ExternalConnectDisconnecter with a no-op Disconnect method.
func NewNopDisconnecter(c ExternalConnecter) ExternalConnectDisconnecter {
return NewTypedNopDisconnecter(c)
// NewNopDisconnector converts an ExternalConnector into an
// ExternalConnectDisconnector with a no-op Disconnect method.
func NewNopDisconnector(c ExternalConnector) ExternalConnectDisconnector {
return NewTypedNopDisconnector(c)
}
// NewTypedNopDisconnecter converts an TypedExternalConnecter into an
// ExternalConnectDisconnecter with a no-op Disconnect method.
func NewTypedNopDisconnecter[managed resource.Managed](c TypedExternalConnecter[managed]) TypedExternalConnectDisconnecter[managed] {
return &TypedNopDisconnecter[managed]{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 ExternalConnectDisconnecter produces a new ExternalClient given the supplied
// An ExternalConnectDisconnector produces a new ExternalClient given the supplied
// Managed resource.
type ExternalConnectDisconnecter = TypedExternalConnectDisconnecter[resource.Managed]
type ExternalConnectDisconnector = TypedExternalConnectDisconnector[resource.Managed]
// A TypedExternalConnectDisconnecter produces a new ExternalClient given the supplied
// A TypedExternalConnectDisconnector produces a new ExternalClient given the supplied
// Managed resource.
type TypedExternalConnectDisconnecter[managed resource.Managed] interface {
TypedExternalConnecter[managed]
ExternalDisconnecter
type TypedExternalConnectDisconnector[managed resource.Managed] interface {
TypedExternalConnector[managed]
ExternalDisconnector
}
// An ExternalConnectorFn is a function that satisfies the ExternalConnecter
// An ExternalConnectorFn is a function that satisfies the ExternalConnector
// interface.
type ExternalConnectorFn = TypedExternalConnectorFn[resource.Managed]
// An TypedExternalConnectorFn is a function that satisfies the
// TypedExternalConnecter interface.
// 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
@ -307,7 +330,7 @@ func (ec TypedExternalConnectorFn[managed]) Connect(ctx context.Context, mg mana
return ec(ctx, mg)
}
// An ExternalDisconnectorFn is a function that satisfies the ExternalConnecter
// An ExternalDisconnectorFn is a function that satisfies the ExternalConnector
// interface.
type ExternalDisconnectorFn func(ctx context.Context) error
@ -316,25 +339,25 @@ func (ed ExternalDisconnectorFn) Disconnect(ctx context.Context) error {
return ed(ctx)
}
// ExternalConnectDisconnecterFns are functions that satisfy the
// ExternalConnectDisconnecter interface.
type ExternalConnectDisconnecterFns = TypedExternalConnectDisconnecterFns[resource.Managed]
// ExternalConnectDisconnectorFns are functions that satisfy the
// ExternalConnectDisconnector interface.
type ExternalConnectDisconnectorFns = TypedExternalConnectDisconnectorFns[resource.Managed]
// TypedExternalConnectDisconnecterFns are functions that satisfy the
// TypedExternalConnectDisconnecter interface.
type TypedExternalConnectDisconnecterFns[managed resource.Managed] struct {
// 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 TypedExternalConnectDisconnecterFns[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) {
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 TypedExternalConnectDisconnecterFns[managed]) Disconnect(ctx context.Context) error {
func (fns TypedExternalConnectDisconnectorFns[managed]) Disconnect(ctx context.Context) error {
return fns.DisconnectFn(ctx)
}
@ -425,11 +448,11 @@ func (e TypedExternalClientFns[managed]) Disconnect(ctx context.Context) error {
return e.DisconnectFn(ctx)
}
// A NopConnecter does nothing.
type NopConnecter struct{}
// A NopConnector does nothing.
type NopConnector struct{}
// Connect returns a NopClient. It never returns an error.
func (c *NopConnecter) Connect(_ context.Context, _ resource.Managed) (ExternalClient, error) {
func (c *NopConnector) Connect(_ context.Context, _ resource.Managed) (ExternalClient, error) {
return &NopClient{}, nil
}
@ -571,6 +594,7 @@ type Reconciler struct {
record event.Recorder
metricRecorder MetricRecorder
change ChangeLogger
deterministicExternalName bool
}
type mrManaged struct {
@ -579,6 +603,7 @@ type mrManaged struct {
resource.Finalizer
Initializer
ReferenceResolver
LocalConnectionPublisher
}
func defaultMRManaged(m manager.Manager) mrManaged {
@ -587,20 +612,40 @@ func defaultMRManaged(m manager.Manager) mrManaged {
Finalizer: resource.NewAPIFinalizer(m.GetClient(), FinalizerName),
Initializer: NewNameAsExternalName(m.GetClient()),
ReferenceResolver: NewAPISimpleReferenceResolver(m.GetClient()),
ConnectionPublisher: PublisherChain([]ConnectionPublisher{
NewAPISecretPublisher(m.GetClient(), m.GetScheme()),
&DisabledSecretStoreManager{},
}),
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 {
ExternalConnectDisconnecter
ExternalConnectDisconnector
}
func defaultMRExternal() mrExternal {
return mrExternal{
ExternalConnectDisconnecter: NewNopDisconnecter(&NopConnecter{}),
ExternalConnectDisconnector: NewNopDisconnector(&NopConnector{}),
}
}
@ -676,44 +721,24 @@ func WithCreationGracePeriod(d time.Duration) ReconcilerOption {
}
}
// WithExternalConnecter specifies how the Reconciler should connect to the API
// WithExternalConnector specifies how the Reconciler should connect to the API
// used to sync and delete external resources.
func WithExternalConnecter(c ExternalConnecter) ReconcilerOption {
func WithExternalConnector(c ExternalConnector) ReconcilerOption {
return func(r *Reconciler) {
r.external.ExternalConnectDisconnecter = NewNopDisconnecter(c)
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 TypedExternalConnecter[managed]) ReconcilerOption {
func WithTypedExternalConnector[managed resource.Managed](c TypedExternalConnector[managed]) ReconcilerOption {
return func(r *Reconciler) {
r.external.ExternalConnectDisconnecter = &typedExternalConnectDisconnecterWrapper[managed]{
c: NewTypedNopDisconnecter(c),
r.external.ExternalConnectDisconnector = &typedExternalConnectDisconnectorWrapper[managed]{
c: NewTypedNopDisconnector(c),
}
}
}
// WithExternalConnectDisconnecter 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 WithExternalConnectDisconnecter(c ExternalConnectDisconnecter) ReconcilerOption {
return func(r *Reconciler) {
r.external.ExternalConnectDisconnecter = c
}
}
// WithTypedExternalConnectDisconnecter 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 WithTypedExternalConnectDisconnecter[managed resource.Managed](c TypedExternalConnectDisconnecter[managed]) ReconcilerOption {
return func(r *Reconciler) {
r.external.ExternalConnectDisconnecter = &typedExternalConnectDisconnecterWrapper[managed]{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
@ -724,11 +749,21 @@ func WithCriticalAnnotationUpdater(u CriticalAnnotationUpdater) ReconcilerOption
}
}
// WithConnectionPublishers specifies how the Reconciler should publish
// withConnectionPublishers specifies how the Reconciler should publish
// its connection details such as credentials and endpoints.
func WithConnectionPublishers(p ...ConnectionPublisher) ReconcilerOption {
// for unit testing only.
func withConnectionPublishers(p ConnectionPublisher) ReconcilerOption {
return func(r *Reconciler) {
r.managed.ConnectionPublisher = PublisherChain(p)
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
}
}
@ -793,6 +828,19 @@ func WithChangeLogger(c ChangeLogger) ReconcilerOption {
}
}
// 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
}
}
// 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
@ -838,7 +886,6 @@ func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOp
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)
@ -876,7 +923,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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.
policy := NewManagementPoliciesResolver(managementPoliciesEnabled, managed.GetManagementPolicies(), managed.GetDeletionPolicy(), WithSupportedManagementPolicies(r.supportedManagementPolicies))
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.
@ -901,11 +955,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.ReconcileError(err))
return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -926,23 +983,30 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.Deleting(), xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), 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(xpv1.Deleting(), xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -952,6 +1016,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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
}
@ -960,25 +1025,34 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), 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 we've leaked a resource. The safest thing to
// do is to refuse to proceed.
// 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(xpv1.Creating(), xpv1.ReconcileError(errors.New(errCreateIncomplete)))
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, managed), 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.
@ -996,11 +1070,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
}
@ -1013,13 +1090,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.ReconcileError(errors.Wrap(err, errReconcileConnect)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
defer func() {
if err := r.external.Disconnect(ctx); err != nil {
log.Debug("Cannot disconnect from provider", "error", err)
@ -1041,11 +1122,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.ReconcileError(errors.Wrap(err, errReconcileObserve)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1054,6 +1138,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
if !observation.ResourceExists && policy.ShouldOnlyObserve() {
record.Event(managed, event.Warning(reasonCannotObserve, errors.New(errExternalResourceNotExist)))
status.MarkConditions(xpv1.ReconcileError(errors.Wrap(errors.New(errExternalResourceNotExist), errReconcileObserve)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1065,6 +1150,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
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
}
@ -1087,11 +1173,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.Deleting(), xpv1.ReconcileError(errors.Wrap(err, errReconcileDelete)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1103,36 +1192,47 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.Deleting(), xpv1.ReconcileSuccess())
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), 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(xpv1.Deleting(), xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), 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(xpv1.Deleting(), xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1142,6 +1242,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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
}
@ -1150,11 +1251,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1163,10 +1267,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1179,13 +1286,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errUpdateManaged)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1197,6 +1308,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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))
}
@ -1208,6 +1320,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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)))
@ -1222,7 +1335,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_CREATE, err, creation.AdditionalDetails); err != nil {
log.Info(errRecordChangeLog, "error", err)
}
status.MarkConditions(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errReconcileCreate)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1245,13 +1360,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errUpdateManagedAnnotations)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1260,11 +1379,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.Creating(), xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1275,6 +1397,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
log.Debug("Successfully requested creation of external resource")
record.Event(managed, event.Normal(reasonCreated, "Successfully requested creation of external resource"))
status.MarkConditions(xpv1.Creating(), xpv1.ReconcileSuccess())
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1290,6 +1413,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
log.Debug(errUpdateManaged, "error", err)
record.Event(managed, event.Warning(reasonCannotUpdateManaged, err))
status.MarkConditions(xpv1.ReconcileError(errors.Wrap(err, errUpdateManaged)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
}
@ -1324,6 +1448,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
reconcileAfter := r.pollIntervalHook(managed, r.pollInterval)
log.Debug("Skipping update due to managementPolicies. Reconciliation succeeded", "requeue-after", time.Now().Add(reconcileAfter))
status.MarkConditions(xpv1.ReconcileSuccess())
return reconcile.Result{RequeueAfter: reconcileAfter}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1335,16 +1460,20 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
// 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(xpv1.ReconcileError(errors.Wrap(err, errReconcileUpdate)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), 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)
}
@ -1356,6 +1485,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
log.Debug("Cannot publish connection details", "error", err)
record.Event(managed, event.Warning(reasonCannotPublish, err))
status.MarkConditions(xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
@ -1368,5 +1498,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
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(xpv1.ReconcileSuccess())
return reconcile.Result{RequeueAfter: reconcileAfter}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}

View File

@ -0,0 +1,123 @@
/*
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/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 diff suppressed because it is too large Load Diff

View File

@ -9,25 +9,27 @@ import (
const errFmtUnexpectedObjectType = "unexpected object type %T"
// typedExternalConnectDisconnecterWrapper wraps a TypedExternalConnecter to a
// typedExternalConnectDisconnectorWrapper wraps a TypedExternalConnector to a
// common ExternalConnector.
type typedExternalConnectDisconnecterWrapper[managed resource.Managed] struct {
c TypedExternalConnectDisconnecter[managed]
type typedExternalConnectDisconnectorWrapper[managed resource.Managed] struct {
c TypedExternalConnectDisconnector[managed]
}
func (c *typedExternalConnectDisconnecterWrapper[managed]) Connect(ctx context.Context, mg resource.Managed) (ExternalClient, error) {
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 *typedExternalConnectDisconnecterWrapper[managed]) Disconnect(ctx context.Context) error {
func (c *typedExternalConnectDisconnectorWrapper[managed]) Disconnect(ctx context.Context) error {
return c.c.Disconnect(ctx)
}
@ -42,6 +44,7 @@ func (c *typedExternalClientWrapper[managed]) Observe(ctx context.Context, mg re
if !ok {
return ExternalObservation{}, errors.Errorf(errFmtUnexpectedObjectType, mg)
}
return c.c.Observe(ctx, cr)
}
@ -50,13 +53,16 @@ func (c *typedExternalClientWrapper[managed]) Create(ctx context.Context, mg res
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)
}
@ -65,6 +71,7 @@ func (c *typedExternalClientWrapper[managed]) Delete(ctx context.Context, mg res
if !ok {
return ExternalDelete{}, errors.Errorf(errFmtUnexpectedObjectType, mg)
}
return c.c.Delete(ctx, cr)
}

View File

@ -87,6 +87,8 @@ type Reconciler struct {
newConfig func() resource.ProviderConfig
newUsageList func() resource.ProviderConfigUsageList
legacyPCU bool
log logging.Logger
record event.Recorder
}
@ -118,6 +120,7 @@ func NewReconciler(m manager.Manager, of resource.ProviderConfigKinds, o ...Reco
//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.
@ -128,6 +131,7 @@ func NewReconciler(m manager.Manager, of resource.ProviderConfigKinds, o ...Reco
newConfig: nc,
newUsageList: nul,
legacyPCU: isLegacyPCU,
log: logging.NewNopLogger(),
record: event.NewNopRecorder(),
@ -165,9 +169,19 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
)
l := r.newUsageList()
if err := r.client.List(ctx, l, client.MatchingLabels{xpv1.LabelKeyProviderName: pc.GetName()}); err != nil {
matchingLabels := client.MatchingLabels{
xpv1.LabelKeyProviderName: pc.GetName(),
}
if !r.legacyPCU {
matchingLabels[xpv1.LabelKeyProviderKind] = pc.GetObjectKind().GroupVersionKind().Kind
}
if err := r.client.List(ctx, l, matchingLabels); err != nil {
log.Debug(errListPCUs, "error", err)
r.record.Event(pc, event.Warning(reasonAccount, errors.Wrap(err, errListPCUs)))
return reconcile.Result{RequeueAfter: shortWait}, nil
}
@ -181,11 +195,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
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) {
@ -198,10 +215,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
// 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
@ -212,6 +231,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
}
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
@ -219,5 +239,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
// 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)
}

View File

@ -50,11 +50,14 @@ func (p *ProviderConfigUsageList) GetObjectKind() schema.ObjectKind {
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
}
@ -90,10 +93,11 @@ func TestReconciler(t *testing.T) {
Client: &test.MockClient{
MockGet: test.NewMockGetFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -109,10 +113,11 @@ func TestReconciler(t *testing.T) {
Client: &test.MockClient{
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -129,10 +134,11 @@ func TestReconciler(t *testing.T) {
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -155,10 +161,11 @@ func TestReconciler(t *testing.T) {
}),
MockDelete: test.NewMockDeleteFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -193,10 +200,11 @@ func TestReconciler(t *testing.T) {
}),
MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -217,10 +225,11 @@ func TestReconciler(t *testing.T) {
MockList: test.NewMockListFn(nil),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -241,10 +250,11 @@ func TestReconciler(t *testing.T) {
MockList: test.NewMockListFn(nil),
MockUpdate: test.NewMockUpdateFn(nil),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -261,10 +271,11 @@ func TestReconciler(t *testing.T) {
MockList: test.NewMockListFn(nil),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -282,10 +293,11 @@ func TestReconciler(t *testing.T) {
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockSubResourceUpdateFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -304,10 +316,11 @@ func TestReconciler(t *testing.T) {
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
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{}),
},
},
@ -320,8 +333,8 @@ func TestReconciler(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewReconciler(tc.args.m, tc.args.of)
got, err := r.Reconcile(context.Background(), reconcile.Request{})
got, err := r.Reconcile(context.Background(), reconcile.Request{})
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)
}

View File

@ -0,0 +1,310 @@
/*
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"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
// A NamespacedResolutionRequest requests that a reference to a particular kind of
// managed resource be resolved.
type NamespacedResolutionRequest struct {
CurrentValue string
Reference *xpv1.NamespacedReference
Selector *xpv1.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 *xpv1.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 []xpv1.NamespacedReference
Selector *xpv1.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 []xpv1.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: &xpv1.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
}
valueMap := make(map[string]xpv1.NamespacedReference)
// The references are already set - resolve them.
if len(req.References) > 0 {
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)
}
valueMap[req.Extract(req.To.Managed)] = req.References[i]
}
sortedKeys, sortedRefs := sortGenericMapByKeys(valueMap)
rsp := MultiNamespacedResolutionResponse{ResolvedValues: sortedKeys, ResolvedReferences: sortedRefs}
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)
}
for _, to := range req.To.List.GetItems() {
if ControllersMustMatchNamespaced(req.Selector) && !meta.HaveSameController(r.from, to) {
continue
}
valueMap[req.Extract(to)] = xpv1.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 *xpv1.NamespacedSelector) bool {
if s == nil {
return false
}
return s.MatchControllerRef != nil && *s.MatchControllerRef
}

View File

@ -0,0 +1,969 @@
package reference
import (
"context"
"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"
"sigs.k8s.io/controller-runtime/pkg/client"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
func TestNamespacedResolve(t *testing.T) {
errBoom := errors.New("boom")
now := metav1.Now()
value := "coolv"
ref := &xpv1.NamespacedReference{Name: "cool", Namespace: "cool-ns"}
nsOmittedRef := &xpv1.NamespacedReference{Name: "cool"}
optionalPolicy := xpv1.ResolutionPolicyOptional
alwaysPolicy := xpv1.ResolvePolicyAlways
optionalRef := &xpv1.NamespacedReference{Name: "cool", Namespace: "cool-ns", Policy: &xpv1.Policy{Resolution: &optionalPolicy}}
alwaysRef := &xpv1.NamespacedReference{Name: "cool", Namespace: "cool-ns", Policy: &xpv1.Policy{Resolve: &alwaysPolicy}}
controlled := &fake.Managed{}
controlled.SetName(value)
meta.SetExternalName(controlled, value)
meta.AddControllerReference(controlled, meta.AsController(&xpv1.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: &xpv1.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: &xpv1.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: &xpv1.NamespacedSelector{
Policy: &xpv1.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: &xpv1.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: &xpv1.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: &xpv1.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: &xpv1.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: &xpv1.NamespacedSelector{
MatchControllerRef: func() *bool { t := true; return &t }(),
Policy: &xpv1.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: &xpv1.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: &xpv1.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 := xpv1.NamespacedReference{Name: "cool", Namespace: "cool-ns"}
nsOmittedRef := xpv1.NamespacedReference{Name: "cool"}
optionalPolicy := xpv1.ResolutionPolicyOptional
alwaysPolicy := xpv1.ResolvePolicyAlways
optionalRef := xpv1.NamespacedReference{Name: "cool", Policy: &xpv1.Policy{Resolution: &optionalPolicy}}
alwaysRef := xpv1.NamespacedReference{Name: "cool", Policy: &xpv1.Policy{Resolve: &alwaysPolicy}}
controlled := &fake.Managed{}
controlled.SetName(value)
meta.SetExternalName(controlled, value)
meta.AddControllerReference(controlled, meta.AsController(&xpv1.TypedReference{UID: types.UID("very-unique")}))
controlled2 := &fake.Managed{}
controlled2.SetName(value2)
meta.SetExternalName(controlled2, value2)
meta.AddControllerReference(controlled2, meta.AsController(&xpv1.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: []xpv1.NamespacedReference{alwaysRef},
To: To{Managed: &fake.Managed{}},
Extract: ExternalName(),
CurrentValues: []string{"oldValue"},
},
},
want: want{
rsp: MultiNamespacedResolutionResponse{
ResolvedValues: []string{value},
ResolvedReferences: []xpv1.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: []xpv1.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: []xpv1.NamespacedReference{ref},
To: To{Managed: &fake.Managed{}},
Extract: func(resource.Managed) string { return "" },
},
},
want: want{
rsp: MultiNamespacedResolutionResponse{
ResolvedValues: []string{""},
ResolvedReferences: []xpv1.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: []xpv1.NamespacedReference{ref},
To: To{Managed: &fake.Managed{}},
Extract: ExternalName(),
},
},
want: want{
rsp: MultiNamespacedResolutionResponse{
ResolvedValues: []string{value},
ResolvedReferences: []xpv1.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: []xpv1.NamespacedReference{ref},
To: To{Managed: &fake.Managed{}},
Extract: ExternalName(),
},
},
want: want{
rsp: MultiNamespacedResolutionResponse{
ResolvedValues: []string{value},
ResolvedReferences: []xpv1.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: []xpv1.NamespacedReference{nsOmittedRef},
To: To{Managed: &fake.Managed{}},
Extract: ExternalName(),
},
},
want: want{
rsp: MultiNamespacedResolutionResponse{
ResolvedValues: []string{value},
ResolvedReferences: []xpv1.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: []xpv1.NamespacedReference{ref},
To: To{Managed: &fake.Managed{}},
Extract: ExternalName(),
},
},
want: want{
rsp: MultiNamespacedResolutionResponse{
ResolvedValues: []string{value},
ResolvedReferences: []xpv1.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: []xpv1.NamespacedReference{optionalRef},
To: To{Managed: &fake.Managed{}},
Extract: func(resource.Managed) string { return "" },
},
},
want: want{
rsp: MultiNamespacedResolutionResponse{
ResolvedValues: []string{""},
ResolvedReferences: []xpv1.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: &xpv1.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: &xpv1.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: &xpv1.NamespacedSelector{
Policy: &xpv1.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: &xpv1.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: []xpv1.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: &xpv1.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: []xpv1.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: &xpv1.NamespacedSelector{
MatchControllerRef: func() *bool { t := true; return &t }(),
Policy: &xpv1.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: []xpv1.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: MultiNamespacedResolutionRequest{
References: []xpv1.NamespacedReference{ref},
Selector: &xpv1.NamespacedSelector{
MatchControllerRef: func() *bool { t := true; return &t }(),
},
To: To{Managed: &fake.Managed{}},
Extract: ExternalName(),
},
},
want: want{
rsp: MultiNamespacedResolutionResponse{
ResolvedValues: []string{value},
ResolvedReferences: []xpv1.NamespacedReference{ref},
},
},
},
"OrderOutput": {
reason: "Output values should ordered",
c: &test.MockClient{
MockList: test.NewMockListFn(nil),
},
from: controlled,
args: args{
req: MultiNamespacedResolutionRequest{
Selector: &xpv1.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: []xpv1.NamespacedReference{{Name: value2}, {Name: value}},
},
err: nil,
},
},
}
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 *xpv1.NamespacedSelector
want bool
}{
"NilSelector": {
s: nil,
want: false,
},
"NilMatchControllerRef": {
s: &xpv1.NamespacedSelector{},
want: false,
},
"False": {
s: &xpv1.NamespacedSelector{MatchControllerRef: func() *bool { f := false; return &f }()},
want: false,
},
"True": {
s: &xpv1.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)
}
})
}
}

View File

@ -20,6 +20,8 @@ package reference
import (
"context"
"maps"
"slices"
"strconv"
kerrors "k8s.io/apimachinery/pkg/api/errors"
@ -49,6 +51,7 @@ func FromPtrValue(v *string) string {
if v == nil {
return ""
}
return *v
}
@ -57,6 +60,7 @@ func FromFloatPtrValue(v *float64) string {
if v == nil {
return ""
}
return strconv.FormatFloat(*v, 'f', 0, 64)
}
@ -65,6 +69,7 @@ func FromIntPtrValue(v *int64) string {
if v == nil {
return ""
}
return strconv.FormatInt(*v, 10)
}
@ -73,6 +78,7 @@ func ToPtrValue(v string) *string {
if v == "" {
return nil
}
return &v
}
@ -81,10 +87,12 @@ func ToFloatPtrValue(v string) *float64 {
if v == "" {
return nil
}
vParsed, err := strconv.ParseFloat(v, 64)
if err != nil {
return nil
}
return &vParsed
}
@ -93,10 +101,12 @@ func ToIntPtrValue(v string) *int64 {
if v == "" {
return nil
}
vParsed, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil
}
return &vParsed
}
@ -107,27 +117,30 @@ func ToIntPtrValue(v string) *int64 {
// string pointers and need to be resolved as part of `ResolveMultiple`.
func FromPtrValues(v []*string) []string {
res := make([]string, len(v))
for i := range 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 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 len(v) {
for i := range v {
res[i] = FromIntPtrValue(v[i])
}
return res
}
@ -138,27 +151,30 @@ func FromIntPtrValues(v []*int64) []string {
// string pointers and need to be resolved as part of `ResolveMultiple`.
func ToPtrValues(v []string) []*string {
res := make([]*string, len(v))
for i := range 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 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 len(v) {
for i := range v {
res[i] = ToIntPtrValue(v[i])
}
return res
}
@ -188,12 +204,14 @@ type ResolutionRequest struct {
Selector *xpv1.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
@ -242,12 +260,14 @@ type MultiResolutionRequest struct {
Selector *xpv1.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
@ -323,19 +343,22 @@ func (r *APIResolver) Resolve(ctx context.Context, req ResolutionRequest) (Resol
// The reference is already set - resolve it.
if req.Reference != nil {
if err := r.client.Get(ctx, types.NamespacedName{Name: req.Reference.Name}, req.To.Managed); err != 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 err := r.client.List(ctx, req.To.List, client.MatchingLabels(req.Selector.MatchLabels)); err != nil {
// 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)
}
@ -345,6 +368,7 @@ func (r *APIResolver) Resolve(ctx context.Context, req ResolutionRequest) (Resol
}
rsp := ResolutionResponse{ResolvedValue: req.Extract(to), ResolvedReference: &xpv1.Reference{Name: to.GetName()}}
return rsp, getResolutionError(req.Selector.Policy, rsp.Validate())
}
@ -361,41 +385,47 @@ func (r *APIResolver) ResolveMultiple(ctx context.Context, req MultiResolutionRe
return MultiResolutionResponse{ResolvedValues: req.CurrentValues, ResolvedReferences: req.References}, nil
}
valueMap := make(map[string]xpv1.Reference)
// The references are already set - resolve them.
if len(req.References) > 0 {
vals := make([]string, len(req.References))
for i := range req.References {
if err := r.client.Get(ctx, types.NamespacedName{Name: req.References[i].Name}, req.To.Managed); err != nil {
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)
}
vals[i] = req.Extract(req.To.Managed)
valueMap[req.Extract(req.To.Managed)] = req.References[i]
}
rsp := MultiResolutionResponse{ResolvedValues: vals, ResolvedReferences: req.References}
sortedKeys, sortedRefs := sortMapByKeys(valueMap)
rsp := MultiResolutionResponse{ResolvedValues: sortedKeys, ResolvedReferences: sortedRefs}
return rsp, rsp.Validate()
}
// No references were set, but a selector was. Select and resolve references.
if err := r.client.List(ctx, req.To.List, client.MatchingLabels(req.Selector.MatchLabels)); err != nil {
// 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)
}
items := req.To.List.GetItems()
refs := make([]xpv1.Reference, 0, len(items))
vals := make([]string, 0, len(items))
for _, to := range req.To.List.GetItems() {
if ControllersMustMatch(req.Selector) && !meta.HaveSameController(r.from, to) {
continue
}
vals = append(vals, req.Extract(to))
refs = append(refs, xpv1.Reference{Name: to.GetName()})
valueMap[req.Extract(to)] = xpv1.Reference{Name: to.GetName()}
}
rsp := MultiResolutionResponse{ResolvedValues: vals, ResolvedReferences: refs}
sortedKeys, sortedRefs := sortMapByKeys(valueMap)
rsp := MultiResolutionResponse{ResolvedValues: sortedKeys, ResolvedReferences: sortedRefs}
return rsp, getResolutionError(req.Selector.Policy, rsp.Validate())
}
@ -403,9 +433,21 @@ func getResolutionError(p *xpv1.Policy, err error) error {
if !p.IsResolutionPolicyOptional() {
return err
}
return nil
}
func sortMapByKeys(m map[string]xpv1.Reference) ([]string, []xpv1.Reference) {
keys := slices.Sorted(maps.Keys(m))
values := make([]xpv1.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.
@ -413,5 +455,6 @@ func ControllersMustMatch(s *xpv1.Selector) bool {
if s == nil {
return false
}
return s.MatchControllerRef != nil && *s.MatchControllerRef
}

View File

@ -157,10 +157,12 @@ func TestResolve(t *testing.T) {
ctx context.Context
req ResolutionRequest
}
type want struct {
rsp ResolutionResponse
err error
}
cases := map[string]struct {
reason string
c client.Reader
@ -286,6 +288,30 @@ func TestResolve(t *testing.T) {
},
},
},
"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{
@ -384,6 +410,33 @@ func TestResolve(t *testing.T) {
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: &xpv1.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: &xpv1.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",
@ -443,10 +496,12 @@ func TestResolve(t *testing.T) {
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)
}
@ -458,6 +513,7 @@ func TestResolveMultiple(t *testing.T) {
errBoom := errors.New("boom")
now := metav1.Now()
value := "coolv"
value2 := "cooler"
ref := xpv1.Reference{Name: "cool"}
optionalPolicy := xpv1.ResolutionPolicyOptional
alwaysPolicy := xpv1.ResolvePolicyAlways
@ -469,14 +525,21 @@ func TestResolveMultiple(t *testing.T) {
meta.SetExternalName(controlled, value)
meta.AddControllerReference(controlled, meta.AsController(&xpv1.TypedReference{UID: types.UID("very-unique")}))
controlled2 := &fake.Managed{}
controlled2.SetName(value2)
meta.SetExternalName(controlled2, value2)
meta.AddControllerReference(controlled2, meta.AsController(&xpv1.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
@ -603,6 +666,30 @@ func TestResolveMultiple(t *testing.T) {
},
},
},
"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: []xpv1.Reference{ref},
To: To{Managed: &fake.Managed{}},
Extract: ExternalName(),
Namespace: "cool-ns",
},
},
want: want{
rsp: MultiResolutionResponse{
ResolvedValues: []string{value},
ResolvedReferences: []xpv1.Reference{ref},
},
},
},
"OptionalReference": {
reason: "No error should be returned when the resolution policy is Optional",
c: &test.MockClient{
@ -702,6 +789,33 @@ func TestResolveMultiple(t *testing.T) {
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: &xpv1.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: []xpv1.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",
@ -757,14 +871,46 @@ func TestResolveMultiple(t *testing.T) {
},
},
},
"OrderOutput": {
reason: "Output values should ordered",
c: &test.MockClient{
MockList: test.NewMockListFn(nil),
},
from: controlled,
args: args{
req: MultiResolutionRequest{
Selector: &xpv1.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: []xpv1.Reference{{Name: value2}, {Name: value}},
},
err: nil,
},
},
}
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)
}

View File

@ -67,6 +67,7 @@ func (a *APIPatchingApplicator) Apply(ctx context.Context, o client.Object, ao .
// 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")
}
@ -118,6 +119,7 @@ func (a *APIUpdatingApplicator) Apply(ctx context.Context, o client.Object, ao .
// 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")
}
@ -131,6 +133,7 @@ func (a *APIUpdatingApplicator) Apply(ctx context.Context, o client.Object, ao .
// 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")
}
@ -163,7 +166,9 @@ 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)
}
@ -172,7 +177,9 @@ 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)
}

View File

@ -142,10 +142,12 @@ func TestAPIPatchingApplicator(t *testing.T) {
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)
}
@ -157,6 +159,7 @@ func TestAPIUpdatingApplicator(t *testing.T) {
errBoom := errors.New("boom")
desired := &object{}
desired.SetName("desired")
current := &object{}
current.SetName("current")
@ -270,10 +273,12 @@ func TestAPIUpdatingApplicator(t *testing.T) {
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)
}
@ -328,10 +333,12 @@ func TestManagedRemoveFinalizer(t *testing.T) {
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)
}
@ -386,10 +393,12 @@ func TestAPIFinalizerAdder(t *testing.T) {
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)
}

View File

@ -63,10 +63,10 @@ func (e *EnqueueRequestForProviderConfig) Generic(_ context.Context, evt event.G
}
func addProviderConfig(obj runtime.Object, queue adder) {
pcr, ok := obj.(RequiredProviderConfigReferencer)
if !ok {
return
}
switch pcr := obj.(type) {
case TypedProviderConfigUsage:
queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: pcr.GetProviderConfigReference().Name, Namespace: pcr.GetNamespace()}})
case LegacyProviderConfigUsage:
queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: pcr.GetProviderConfigReference().Name}})
}
}

View File

@ -20,6 +20,7 @@ import (
"testing"
"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"
@ -47,8 +48,8 @@ func TestAddProviderConfig(t *testing.T) {
"NotProviderConfigReferencer": {
queue: addFn(func(_ any) { t.Errorf("queue.Add() called unexpectedly") }),
},
"IsProviderConfigReferencer": {
obj: &fake.ProviderConfigUsage{
"IsLegacyProviderConfigReferencer": {
obj: &fake.LegacyProviderConfigUsage{
RequiredProviderConfigReferencer: fake.RequiredProviderConfigReferencer{
Ref: xpv1.Reference{Name: name},
},
@ -60,6 +61,23 @@ func TestAddProviderConfig(t *testing.T) {
}
}),
},
"IsProviderConfigReferencer": {
obj: &fake.ProviderConfigUsage{
ObjectMeta: metav1.ObjectMeta{
Name: "some-pcu",
Namespace: "foo",
},
RequiredTypedProviderConfigReferencer: fake.RequiredTypedProviderConfigReferencer{
Ref: xpv1.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)
}
}),
},
}
for _, tc := range cases {

View File

@ -67,14 +67,29 @@ func (m *ManagedResourceReferencer) SetResourceReference(r *corev1.ObjectReferen
// GetResourceReference gets the ResourceReference.
func (m *ManagedResourceReferencer) GetResourceReference() *corev1.ObjectReference { return m.Ref }
// ProviderConfigReferencer is a mock that implements ProviderConfigReferencer interface.
type ProviderConfigReferencer struct{ Ref *xpv1.Reference }
// TypedProviderConfigReferencer is a mock that implements resource.TypedProviderConfigReferencer interface.
type TypedProviderConfigReferencer struct{ Ref *xpv1.ProviderConfigReference }
// SetProviderConfigReference sets the ProviderConfigReference.
func (m *ProviderConfigReferencer) SetProviderConfigReference(p *xpv1.Reference) { m.Ref = p }
func (m *TypedProviderConfigReferencer) SetProviderConfigReference(p *xpv1.ProviderConfigReference) {
m.Ref = p
}
// GetProviderConfigReference gets the ProviderConfigReference.
func (m *ProviderConfigReferencer) GetProviderConfigReference() *xpv1.Reference { return m.Ref }
func (m *TypedProviderConfigReferencer) GetProviderConfigReference() *xpv1.ProviderConfigReference {
return m.Ref
}
// LegacyProviderConfigReferencer is a mock that implements resource.ProviderConfigReferencer interface.
type LegacyProviderConfigReferencer struct{ Ref *xpv1.Reference }
// SetProviderConfigReference sets the ProviderConfigReference.
func (m *LegacyProviderConfigReferencer) SetProviderConfigReference(p *xpv1.Reference) { m.Ref = p }
// GetProviderConfigReference gets the ProviderConfigReference.
func (m *LegacyProviderConfigReferencer) GetProviderConfigReference() *xpv1.Reference {
return m.Ref
}
// RequiredProviderConfigReferencer is a mock that implements the
// RequiredProviderConfigReferencer interface.
@ -90,6 +105,20 @@ func (m *RequiredProviderConfigReferencer) GetProviderConfigReference() xpv1.Ref
return m.Ref
}
// RequiredTypedProviderConfigReferencer is a mock that implements the
// RequiredTypedProviderConfigReferencer interface.
type RequiredTypedProviderConfigReferencer struct{ Ref xpv1.ProviderConfigReference }
// SetProviderConfigReference sets the ProviderConfigReference.
func (m *RequiredTypedProviderConfigReferencer) SetProviderConfigReference(p xpv1.ProviderConfigReference) {
m.Ref = p
}
// GetProviderConfigReference gets the ProviderConfigReference.
func (m *RequiredTypedProviderConfigReferencer) GetProviderConfigReference() xpv1.ProviderConfigReference {
return m.Ref
}
// RequiredTypedResourceReferencer is a mock that implements the
// RequiredTypedResourceReferencer interface.
type RequiredTypedResourceReferencer struct{ Ref xpv1.TypedReference }
@ -132,21 +161,6 @@ func (m *ConnectionSecretWriterTo) GetWriteConnectionSecretToReference() *xpv1.S
return m.Ref
}
// ConnectionDetailsPublisherTo is a mock that implements ConnectionDetailsPublisherTo interface.
type ConnectionDetailsPublisherTo struct {
To *xpv1.PublishConnectionDetailsTo
}
// SetPublishConnectionDetailsTo sets the PublishConnectionDetailsTo.
func (m *ConnectionDetailsPublisherTo) SetPublishConnectionDetailsTo(to *xpv1.PublishConnectionDetailsTo) {
m.To = to
}
// GetPublishConnectionDetailsTo gets the PublishConnectionDetailsTo.
func (m *ConnectionDetailsPublisherTo) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo {
return m.To
}
// Manageable implements the Manageable interface.
type Manageable struct{ Policy xpv1.ManagementPolicies }
@ -315,22 +329,21 @@ func (o *Object) GetObjectKind() schema.ObjectKind {
// 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
ProviderConfigReferencer
ConnectionSecretWriterTo
ConnectionDetailsPublisherTo
Manageable
Orphanable
xpv1.ConditionedStatus
}
@ -342,11 +355,71 @@ func (m *Managed) GetObjectKind() schema.ObjectKind {
// 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
xpv1.ConditionedStatus
}
// 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
xpv1.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
}
@ -362,7 +435,6 @@ type Composite struct {
EnvironmentConfigReferencer
ClaimReferencer
ConnectionSecretWriterTo
ConnectionDetailsPublisherTo
xpv1.ResourceStatus
ConnectionDetailsLastPublishedTimer
@ -376,11 +448,14 @@ func (m *Composite) GetObjectKind() schema.ObjectKind {
// 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
}
@ -388,7 +463,6 @@ func (m *Composite) DeepCopyObject() runtime.Object {
type Composed struct {
metav1.ObjectMeta
ConnectionSecretWriterTo
ConnectionDetailsPublisherTo
xpv1.ResourceStatus
}
@ -400,11 +474,14 @@ func (m *Composed) GetObjectKind() schema.ObjectKind {
// 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
}
@ -419,7 +496,6 @@ type CompositeClaim struct {
CompositionUpdater
CompositeResourceReferencer
LocalConnectionSecretWriterTo
ConnectionDetailsPublisherTo
xpv1.ResourceStatus
ConnectionDetailsLastPublishedTimer
@ -433,11 +509,14 @@ func (m *CompositeClaim) GetObjectKind() schema.ObjectKind {
// 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
}
@ -457,6 +536,7 @@ type Manager struct {
func (m *Manager) Elected() <-chan struct{} {
e := make(chan struct{})
close(e)
return e
}
@ -490,6 +570,7 @@ func GVK(o runtime.Object) schema.GroupVersionKind {
func SchemeWith(o ...runtime.Object) *runtime.Scheme {
s := runtime.NewScheme()
s.AddKnownTypes(GV, o...)
return s
}
@ -499,20 +580,9 @@ type MockConnectionSecretOwner struct {
runtime.Object
metav1.ObjectMeta
To *xpv1.PublishConnectionDetailsTo
WriterTo *xpv1.SecretReference
}
// GetPublishConnectionDetailsTo returns the publish connection details to reference.
func (m *MockConnectionSecretOwner) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo {
return m.To
}
// SetPublishConnectionDetailsTo sets the publish connection details to reference.
func (m *MockConnectionSecretOwner) SetPublishConnectionDetailsTo(t *xpv1.PublishConnectionDetailsTo) {
m.To = t
}
// GetWriteConnectionSecretToReference returns the connection secret reference.
func (m *MockConnectionSecretOwner) GetWriteConnectionSecretToReference() *xpv1.SecretReference {
return m.WriterTo
@ -531,11 +601,14 @@ func (m *MockConnectionSecretOwner) GetObjectKind() schema.ObjectKind {
// 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
}
@ -546,7 +619,6 @@ type MockLocalConnectionSecretOwner struct {
metav1.ObjectMeta
Ref *xpv1.LocalSecretReference
To *xpv1.PublishConnectionDetailsTo
}
// GetWriteConnectionSecretToReference returns the connection secret reference.
@ -559,16 +631,6 @@ func (m *MockLocalConnectionSecretOwner) SetWriteConnectionSecretToReference(r *
m.Ref = r
}
// SetPublishConnectionDetailsTo sets the publish connectionDetails to.
func (m *MockLocalConnectionSecretOwner) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) {
m.To = r
}
// GetPublishConnectionDetailsTo returns the publish connectionDetails to.
func (m *MockLocalConnectionSecretOwner) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo {
return m.To
}
// GetObjectKind returns schema.ObjectKind.
func (m *MockLocalConnectionSecretOwner) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
@ -577,11 +639,14 @@ func (m *MockLocalConnectionSecretOwner) GetObjectKind() schema.ObjectKind {
// 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
}
@ -601,11 +666,14 @@ func (p *ProviderConfig) GetObjectKind() schema.ObjectKind {
// 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
}
@ -614,7 +682,7 @@ func (p *ProviderConfig) DeepCopyObject() runtime.Object {
type ProviderConfigUsage struct {
metav1.ObjectMeta
RequiredProviderConfigReferencer
RequiredTypedProviderConfigReferencer
RequiredTypedResourceReferencer
}
@ -626,10 +694,41 @@ func (p *ProviderConfigUsage) GetObjectKind() schema.ObjectKind {
// 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
}

View File

@ -61,13 +61,6 @@ type ConnectionSecretWriterTo interface {
GetWriteConnectionSecretToReference() *xpv1.SecretReference
}
// A ConnectionDetailsPublisherTo may write a connection details secret to a
// secret store.
type ConnectionDetailsPublisherTo interface {
SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo)
GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo
}
// A Manageable resource may specify a ManagementPolicies.
type Manageable interface {
SetManagementPolicies(p xpv1.ManagementPolicies)
@ -86,6 +79,13 @@ type ProviderConfigReferencer interface {
SetProviderConfigReference(p *xpv1.Reference)
}
// A TypedProviderConfigReferencer may reference a provider config resource
// with its kind.
type TypedProviderConfigReferencer interface {
GetProviderConfigReference() *xpv1.ProviderConfigReference
SetProviderConfigReference(p *xpv1.ProviderConfigReference)
}
// A RequiredProviderConfigReferencer may reference a provider config resource.
// Unlike ProviderConfigReferencer, the reference is required (i.e. not nil).
type RequiredProviderConfigReferencer interface {
@ -93,6 +93,13 @@ type RequiredProviderConfigReferencer interface {
SetProviderConfigReference(p xpv1.Reference)
}
// A RequiredTypedProviderConfigReferencer may reference a provider config resource.
// Unlike TypedProviderConfigReferencer, the reference is required (i.e. not nil).
type RequiredTypedProviderConfigReferencer interface {
GetProviderConfigReference() xpv1.ProviderConfigReference
SetProviderConfigReference(p xpv1.ProviderConfigReference)
}
// A RequiredTypedResourceReferencer can reference a resource.
type RequiredTypedResourceReferencer interface {
SetResourceReference(r xpv1.TypedReference)
@ -190,18 +197,32 @@ type Object interface {
// A Managed is a Kubernetes object representing a concrete managed
// resource (e.g. a CloudSQL instance).
type Managed interface { //nolint:interfacebloat // This interface has to be big.
type Managed interface {
Object
ProviderConfigReferencer
ConnectionSecretWriterTo
ConnectionDetailsPublisherTo
Manageable
Orphanable
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
@ -210,6 +231,15 @@ type ManagedList interface {
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
@ -221,11 +251,24 @@ type ProviderConfig interface {
// A ProviderConfigUsage indicates a usage of a Crossplane provider config.
type ProviderConfigUsage interface {
Object
RequiredProviderConfigReferencer
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
@ -234,7 +277,7 @@ type ProviderConfigUsageList interface {
GetItems() []ProviderConfigUsage
}
// A Composite resource composes one or more Composed resources.
// A Composite resource (or XR) is composed of other resources.
type Composite interface { //nolint:interfacebloat // This interface has to be big.
Object
@ -244,27 +287,29 @@ type Composite interface { //nolint:interfacebloat // This interface has to be b
CompositionRevisionReferencer
CompositionRevisionSelector
ComposedResourcesReferencer
EnvironmentConfigReferencer
ClaimReferencer
ConnectionSecretWriterTo
ConnectionDetailsPublisherTo
Conditioned
ConnectionDetailsPublishedTimer
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
ConnectionDetailsPublisherTo
ReconciliationObserver
}
// A CompositeClaim for a Composite resource.
// A CompositeClaim of a composite resource (XR).
type CompositeClaim interface { //nolint:interfacebloat // This interface has to be big.
Object
@ -276,9 +321,11 @@ type CompositeClaim interface { //nolint:interfacebloat // This interface has to
CompositeResourceDeleter
CompositeResourceReferencer
LocalConnectionSecretWriterTo
ConnectionDetailsPublisherTo
Conditioned
ConnectionDetailsPublishedTimer
ReconciliationObserver
}
// A Claim of a composite resource (XR).
type Claim = CompositeClaim

Some files were not shown because too many files have changed in this diff Show More