├── .gitmodules ├── .gitignore ├── cluster ├── images │ └── provider-nop │ │ ├── Dockerfile │ │ └── Makefile └── local │ └── integration_tests.sh ├── hack └── boilerplate.go.txt ├── .github ├── workflows │ ├── tag.yml │ ├── codeql.yml │ ├── backport.yml │ ├── promote.yml │ ├── commands.yml │ └── ci.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── internal ├── controller │ ├── doc.go │ ├── controller.go │ ├── nopresource │ │ ├── nopresource.go │ │ └── nopresource_test.go │ └── clusternopresource │ │ ├── clusternopresource.go │ │ └── clusternopresource_test.go └── nopprovider │ └── observe.go ├── apis ├── v1alpha1 │ ├── doc.go │ ├── zz_generated.managedlist.go │ ├── nop_cluster_resource_types.go │ ├── nop_resource_types.go │ ├── register.go │ ├── nop_types.go │ ├── zz_generated.managed.go │ └── zz_generated.deepcopy.go ├── generate.go └── template.go ├── OWNERS.md ├── package ├── crossplane.yaml ├── webhookconfigurations │ └── manifests.yaml └── crds │ ├── nop.crossplane.io_nopresources.yaml │ └── nop.crossplane.io_clusternopresources.yaml ├── examples ├── clusternopresource.yaml └── nopresource.yaml ├── README.md ├── go.mod ├── design └── one-pager-provider-nop.md ├── cmd └── provider │ └── main.go ├── Makefile ├── .golangci.yml ├── LICENSE └── go.sum /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "build"] 2 | path = build 3 | url = https://github.com/crossplane/build 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache 2 | /.work 3 | /_output 4 | cover.out 5 | /vendor 6 | /.vendor-new 7 | .registry 8 | 9 | # ignore GoLand files (debug config etc...) 10 | /.idea 11 | 12 | # ignore vscode files (debug config etc...) 13 | /.vscode 14 | 15 | # ignore asdf local versions 16 | /.tool-versions 17 | -------------------------------------------------------------------------------- /cluster/images/provider-nop/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcr.io/distroless/static@sha256:1f580b0a1922c3e54ae15b0758b5747b260bd99d39d40c2edb3e7f6e2452298b 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | ADD bin/$TARGETOS\_$TARGETARCH/provider /usr/local/bin/provider-nop 7 | 8 | USER 65532 9 | ENTRYPOINT ["provider-nop"] -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version (e.g. v0.1.0)' 8 | required: true 9 | message: 10 | description: 'Tag message' 11 | required: true 12 | 13 | jobs: 14 | create-tag: 15 | runs-on: ubuntu-22.04 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Create Tag 22 | uses: negz/create-tag@v1 23 | with: 24 | version: ${{ github.event.inputs.version }} 25 | message: ${{ github.event.inputs.message }} 26 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /internal/controller/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package controller implements managed resource controllers for provider-nop 18 | package controller 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Help us make Crossplane more useful 4 | labels: enhancement 5 | --- 6 | 13 | 14 | ### What problem are you facing? 15 | 20 | 21 | ### How could Crossplane help solve your problem? 22 | 25 | -------------------------------------------------------------------------------- /apis/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains the v1alpha1 group resources of the nop provider. 18 | // +kubebuilder:object:generate=true 19 | // +groupName=nop.crossplane.io 20 | // +versionName=v1alpha1 21 | package v1alpha1 22 | -------------------------------------------------------------------------------- /OWNERS.md: -------------------------------------------------------------------------------- 1 | # OWNERS 2 | 3 | This page lists all maintainers for **this** repository. Each repository in the [Crossplane 4 | organization](https://github.com/crossplane/) will list their repository maintainers in their own 5 | `OWNERS.md` file. 6 | 7 | Please see the Crossplane 8 | [GOVERNANCE.md](https://github.com/crossplane/crossplane/blob/master/GOVERNANCE.md) for governance 9 | guidelines and responsibilities for the steering committee and maintainers. 10 | 11 | ## Maintainers 12 | 13 | * Nic Cope ([negz](https://github.com/negz)) 14 | * Hasan Turken ([turkenh](https://github.com/turkenh)) 15 | * Philippe Scorsolini ([phisco](https://github.com/phisco)) 16 | * Jared Watts ([jbw976](https://github.com/jbw976)) 17 | 18 | ## Emeritus Maintainers 19 | 20 | * Daniel Mangum ([hasheddan](https://github.com/hasheddan)) 21 | -------------------------------------------------------------------------------- /package/crossplane.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: meta.pkg.crossplane.io/v1 2 | kind: Provider 3 | metadata: 4 | name: provider-nop 5 | annotations: 6 | meta.crossplane.io/company: Crossplane 7 | meta.crossplane.io/maintainer: Crossplane Maintainers 8 | meta.crossplane.io/source: github.com/crossplane-contrib/provider-nop 9 | meta.crossplane.io/license: Apache-2.0 10 | meta.crossplane.io/descriptionShort: | 11 | The nop Crossplane provider enables testing managed resources. 12 | meta.crossplane.io/description: | 13 | The nop Crossplane provider adds support for managing resources that 14 | reflect the conditions from the spec in their status. This can be useful 15 | for testing managing managed resources in larger systems. 16 | meta.crossplane.io/readme: | 17 | The nop Crossplane provider enables testing managed resources. 18 | spec: 19 | capabilities: 20 | - safe-start -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release-* 8 | workflow_dispatch: {} 9 | 10 | jobs: 11 | detect-noop: 12 | runs-on: ubuntu-22.04 13 | outputs: 14 | noop: ${{ steps.noop.outputs.should_skip }} 15 | steps: 16 | - name: Detect No-op Changes 17 | id: noop 18 | uses: fkirc/skip-duplicate-actions@v5.2.0 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | paths_ignore: '["**.md", "**.png", "**.jpg"]' 22 | do_not_skip: '["workflow_dispatch", "schedule", "push"]' 23 | concurrent_skipping: false 24 | 25 | analyze: 26 | runs-on: ubuntu-22.04 27 | needs: detect-noop 28 | if: needs.detect-noop.outputs.noop != 'true' 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | with: 34 | submodules: true 35 | 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v2 38 | with: 39 | languages: go 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v2 43 | -------------------------------------------------------------------------------- /package/webhookconfigurations/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: ValidatingWebhookConfiguration 4 | metadata: 5 | name: validating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /validate-nop-crossplane-io-v1alpha1-clusternopresource 14 | failurePolicy: Fail 15 | name: clusternopresources.nop.crossplane.io 16 | rules: 17 | - apiGroups: 18 | - nop.crossplane.io 19 | apiVersions: 20 | - v1alpha1 21 | operations: 22 | - CREATE 23 | - UPDATE 24 | resources: 25 | - clusternopresources 26 | sideEffects: None 27 | - admissionReviewVersions: 28 | - v1 29 | clientConfig: 30 | service: 31 | name: webhook-service 32 | namespace: system 33 | path: /validate-nop-crossplane-io-v1alpha1-nopresource 34 | failurePolicy: Fail 35 | name: nopresources.nop.crossplane.io 36 | rules: 37 | - apiGroups: 38 | - nop.crossplane.io 39 | apiVersions: 40 | - v1alpha1 41 | operations: 42 | - CREATE 43 | - UPDATE 44 | resources: 45 | - nopresources 46 | sideEffects: None 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Help us diagnose and fix bugs in Crossplane 4 | labels: bug 5 | --- 6 | 13 | 14 | ### What happened? 15 | 19 | 20 | 21 | ### How can we reproduce it? 22 | 27 | 28 | ### What environment did it happen in? 29 | Crossplane version: 30 | 31 | 41 | -------------------------------------------------------------------------------- /cluster/images/provider-nop/Makefile: -------------------------------------------------------------------------------- 1 | # ==================================================================================== 2 | # Setup Project 3 | 4 | include ../../../build/makelib/common.mk 5 | 6 | # ==================================================================================== 7 | # Options 8 | 9 | include ../../../build/makelib/imagelight.mk 10 | 11 | # ==================================================================================== 12 | # Targets 13 | 14 | img.build: 15 | @$(INFO) docker build $(IMAGE) 16 | @$(MAKE) BUILD_ARGS="--load" img.build.shared 17 | @$(OK) docker build $(IMAGE) 18 | 19 | img.publish: 20 | @$(INFO) Skipping image publish for $(IMAGE) 21 | @echo Publish is deferred to xpkg machinery 22 | @$(OK) Image publish skipped for $(IMAGE) 23 | 24 | img.build.shared: 25 | @cp Dockerfile $(IMAGE_TEMP_DIR) || $(FAIL) 26 | @cp -r $(OUTPUT_DIR)/bin/ $(IMAGE_TEMP_DIR)/bin || $(FAIL) 27 | @docker buildx build $(BUILD_ARGS) \ 28 | --platform $(IMAGE_PLATFORMS) \ 29 | -t $(IMAGE) \ 30 | $(IMAGE_TEMP_DIR) || $(FAIL) 31 | 32 | img.promote: 33 | @$(INFO) Skipping image promotion from $(FROM_IMAGE) to $(TO_IMAGE) 34 | @echo Promote is deferred to xpkg machinery 35 | @$(OK) Image promotion skipped for $(FROM_IMAGE) to $(TO_IMAGE) 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Description of your changes 10 | 11 | 20 | Fixes # 21 | 22 | I have: 23 | 24 | - [ ] Read and followed Crossplane's [contribution process]. 25 | - [ ] Run `make reviewable` to ensure this PR is ready for review. 26 | - [ ] Added `backport release-x.y` labels to auto-backport this PR if necessary. 27 | 28 | ### How has this code been tested 29 | 30 | 35 | 36 | [contribution process]: https://git.io/fj2m9 37 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | 3 | on: 4 | # NOTE(negz): This is a risky target, but we run this action only when and if 5 | # a PR is closed, then filter down to specifically merged PRs. We also don't 6 | # invoke any scripts, etc from within the repo. I believe the fact that we'll 7 | # be able to review PRs before this runs makes this fairly safe. 8 | # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 9 | pull_request_target: 10 | types: [closed] 11 | # See also commands.yml for the /backport triggered variant of this workflow. 12 | 13 | jobs: 14 | # NOTE(negz): I tested many backport GitHub actions before landing on this 15 | # one. Many do not support merge commits, or do not support pull requests with 16 | # more than one commit. This one does. It also handily links backport PRs with 17 | # new PRs, and provides commentary and instructions when it can't backport. 18 | # The main gotcha with this action is that PRs _must_ be labelled before they're 19 | # merged to trigger a backport. 20 | open-pr: 21 | runs-on: ubuntu-22.04 22 | if: github.event.pull_request.merged 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Open Backport PR 28 | uses: korthout/backport-action@v1 29 | -------------------------------------------------------------------------------- /apis/generate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Remove existing manifests 18 | //go:generate rm -rf ../package/crds 19 | //go:generate rm -rf ../package/webhookconfigurations 20 | 21 | // Generate deepcopy methodsets and CRD manifests 22 | //go:generate go tool controller-gen object:headerFile=../hack/boilerplate.go.txt paths=./... crd:crdVersions=v1 output:artifacts:config=../package/crds 23 | 24 | // Generate crossplane-runtime methodsets 25 | //go:generate go tool angryjet generate-methodsets --header-file=../hack/boilerplate.go.txt ../apis/... 26 | 27 | // Generate webhook manifests 28 | //go:generate go tool controller-gen webhook paths=./... output:artifacts:config=../package/webhookconfigurations 29 | 30 | package apis 31 | -------------------------------------------------------------------------------- /apis/v1alpha1/zz_generated.managedlist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1alpha1 19 | 20 | import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 21 | 22 | // GetItems of this ClusterNopResourceList. 23 | func (l *ClusterNopResourceList) GetItems() []resource.Managed { 24 | items := make([]resource.Managed, len(l.Items)) 25 | for i := range l.Items { 26 | items[i] = &l.Items[i] 27 | } 28 | return items 29 | } 30 | 31 | // GetItems of this NopResourceList. 32 | func (l *NopResourceList) GetItems() []resource.Managed { 33 | items := make([]resource.Managed, len(l.Items)) 34 | for i := range l.Items { 35 | items[i] = &l.Items[i] 36 | } 37 | return items 38 | } 39 | -------------------------------------------------------------------------------- /apis/template.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package apis contains Kubernetes API for the nop provider. 18 | package apis 19 | 20 | import ( 21 | "k8s.io/apimachinery/pkg/runtime" 22 | 23 | v1alpha1 "github.com/crossplane-contrib/provider-nop/apis/v1alpha1" 24 | ) 25 | 26 | func init() { 27 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 28 | AddToSchemes = append(AddToSchemes, 29 | v1alpha1.SchemeBuilder.AddToScheme, 30 | ) 31 | } 32 | 33 | // AddToSchemes may be used to add all resources defined in the project to a Scheme. 34 | var AddToSchemes runtime.SchemeBuilder 35 | 36 | // AddToScheme adds all Resources to the Scheme. 37 | func AddToScheme(s *runtime.Scheme) error { 38 | return AddToSchemes.AddToScheme(s) 39 | } 40 | -------------------------------------------------------------------------------- /internal/controller/controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | ctrl "sigs.k8s.io/controller-runtime" 21 | 22 | "github.com/crossplane/crossplane-runtime/v2/pkg/controller" 23 | 24 | "github.com/crossplane-contrib/provider-nop/internal/controller/clusternopresource" 25 | "github.com/crossplane-contrib/provider-nop/internal/controller/nopresource" 26 | ) 27 | 28 | // Setup creates all nop controllers with the supplied logger and adds them to 29 | // the supplied manager. 30 | func Setup(mgr ctrl.Manager, o controller.Options) error { 31 | for _, setup := range []func(ctrl.Manager, controller.Options) error{ 32 | nopresource.SetupGated, 33 | clusternopresource.SetupGated, 34 | } { 35 | if err := setup(mgr, o); err != nil { 36 | return err 37 | } 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/promote.yml: -------------------------------------------------------------------------------- 1 | name: Promote 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version (e.g. v0.1.0)' 8 | required: true 9 | channel: 10 | description: 'Release channel' 11 | required: true 12 | default: 'stable' 13 | 14 | env: 15 | # Common users. We can't run a step 'if secrets.AWS_USR != ""' but we can run 16 | # a step 'if env.AWS_USR' != ""', so we copy these to succinctly test whether 17 | # credentials have been provided before trying to run steps that need them. 18 | DOCKER_USR: ${{ secrets.DOCKER_USR }} 19 | AWS_USR: ${{ secrets.AWS_USR }} 20 | 21 | jobs: 22 | promote-artifacts: 23 | runs-on: ubuntu-22.04 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | with: 29 | submodules: true 30 | 31 | - name: Fetch History 32 | run: git fetch --prune --unshallow 33 | 34 | - name: Login to Docker 35 | uses: docker/login-action@v1 36 | if: env.DOCKER_USR != '' 37 | with: 38 | username: ${{ secrets.DOCKER_USR }} 39 | password: ${{ secrets.DOCKER_PSW }} 40 | 41 | - name: Promote Artifacts in S3 and Docker Hub 42 | if: env.AWS_USR != '' && env.DOCKER_USR != '' 43 | run: make -j2 promote BRANCH_NAME=${GITHUB_REF##*/} 44 | env: 45 | VERSION: ${{ github.event.inputs.version }} 46 | CHANNEL: ${{ github.event.inputs.channel }} 47 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_USR }} 48 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_PSW }} 49 | -------------------------------------------------------------------------------- /apis/v1alpha1/nop_cluster_resource_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // +kubebuilder:object:root=true 24 | 25 | // A ClusterNopResource is an example API type. 26 | // +kubebuilder:subresource:status 27 | // +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" 28 | // +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" 29 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 30 | // +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,nop} 31 | type ClusterNopResource struct { 32 | metav1.TypeMeta `json:",inline"` 33 | metav1.ObjectMeta `json:"metadata,omitempty"` 34 | 35 | Spec NopSpec `json:"spec"` 36 | Status NopStatus `json:"status,omitempty"` 37 | } 38 | 39 | // +kubebuilder:object:root=true 40 | 41 | // ClusterNopResourceList contains a list of ClusterNopResource. 42 | type ClusterNopResourceList struct { 43 | metav1.TypeMeta `json:",inline"` 44 | metav1.ListMeta `json:"metadata,omitempty"` 45 | 46 | Items []ClusterNopResource `json:"items"` 47 | } 48 | -------------------------------------------------------------------------------- /examples/clusternopresource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nop.crossplane.io/v1alpha1 2 | kind: ClusterNopResource 3 | metadata: 4 | name: example 5 | spec: 6 | forProvider: 7 | # The NopResource spec.forProvider.fields is an arbitrary, 8 | # schemaless object. Use it to patch to and from. 9 | # status.atProvider.fields works the same. 10 | fields: 11 | integerField: 42 12 | stringField: "cool" 13 | objectField: 14 | stringField: "cool" 15 | arrayField: 16 | - stringField: "cool" 17 | # This NopResource will set its 'Ready' status condition to 'True' 18 | # after 30 seconds, etc. Note that these conditions will only be processed 19 | # as frequently as the provider's --poll-interval, which defaults to 10s. 20 | # This means that by default granularity of less than 10s is ignored. 21 | conditionAfter: 22 | - time: 30s 23 | conditionType: Ready 24 | conditionStatus: "True" 25 | - time: 60s 26 | conditionType: Ready 27 | conditionStatus: "False" 28 | - time: 90s 29 | conditionType: Ready 30 | conditionStatus: "True" 31 | - time: 90s 32 | conditionType: Green 33 | conditionStatus: "True" 34 | # The NopResource will emit whatever connection details it is told 35 | # to have. These are all plaintext - for testing only. 36 | connectionDetails: 37 | - name: username 38 | value: fakeuser 39 | - name: password 40 | value: verysecurepassword 41 | - name: endpoint 42 | value: 127.0.0.1 43 | # Like all managed resources the NopResource allows you to configure a 44 | # provider config. It ignores the configured value. 45 | providerConfigRef: 46 | name: default 47 | # Simulating connection details (see connectionDetails above) works 48 | # only when the NopResource writes a connection secret. 49 | writeConnectionSecretToRef: 50 | namespace: crossplane-system 51 | name: nop-example-resource -------------------------------------------------------------------------------- /examples/nopresource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nop.crossplane.io/v1alpha1 2 | kind: NopResource 3 | metadata: 4 | name: example 5 | namespace: default 6 | spec: 7 | forProvider: 8 | # The NopResource spec.forProvider.fields is an arbitrary, 9 | # schemaless object. Use it to patch to and from. 10 | # status.atProvider.fields works the same. 11 | fields: 12 | integerField: 42 13 | stringField: "cool" 14 | objectField: 15 | stringField: "cool" 16 | arrayField: 17 | - stringField: "cool" 18 | # This NopResource will set its 'Ready' status condition to 'True' 19 | # after 30 seconds, etc. Note that these conditions will only be processed 20 | # as frequently as the provider's --poll-interval, which defaults to 10s. 21 | # This means that by default granularity of less than 10s is ignored. 22 | conditionAfter: 23 | - time: 30s 24 | conditionType: Ready 25 | conditionStatus: "True" 26 | - time: 60s 27 | conditionType: Ready 28 | conditionStatus: "False" 29 | - time: 90s 30 | conditionType: Ready 31 | conditionStatus: "True" 32 | - time: 90s 33 | conditionType: Green 34 | conditionStatus: "True" 35 | # The NopResource will emit whatever connection details it is told 36 | # to have. These are all plaintext - for testing only. 37 | connectionDetails: 38 | - name: username 39 | value: fakeuser 40 | - name: password 41 | value: verysecurepassword 42 | - name: endpoint 43 | value: 127.0.0.1 44 | # Like all managed resources the NopResource allows you to configure a 45 | # provider config. It ignores the configured value. 46 | providerConfigRef: 47 | name: default 48 | # Simulating connection details (see connectionDetails above) works 49 | # only when the NopResource writes a connection secret. 50 | writeConnectionSecretToRef: 51 | namespace: default 52 | name: nop-example-resource -------------------------------------------------------------------------------- /apis/v1alpha1/nop_resource_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 23 | "github.com/crossplane/crossplane-runtime/v2/pkg/conditions" 24 | ) 25 | 26 | // +kubebuilder:object:root=true 27 | 28 | // A NopResource is an example API type. 29 | // +kubebuilder:subresource:status 30 | // +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" 31 | // +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" 32 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 33 | // +kubebuilder:resource:scope=Namespaced,categories={crossplane,managed,nop} 34 | type NopResource struct { 35 | metav1.TypeMeta `json:",inline"` 36 | metav1.ObjectMeta `json:"metadata,omitempty"` 37 | 38 | Spec NopSpec `json:"spec"` 39 | Status NopStatus `json:"status,omitempty"` 40 | } 41 | 42 | // SetConditions sets the supplied conditions, replacing any existing conditions 43 | // of the same type. This is a no-op if all supplied conditions are identical, 44 | // ignoring the last transition time, to those already set. 45 | func (in *NopResource) SetConditions(c ...xpv1.Condition) { 46 | in.Status.SetConditions(c...) 47 | } 48 | 49 | // GetCondition returns the status condition for the given ConditionType if exists, 50 | // otherwise returns nil. 51 | func (in *NopResource) GetCondition(ct xpv1.ConditionType) xpv1.Condition { 52 | return in.Status.GetCondition(ct) 53 | } 54 | 55 | var _ conditions.ObjectWithConditions = (*NopResource)(nil) 56 | 57 | // +kubebuilder:object:root=true 58 | 59 | // NopResourceList contains a list of NopResource. 60 | type NopResourceList struct { 61 | metav1.TypeMeta `json:",inline"` 62 | metav1.ListMeta `json:"metadata,omitempty"` 63 | 64 | Items []NopResource `json:"items"` 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/commands.yml: -------------------------------------------------------------------------------- 1 | name: Comment Commands 2 | 3 | on: issue_comment 4 | 5 | jobs: 6 | points: 7 | runs-on: ubuntu-22.04 8 | if: startsWith(github.event.comment.body, '/points') 9 | 10 | steps: 11 | - name: Extract Command 12 | id: command 13 | uses: xt0rted/slash-command-action@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | command: points 17 | reaction: "true" 18 | reaction-type: "eyes" 19 | allow-edits: "false" 20 | permission-level: write 21 | - name: Handle Command 22 | uses: actions/github-script@v4 23 | env: 24 | POINTS: ${{ steps.command.outputs.command-arguments }} 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | script: | 28 | const points = process.env.POINTS 29 | 30 | if (isNaN(parseInt(points))) { 31 | console.log("Malformed command - expected '/points '") 32 | github.reactions.createForIssueComment({ 33 | owner: context.repo.owner, 34 | repo: context.repo.repo, 35 | comment_id: context.payload.comment.id, 36 | content: "confused" 37 | }) 38 | return 39 | } 40 | const label = "points/" + points 41 | 42 | // Delete our needs-points-label label. 43 | try { 44 | await github.issues.deleteLabel({ 45 | issue_number: context.issue.number, 46 | owner: context.repo.owner, 47 | repo: context.repo.repo, 48 | name: ['needs-points-label'] 49 | }) 50 | console.log("Deleted 'needs-points-label' label.") 51 | } 52 | catch(e) { 53 | console.log("Label 'needs-points-label' probably didn't exist.") 54 | } 55 | 56 | // Add our points label. 57 | github.issues.addLabels({ 58 | issue_number: context.issue.number, 59 | owner: context.repo.owner, 60 | repo: context.repo.repo, 61 | labels: [label] 62 | }) 63 | console.log("Added '" + label + "' label.") 64 | 65 | # NOTE(negz): See also backport.yml, which is the variant that triggers on PR 66 | # merge rather than on comment. 67 | backport: 68 | runs-on: ubuntu-22.04 69 | if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/backport') 70 | steps: 71 | - name: Extract Command 72 | id: command 73 | uses: xt0rted/slash-command-action@v1 74 | with: 75 | repo-token: ${{ secrets.GITHUB_TOKEN }} 76 | command: backport 77 | reaction: "true" 78 | reaction-type: "eyes" 79 | allow-edits: "false" 80 | permission-level: write 81 | 82 | - name: Checkout 83 | uses: actions/checkout@v3 84 | 85 | - name: Open Backport PR 86 | uses: korthout/backport-action@v1 87 | -------------------------------------------------------------------------------- /apis/v1alpha1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "reflect" 21 | 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | "sigs.k8s.io/controller-runtime/pkg/scheme" 24 | 25 | "github.com/crossplane/crossplane-runtime/v2/pkg/webhook" 26 | ) 27 | 28 | // Package type metadata. 29 | const ( 30 | Group = "nop.crossplane.io" 31 | Version = "v1alpha1" 32 | ) 33 | 34 | var ( 35 | // SchemeGroupVersion is group version used to register these objects. 36 | SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} 37 | 38 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 39 | SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} 40 | ) 41 | 42 | // NopResource type metadata. 43 | var ( 44 | ClusterNopResourceKind = reflect.TypeOf(ClusterNopResource{}).Name() 45 | ClusterNopResourceGroupKind = schema.GroupKind{Group: Group, Kind: ClusterNopResourceKind}.String() 46 | ClusterNopResourceKindAPIVersion = NopResourceKind + "." + SchemeGroupVersion.String() 47 | ClusterNopResourceGroupVersionKind = SchemeGroupVersion.WithKind(ClusterNopResourceKind) 48 | 49 | NopResourceKind = reflect.TypeOf(NopResource{}).Name() 50 | NopResourceGroupKind = schema.GroupKind{Group: Group, Kind: NopResourceKind}.String() 51 | NopResourceKindAPIVersion = NopResourceKind + "." + SchemeGroupVersion.String() 52 | NopResourceGroupVersionKind = SchemeGroupVersion.WithKind(NopResourceKind) 53 | 54 | // NopResourceValidator is doing nothing on purpose at the moment, you now... a nop validator. 55 | NopResourceValidator = webhook.NewValidator() 56 | 57 | // ClusterNopResourceValidator is doing nothing on purpose at the moment, you now... a nop validator. 58 | ClusterNopResourceValidator = webhook.NewValidator() 59 | ) 60 | 61 | func init() { 62 | SchemeBuilder.Register( 63 | &NopResource{}, &NopResourceList{}, 64 | &ClusterNopResource{}, &ClusterNopResourceList{}, 65 | ) 66 | } 67 | 68 | // +kubebuilder:webhook:verbs=create;update,path=/validate-nop-crossplane-io-v1alpha1-nopresource,mutating=false,failurePolicy=fail,groups=nop.crossplane.io,resources=nopresources,versions=v1alpha1,name=nopresources.nop.crossplane.io,sideEffects=None,admissionReviewVersions=v1 69 | // +kubebuilder:webhook:verbs=create;update,path=/validate-nop-crossplane-io-v1alpha1-clusternopresource,mutating=false,failurePolicy=fail,groups=nop.crossplane.io,resources=clusternopresources,versions=v1alpha1,name=clusternopresources.nop.crossplane.io,sideEffects=None,admissionReviewVersions=v1 70 | -------------------------------------------------------------------------------- /internal/nopprovider/observe.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package nopprovider is logic for the controllers for a managed resource that does nothing. 18 | package nopprovider 19 | 20 | import ( 21 | "sort" 22 | "time" 23 | 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | 26 | xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 27 | "github.com/crossplane/crossplane-runtime/v2/pkg/conditions" 28 | "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" 29 | 30 | "github.com/crossplane-contrib/provider-nop/apis/v1alpha1" 31 | ) 32 | 33 | // Observe doesn't actually observe an external resource. Instead, it sets the 34 | // most recent conditions that should occur per nop.conditionAfter. 35 | func Observe(nop v1alpha1.NopParameters, age time.Duration, status conditions.ConditionSet) (managed.ExternalObservation, error) { 36 | // Sort conditions, with those that should occur latest appearing first. 37 | // We rely on the fact that the managed.Reconciler won't persist this sorted 38 | // array because it occurred during the Observe function, and we didn't 39 | // return ResourceLateInitialized: true. 40 | sort.SliceStable(nop.ConditionAfter, func(i, j int) bool { 41 | return nop.ConditionAfter[i].Time.Duration > nop.ConditionAfter[j].Time.Duration 42 | }) 43 | 44 | set := map[xpv1.ConditionType]bool{} 45 | for _, ca := range nop.ConditionAfter { 46 | if ca.Time.Duration > age { 47 | // This condition should not occur yet. 48 | continue 49 | } 50 | 51 | if set[ca.ConditionType] { 52 | // We already encountered and set a condition of this type. 53 | continue 54 | } 55 | 56 | // This is the latest condition of this type that should be set. 57 | var reason xpv1.ConditionReason 58 | if ca.ConditionReason != nil { 59 | reason = *ca.ConditionReason 60 | } 61 | status.MarkConditions(xpv1.Condition{ 62 | Type: ca.ConditionType, 63 | Status: ca.ConditionStatus, 64 | Reason: reason, 65 | LastTransitionTime: metav1.Now(), 66 | }) 67 | 68 | set[ca.ConditionType] = true 69 | } 70 | 71 | // Emit any connection details we were asked to. 72 | cd := managed.ConnectionDetails{} 73 | for _, nv := range nop.ConnectionDetails { 74 | cd[nv.Name] = []byte(nv.Value) 75 | } 76 | 77 | // If our managed resource has not been deleted we report that our 78 | // pretend external resource exists and is up-to-date. This means 79 | // we'll never call the CreateFn or UpdateFn. 80 | return managed.ExternalObservation{ResourceExists: true, ResourceUpToDate: true, ConnectionDetails: cd}, nil 81 | } 82 | -------------------------------------------------------------------------------- /apis/v1alpha1/nop_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | 24 | xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 25 | ) 26 | 27 | // ConditionAfter specifies a condition of a NopResource that should be 28 | // set after a certain duration. 29 | type ConditionAfter struct { 30 | // Time is the duration after which the condition should be set. 31 | Time metav1.Duration `json:"time"` 32 | 33 | // ConditionType to set - e.g. Ready. 34 | ConditionType xpv1.ConditionType `json:"conditionType"` 35 | 36 | // ConditionStatus to set - e.g. True. 37 | ConditionStatus corev1.ConditionStatus `json:"conditionStatus"` 38 | 39 | // ConditionReason to set - e.g. Available. 40 | // +optional 41 | ConditionReason *xpv1.ConditionReason `json:"conditionReason,omitempty"` 42 | } 43 | 44 | // ConnectionDetail specifies a connection detail a NopResource should 45 | // emit. 46 | type ConnectionDetail struct { 47 | // Name of the connection detail. 48 | Name string `json:"name"` 49 | 50 | // Value of the connection detail. 51 | Value string `json:"value"` 52 | } 53 | 54 | // NopParameters are the configurable fields of a NopResource. 55 | type NopParameters struct { 56 | // ConditionAfter can be used to set status conditions after a specified 57 | // time. By default, a NopResource will only have a status condition of 58 | // Type: Synced. It will never have a status condition of Type: Ready 59 | // unless one is configured here. 60 | // +optional 61 | ConditionAfter []ConditionAfter `json:"conditionAfter,omitempty"` 62 | 63 | // ConnectionDetails that this NopResource should emit on each reconcile. 64 | // +optional 65 | ConnectionDetails []ConnectionDetail `json:"connectionDetails,omitempty"` 66 | 67 | // Fields is an arbitrary object you can patch to and from. It has no 68 | // schema, is not validated, and is not used by the NopResource controller. 69 | // +optional 70 | Fields runtime.RawExtension `json:"fields,omitempty"` 71 | } 72 | 73 | // NopObservation are the observable fields of a NopResource. 74 | type NopObservation struct { 75 | // Fields is an arbitrary object you can patch to and from. It has no 76 | // schema, is not validated, and is not used by the NopResource controller. 77 | // +optional 78 | Fields runtime.RawExtension `json:"fields,omitempty"` 79 | } 80 | 81 | // A NopSpec defines the desired state of a NopResource. 82 | type NopSpec struct { 83 | xpv1.ResourceSpec `json:",inline"` 84 | 85 | ForProvider NopParameters `json:"forProvider"` 86 | } 87 | 88 | // A NopStatus represents the observed state of a NopResource. 89 | type NopStatus struct { 90 | xpv1.ResourceStatus `json:",inline"` 91 | 92 | AtProvider NopObservation `json:"atProvider,omitempty"` 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # provider-nop 2 | 3 | `provider-nop` is a Crossplane provider that does nothing. It provides two 4 | managed resources - `ClusterNopResource` and `NopResource` that does not 5 | orchestrate any external system. Each `[Cluster]NopResource` can be configured 6 | to emit arbitrary status conditions after a specified period of time. A 7 | `[Cluster]NopResource` can also emit arbitrary connection details. 8 | 9 | The main value of a `ClusterNopResource` is that it can be used to create a Crossplane 10 | `Composition` that can satisfy any kind of composite resource by doing nothing. 11 | This can be useful for systems that automatically create a real composite 12 | resource (one that composes real cloud infrastructure) when running in 13 | production, but that wish to avoid creating real infrastructure when running in 14 | development. It can also be useful for developing and testing Crossplane itself. 15 | 16 | The below `Composition` satisfies the `SQLInstance` composite resource kind by 17 | by composing a `ClusterNopResource`. When an `SQLInstance` is created it will become 18 | ready and write fake data to a connection secret. 19 | 20 | ```yaml 21 | apiVersion: apiextensions.crossplane.io/v1 22 | kind: Composition 23 | metadata: 24 | name: nop.sqlinstances.example.org 25 | spec: 26 | writeConnectionSecretsToNamespace: crossplane-system 27 | compositeTypeRef: 28 | apiVersion: example.org/v1alpha1 29 | kind: SQLInstance 30 | resources: 31 | - name: nop 32 | base: 33 | apiVersion: nop.crossplane.io/v1alpha1 34 | kind: ClusterNopResource 35 | spec: 36 | forProvider: 37 | # The NopResource spec.forProvider.fields is an arbitrary, 38 | # schemaless object. Use it to patch to and from. 39 | # status.atProvider.fields works the same. 40 | fields: 41 | integerField: 42 42 | stringField: "cool" 43 | objectField: 44 | stringField: "cool" 45 | arrayField: 46 | - stringField: "cool" 47 | # This NopResource will set its 'Ready' status condition to 'True' 48 | # after 30 seconds, etc. Note that these conditions will only be processed 49 | # as frequently as the provider's --poll-interval, which defaults to 10s. 50 | # This means that by default granularity of less than 10s is ignored. 51 | conditionAfter: 52 | - time: 30s 53 | conditionType: Ready 54 | conditionStatus: "True" 55 | - time: 60s 56 | conditionType: Ready 57 | conditionStatus: "False" 58 | - time: 90s 59 | conditionType: Ready 60 | conditionStatus: "True" 61 | - time: 90s 62 | conditionType: Green 63 | conditionStatus: "True" 64 | # The NopResource will emit whatever connection details it is told 65 | # to have. These are all plaintext - for testing only. 66 | connectionDetails: 67 | - name: username 68 | value: fakeuser 69 | - name: password 70 | value: verysecurepassword 71 | - name: endpoint 72 | value: 127.0.0.1 73 | # Like all managed resources the NopResource allows you to configure a 74 | # provider config. It ignores the configured value. 75 | providerConfigRef: 76 | name: default 77 | # Simulating connection details (see connectionDetails above) works 78 | # only when the NopResource writes a connection secret. 79 | writeConnectionSecretToRef: 80 | namespace: crossplane-system 81 | name: nop-example-resource 82 | patches: 83 | # You can use the schemaless 'fields' objects to patch to and from. 84 | - type: FromCompositeFieldPath 85 | fromFieldPath: spec.parameters.storageGB 86 | toFieldPath: spec.forProvider.fields.storageGB 87 | transforms: 88 | - type: string 89 | string: 90 | fmt: "%d" 91 | - type: ToCompositeFieldPath 92 | fromFieldPath: status.atProvider.fields.health 93 | toFieldPath: status.health 94 | ``` 95 | -------------------------------------------------------------------------------- /apis/v1alpha1/zz_generated.managed.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1alpha1 19 | 20 | import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 21 | 22 | // GetCondition of this ClusterNopResource. 23 | func (mg *ClusterNopResource) GetCondition(ct xpv1.ConditionType) xpv1.Condition { 24 | return mg.Status.GetCondition(ct) 25 | } 26 | 27 | // GetDeletionPolicy of this ClusterNopResource. 28 | func (mg *ClusterNopResource) GetDeletionPolicy() xpv1.DeletionPolicy { 29 | return mg.Spec.DeletionPolicy 30 | } 31 | 32 | // GetManagementPolicies of this ClusterNopResource. 33 | func (mg *ClusterNopResource) GetManagementPolicies() xpv1.ManagementPolicies { 34 | return mg.Spec.ManagementPolicies 35 | } 36 | 37 | // GetProviderConfigReference of this ClusterNopResource. 38 | func (mg *ClusterNopResource) GetProviderConfigReference() *xpv1.Reference { 39 | return mg.Spec.ProviderConfigReference 40 | } 41 | 42 | // GetWriteConnectionSecretToReference of this ClusterNopResource. 43 | func (mg *ClusterNopResource) GetWriteConnectionSecretToReference() *xpv1.SecretReference { 44 | return mg.Spec.WriteConnectionSecretToReference 45 | } 46 | 47 | // SetConditions of this ClusterNopResource. 48 | func (mg *ClusterNopResource) SetConditions(c ...xpv1.Condition) { 49 | mg.Status.SetConditions(c...) 50 | } 51 | 52 | // SetDeletionPolicy of this ClusterNopResource. 53 | func (mg *ClusterNopResource) SetDeletionPolicy(r xpv1.DeletionPolicy) { 54 | mg.Spec.DeletionPolicy = r 55 | } 56 | 57 | // SetManagementPolicies of this ClusterNopResource. 58 | func (mg *ClusterNopResource) SetManagementPolicies(r xpv1.ManagementPolicies) { 59 | mg.Spec.ManagementPolicies = r 60 | } 61 | 62 | // SetProviderConfigReference of this ClusterNopResource. 63 | func (mg *ClusterNopResource) SetProviderConfigReference(r *xpv1.Reference) { 64 | mg.Spec.ProviderConfigReference = r 65 | } 66 | 67 | // SetWriteConnectionSecretToReference of this ClusterNopResource. 68 | func (mg *ClusterNopResource) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { 69 | mg.Spec.WriteConnectionSecretToReference = r 70 | } 71 | 72 | // GetDeletionPolicy of this NopResource. 73 | func (mg *NopResource) GetDeletionPolicy() xpv1.DeletionPolicy { 74 | return mg.Spec.DeletionPolicy 75 | } 76 | 77 | // GetManagementPolicies of this NopResource. 78 | func (mg *NopResource) GetManagementPolicies() xpv1.ManagementPolicies { 79 | return mg.Spec.ManagementPolicies 80 | } 81 | 82 | // GetProviderConfigReference of this NopResource. 83 | func (mg *NopResource) GetProviderConfigReference() *xpv1.Reference { 84 | return mg.Spec.ProviderConfigReference 85 | } 86 | 87 | // GetWriteConnectionSecretToReference of this NopResource. 88 | func (mg *NopResource) GetWriteConnectionSecretToReference() *xpv1.SecretReference { 89 | return mg.Spec.WriteConnectionSecretToReference 90 | } 91 | 92 | // SetDeletionPolicy of this NopResource. 93 | func (mg *NopResource) SetDeletionPolicy(r xpv1.DeletionPolicy) { 94 | mg.Spec.DeletionPolicy = r 95 | } 96 | 97 | // SetManagementPolicies of this NopResource. 98 | func (mg *NopResource) SetManagementPolicies(r xpv1.ManagementPolicies) { 99 | mg.Spec.ManagementPolicies = r 100 | } 101 | 102 | // SetProviderConfigReference of this NopResource. 103 | func (mg *NopResource) SetProviderConfigReference(r *xpv1.Reference) { 104 | mg.Spec.ProviderConfigReference = r 105 | } 106 | 107 | // SetWriteConnectionSecretToReference of this NopResource. 108 | func (mg *NopResource) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { 109 | mg.Spec.WriteConnectionSecretToReference = r 110 | } 111 | -------------------------------------------------------------------------------- /internal/controller/nopresource/nopresource.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package nopresource is a controller for a managed resource that does nothing. 18 | package nopresource 19 | 20 | import ( 21 | "context" 22 | "time" 23 | 24 | ctrl "sigs.k8s.io/controller-runtime" 25 | 26 | "github.com/crossplane/crossplane-runtime/v2/pkg/conditions" 27 | "github.com/crossplane/crossplane-runtime/v2/pkg/controller" 28 | "github.com/crossplane/crossplane-runtime/v2/pkg/errors" 29 | "github.com/crossplane/crossplane-runtime/v2/pkg/event" 30 | "github.com/crossplane/crossplane-runtime/v2/pkg/meta" 31 | "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" 32 | "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" 33 | "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 34 | "github.com/crossplane/crossplane-runtime/v2/pkg/statemetrics" 35 | 36 | "github.com/crossplane-contrib/provider-nop/apis/v1alpha1" 37 | "github.com/crossplane-contrib/provider-nop/internal/nopprovider" 38 | ) 39 | 40 | // SetupGated registers Setup with the Gate, waiting for the NopResource GKV. 41 | func SetupGated(mgr ctrl.Manager, o controller.Options) error { 42 | o.Gate.Register(func() { 43 | if err := Setup(mgr, o); err != nil { 44 | panic(err) 45 | } 46 | }, v1alpha1.NopResourceGroupVersionKind) 47 | return nil 48 | } 49 | 50 | // Setup adds a controller that reconciles NopResource managed resources. 51 | func Setup(mgr ctrl.Manager, o controller.Options) error { 52 | name := managed.ControllerName(v1alpha1.NopResourceGroupKind) 53 | 54 | r := managed.NewReconciler(mgr, 55 | resource.ManagedKind(v1alpha1.NopResourceGroupVersionKind), 56 | managed.WithPollInterval(o.PollInterval), 57 | managed.WithExternalConnector(&connector{}), 58 | managed.WithLogger(o.Logger.WithValues("controller", name)), 59 | managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), 60 | managed.WithMetricRecorder(o.MetricOptions.MRMetrics), 61 | ) 62 | 63 | if err := ctrl.NewWebhookManagedBy(mgr). 64 | For(&v1alpha1.NopResource{}). 65 | WithValidator(v1alpha1.NopResourceValidator). 66 | Complete(); err != nil { 67 | return errors.Wrap(err, "cannot set up webhooks") 68 | } 69 | 70 | if err := mgr.Add(statemetrics.NewMRStateRecorder( 71 | mgr.GetClient(), o.Logger, o.MetricOptions.MRStateMetrics, &v1alpha1.NopResourceList{}, o.MetricOptions.PollStateMetricInterval)); err != nil { 72 | return err 73 | } 74 | return ctrl.NewControllerManagedBy(mgr). 75 | Named(name). 76 | WithOptions(o.ForControllerRuntime()). 77 | WithEventFilter(resource.DesiredStateChanged()). 78 | For(&v1alpha1.NopResource{}). 79 | Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) 80 | } 81 | 82 | type connector struct{} 83 | 84 | func (c *connector) Connect(_ context.Context, _ resource.Managed) (managed.ExternalClient, error) { 85 | return managed.ExternalClientFns{ 86 | ObserveFn: Observe, 87 | DisconnectFn: func(_ context.Context) error { 88 | return nil 89 | }, 90 | }, nil 91 | } 92 | 93 | // Observe doesn't actually observe an external resource. Instead, it sets the 94 | // most recent conditions that should occur per spec.forProvider.conditionAfter. 95 | func Observe(_ context.Context, mg resource.Managed) (managed.ExternalObservation, error) { 96 | // If our managed resource has been deleted we need to report that 97 | // our pretend external resource is gone in order for the delete 98 | // process to complete. This means we'll never call the DeleteFn. 99 | if meta.WasDeleted(mg) { 100 | return managed.ExternalObservation{ResourceExists: false}, nil 101 | } 102 | 103 | nop, ok := mg.(*v1alpha1.NopResource) 104 | if !ok { 105 | return managed.ExternalObservation{}, errors.Errorf("managed resource was not a %T", &v1alpha1.NopResource{}) 106 | } 107 | status := conditions.ObservedGenerationPropagationManager{}.For(nop) 108 | age := time.Since(nop.CreationTimestamp.Time) 109 | 110 | return nopprovider.Observe(nop.Spec.ForProvider, age, status) 111 | } 112 | -------------------------------------------------------------------------------- /internal/controller/clusternopresource/clusternopresource.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package clusternopresource is a controller for a managed resource that does nothing. 18 | package clusternopresource 19 | 20 | import ( 21 | "context" 22 | "time" 23 | 24 | ctrl "sigs.k8s.io/controller-runtime" 25 | 26 | "github.com/crossplane/crossplane-runtime/v2/pkg/conditions" 27 | "github.com/crossplane/crossplane-runtime/v2/pkg/controller" 28 | "github.com/crossplane/crossplane-runtime/v2/pkg/errors" 29 | "github.com/crossplane/crossplane-runtime/v2/pkg/event" 30 | "github.com/crossplane/crossplane-runtime/v2/pkg/meta" 31 | "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" 32 | "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" 33 | "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 34 | "github.com/crossplane/crossplane-runtime/v2/pkg/statemetrics" 35 | 36 | "github.com/crossplane-contrib/provider-nop/apis/v1alpha1" 37 | "github.com/crossplane-contrib/provider-nop/internal/nopprovider" 38 | ) 39 | 40 | // SetupGated registers Setup with the Gate, waiting for the NopResource GKV. 41 | func SetupGated(mgr ctrl.Manager, o controller.Options) error { 42 | o.Gate.Register(func() { 43 | if err := Setup(mgr, o); err != nil { 44 | panic(err) 45 | } 46 | }, v1alpha1.ClusterNopResourceGroupVersionKind) 47 | return nil 48 | } 49 | 50 | // Setup adds a controller that reconciles NopResource managed resources. 51 | func Setup(mgr ctrl.Manager, o controller.Options) error { 52 | name := managed.ControllerName(v1alpha1.ClusterNopResourceGroupKind) 53 | 54 | r := managed.NewReconciler(mgr, 55 | resource.ManagedKind(v1alpha1.ClusterNopResourceGroupVersionKind), 56 | managed.WithPollInterval(o.PollInterval), 57 | managed.WithExternalConnector(&connector{}), 58 | managed.WithLogger(o.Logger.WithValues("controller", name)), 59 | managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), 60 | managed.WithMetricRecorder(o.MetricOptions.MRMetrics), 61 | ) 62 | 63 | if err := ctrl.NewWebhookManagedBy(mgr). 64 | For(&v1alpha1.ClusterNopResource{}). 65 | WithValidator(v1alpha1.ClusterNopResourceValidator). 66 | Complete(); err != nil { 67 | return errors.Wrap(err, "cannot set up webhooks") 68 | } 69 | 70 | if err := mgr.Add(statemetrics.NewMRStateRecorder( 71 | mgr.GetClient(), o.Logger, o.MetricOptions.MRStateMetrics, &v1alpha1.ClusterNopResourceList{}, o.MetricOptions.PollStateMetricInterval)); err != nil { 72 | return err 73 | } 74 | return ctrl.NewControllerManagedBy(mgr). 75 | Named(name). 76 | WithOptions(o.ForControllerRuntime()). 77 | WithEventFilter(resource.DesiredStateChanged()). 78 | For(&v1alpha1.ClusterNopResource{}). 79 | Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) 80 | } 81 | 82 | type connector struct{} 83 | 84 | func (c *connector) Connect(_ context.Context, _ resource.Managed) (managed.ExternalClient, error) { 85 | return managed.ExternalClientFns{ 86 | ObserveFn: Observe, 87 | DisconnectFn: func(_ context.Context) error { 88 | return nil 89 | }, 90 | }, nil 91 | } 92 | 93 | // Observe doesn't actually observe an external resource. Instead, it sets the 94 | // most recent conditions that should occur per spec.forProvider.conditionAfter. 95 | func Observe(_ context.Context, mg resource.Managed) (managed.ExternalObservation, error) { 96 | // If our managed resource has been deleted we need to report that 97 | // our pretend external resource is gone in order for the delete 98 | // process to complete. This means we'll never call the DeleteFn. 99 | if meta.WasDeleted(mg) { 100 | return managed.ExternalObservation{ResourceExists: false}, nil 101 | } 102 | 103 | nop, ok := mg.(*v1alpha1.ClusterNopResource) 104 | if !ok { 105 | return managed.ExternalObservation{}, errors.Errorf("managed resource was not a %T", &v1alpha1.ClusterNopResource{}) 106 | } 107 | status := conditions.ObservedGenerationPropagationManager{}.For(nop) 108 | age := time.Since(nop.CreationTimestamp.Time) 109 | 110 | return nopprovider.Observe(nop.Spec.ForProvider, age, status) 111 | } 112 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crossplane-contrib/provider-nop 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.5 6 | 7 | tool sigs.k8s.io/controller-tools/cmd/controller-gen 8 | 9 | tool github.com/crossplane/crossplane-tools/cmd/angryjet 10 | 11 | require ( 12 | github.com/crossplane/crossplane-runtime/v2 v2.0.0-20250730220209-c306b1c8b181 13 | github.com/google/go-cmp v0.7.0 14 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 15 | k8s.io/api v0.33.0 16 | k8s.io/apiextensions-apiserver v0.33.0 17 | k8s.io/apimachinery v0.33.0 18 | k8s.io/client-go v0.33.0 19 | sigs.k8s.io/controller-runtime v0.19.0 20 | ) 21 | 22 | require ( 23 | dario.cat/mergo v1.0.1 // indirect 24 | github.com/alecthomas/kingpin/v2 v2.4.0 // indirect 25 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 26 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/blang/semver/v4 v4.0.0 // indirect 29 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 30 | github.com/crossplane/crossplane-tools v0.0.0-20250731192036-00d407d8b7ec // indirect 31 | github.com/dave/jennifer v1.7.1 // indirect 32 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 33 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 34 | github.com/evanphx/json-patch v5.9.11+incompatible // indirect 35 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 36 | github.com/fatih/color v1.18.0 // indirect 37 | github.com/fsnotify/fsnotify v1.7.0 // indirect 38 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 39 | github.com/go-logr/logr v1.4.2 // indirect 40 | github.com/go-logr/zapr v1.3.0 // indirect 41 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 42 | github.com/go-openapi/jsonreference v0.20.2 // indirect 43 | github.com/go-openapi/swag v0.23.0 // indirect 44 | github.com/gobuffalo/flect v1.0.3 // indirect 45 | github.com/gogo/protobuf v1.3.2 // indirect 46 | github.com/google/gnostic-models v0.6.9 // indirect 47 | github.com/google/uuid v1.6.0 // indirect 48 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 49 | github.com/josharian/intern v1.0.0 // indirect 50 | github.com/json-iterator/go v1.1.12 // indirect 51 | github.com/mailru/easyjson v0.7.7 // indirect 52 | github.com/mattn/go-colorable v0.1.13 // indirect 53 | github.com/mattn/go-isatty v0.0.20 // indirect 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 55 | github.com/modern-go/reflect2 v1.0.2 // indirect 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 57 | github.com/pkg/errors v0.9.1 // indirect 58 | github.com/prometheus/client_golang v1.22.0 // indirect 59 | github.com/prometheus/client_model v0.6.1 // indirect 60 | github.com/prometheus/common v0.62.0 // indirect 61 | github.com/prometheus/procfs v0.15.1 // indirect 62 | github.com/spf13/afero v1.11.0 // indirect 63 | github.com/spf13/cobra v1.9.1 // indirect 64 | github.com/spf13/pflag v1.0.6 // indirect 65 | github.com/x448/float16 v0.8.4 // indirect 66 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 67 | go.opentelemetry.io/otel v1.33.0 // indirect 68 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 69 | go.uber.org/multierr v1.11.0 // indirect 70 | go.uber.org/zap v1.27.0 // indirect 71 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 72 | golang.org/x/mod v0.24.0 // indirect 73 | golang.org/x/net v0.39.0 // indirect 74 | golang.org/x/oauth2 v0.27.0 // indirect 75 | golang.org/x/sync v0.13.0 // indirect 76 | golang.org/x/sys v0.32.0 // indirect 77 | golang.org/x/term v0.31.0 // indirect 78 | golang.org/x/text v0.24.0 // indirect 79 | golang.org/x/time v0.9.0 // indirect 80 | golang.org/x/tools v0.32.0 // indirect 81 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 82 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 83 | google.golang.org/grpc v1.68.1 // indirect 84 | google.golang.org/protobuf v1.36.5 // indirect 85 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 86 | gopkg.in/inf.v0 v0.9.1 // indirect 87 | gopkg.in/yaml.v2 v2.4.0 // indirect 88 | gopkg.in/yaml.v3 v3.0.1 // indirect 89 | k8s.io/code-generator v0.33.0 // indirect 90 | k8s.io/component-base v0.33.0 // indirect 91 | k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 // indirect 92 | k8s.io/klog/v2 v2.130.1 // indirect 93 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 94 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 95 | sigs.k8s.io/controller-tools v0.18.0 // indirect 96 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 97 | sigs.k8s.io/randfill v1.0.0 // indirect 98 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 99 | sigs.k8s.io/yaml v1.4.0 // indirect 100 | ) 101 | -------------------------------------------------------------------------------- /design/one-pager-provider-nop.md: -------------------------------------------------------------------------------- 1 | # Crossplane provider-nop 2 | 3 | * Owner: Rahul Grover (@rahulgrover99) 4 | * Reviewers: Crossplane Maintainers 5 | * Status: Draft 6 | 7 | ## Introduction 8 | 9 | There is a lot of machinery around the composition engine that needs to be 10 | tested which raises demand for a provider on which could rely to behave the way 11 | we want it to. It can be used to mock the behavior of providers by creating fake 12 | objects whenever needed so that we could test the response of composition to the 13 | resource becoming ready or not. 14 | 15 | Following is a very simple possible test scenario we might use provider-nop and 16 | its objects for: 17 | - test creates an XRD and a composition of 3 nop objects 18 | - each of those nop objects is told to become ready after 3 seconds 19 | - test creates an XR instance of this XRD 20 | - test waits and verifies that 3 nops objects are created, they all make it to 21 | the ready status, and the XR itself then makes it to the ready status because 22 | all 3 of its composed resources are now ready. 23 | 24 | This provider does exactly what it is told to. It can be used for testing 25 | functionality in core Crossplane without needing to account for the requirements 26 | of external APIs. It supports a single managed resource that contains a spec 27 | that allows for specifying how and when the status of the resource is changed. 28 | It does not orchestrate any external system. 29 | 30 | This provider will be installed while running Crossplane end-to-end tests to 31 | satisfy any kind of composite infrastructure resource to simulate different 32 | scenarios. 33 | 34 | ## Design 35 | 36 | The provider will have a single managed resource `NopResource` that will reflect 37 | whatever is in the spec into the status. It will allow to define the resource 38 | to: 39 | - be ready after this period 40 | - be unhealthy after this period 41 | 42 | The idea is to have an array of fields which will let you declare condition type 43 | and status of the resource at each time interval. The `NopResource` will wait 44 | for the time provided in the spec before allowing the resource to be 45 | ready/unhealthy (or as specified in the Spec). This could be achieved by adding 46 | the logic to the controllers. 47 | 48 | This will be implemented by making use of three fields: 49 | - `conditionType` and `conditionStatus` for declaring condtion of resource 50 | - `timeAfter` for declaring the time elapsed after the creation of resource at 51 | which we need to set the specified condition. 52 | 53 | `ObservableField` will be an arbitrary field in Status that can be useful in 54 | crossplane testing scenarios. For example, while testing bidirectional patching 55 | back to the composite resource from one resource to another which can be 56 | supported by this field. 57 | 58 | The resource structure might look something like: 59 | ```go 60 | type ResourceConditionAfter struct { 61 | Time string `json:"time"` 62 | ConditionType string `json:"conditionType"` 63 | ConditionStatus string `json:"conditionStatus"` 64 | } 65 | 66 | type NopResourceParameters struct { 67 | ConditionAfter []ResourceConditionAfter `json:"conditionAfter"` 68 | } 69 | ``` 70 | 71 | ### `NopResource` Controller 72 | 73 | The controller for this nop type will not call any external APIs during its 74 | reconcile loop and just perform the behaviour as specified by the user in the 75 | spec. 76 | 77 | #### Setup 78 | Since default is too long for the testing use case, the sync period 79 | should be decreased to a small value like `1s` or less using 80 | `WithPollInterval`in the Setup function. This will allow controller to set 81 | various conditions. 82 | 83 | #### Reconciler 84 | We define a new custom reconciler for managing the NopResource. 85 | This is where the main logic will reside that would compare the time elapsed 86 | with the time intervals passed in the Spec. The logic will find the latest 87 | condition status specified for each condition type till the elapsed time. It 88 | will then set the specified condition status for each type for the resource 89 | at each reconcile. 90 | 91 | The config might look something like: 92 | 93 | ```yaml 94 | apiVersion: nop.crossplane.io/v1alpha1 95 | kind: NopResource 96 | metadata: 97 | name: example 98 | spec: 99 | forProvider: 100 | conditionAfter: 101 | - conditionType: "Ready" 102 | conditionStatus: "False" 103 | time: "5s" 104 | - conditionType: "Ready" 105 | conditionStatus: "True" 106 | time: "10s" 107 | - conditionType: "Synced" 108 | conditionStatus: "False" 109 | time: "15s" 110 | - conditionType: "Synced" 111 | conditionStatus: "True" 112 | time: "20s" 113 | ``` 114 | 115 | ## Future Plans 116 | - Array type `ObservableArrays []string` field can be added in both Spec and 117 | Status because there are some edge cases regarding how arrays are patched with 118 | Composition. For example, when we patch an array, there are many ways possible: 119 | - To replace the whole array 120 | - To add the array 121 | - To specify key fields and only replace them 122 | 123 | It might be helpful to have these fields in Spec and Status in future 124 | implementations. 125 | 126 | - A `patchReceiverField` can added as an optional field in Spec would be a 127 | receiver field to test that patches make it into the nop object when it's part 128 | of a composition. For example, following is a possible test scenario: 129 | 1. nop object spec has a field called `patchReceiverField` of type string. 130 | 2. XRD has a field called `coolField` of type string. 131 | 3. The composition under the XRD creates 1 nop object and patches the value 132 | from `coolField` onto this receiver field on the nop object. 133 | 4. Test case verifies that the value of `coolField` made it successfully into 134 | the nop object's receiver field. 135 | -------------------------------------------------------------------------------- /cluster/local/integration_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # setting up colors 5 | BLU='\033[0;34m' 6 | YLW='\033[0;33m' 7 | GRN='\033[0;32m' 8 | RED='\033[0;31m' 9 | NOC='\033[0m' # No Color 10 | echo_info(){ 11 | printf "\n${BLU}%s${NOC}" "$1" 12 | } 13 | echo_step(){ 14 | printf "\n${BLU}>>>>>>> %s${NOC}\n" "$1" 15 | } 16 | echo_sub_step(){ 17 | printf "\n${BLU}>>> %s${NOC}\n" "$1" 18 | } 19 | 20 | echo_step_completed(){ 21 | printf "${GRN} [✔]${NOC}" 22 | } 23 | 24 | echo_success(){ 25 | printf "\n${GRN}%s${NOC}\n" "$1" 26 | } 27 | echo_warn(){ 28 | printf "\n${YLW}%s${NOC}" "$1" 29 | } 30 | echo_error(){ 31 | printf "\n${RED}%s${NOC}" "$1" 32 | exit 1 33 | } 34 | 35 | # ------------------------------ 36 | projectdir="$( cd "$( dirname "${BASH_SOURCE[0]}")"/../.. && pwd )" 37 | 38 | # get the build environment variables from the special build.vars target in the main makefile 39 | eval $(make --no-print-directory -C ${projectdir} build.vars) 40 | 41 | # ------------------------------ 42 | 43 | SAFEHOSTARCH="${SAFEHOSTARCH:-amd64}" 44 | CONTROLLER_IMAGE="${BUILD_REGISTRY}/${PROJECT_NAME}-${SAFEHOSTARCH}" 45 | 46 | version_tag="$(cat ${projectdir}/_output/version)" 47 | # tag as latest version to load into kind cluster 48 | K8S_CLUSTER="${K8S_CLUSTER:-${BUILD_REGISTRY}-inttests}" 49 | 50 | CROSSPLANE_NAMESPACE="crossplane-system" 51 | PACKAGE_NAME="provider-nop" 52 | 53 | # cleanup on exit 54 | if [ "$skipcleanup" != true ]; then 55 | function cleanup { 56 | echo_step "Cleaning up..." 57 | export KUBECONFIG= 58 | "${KIND}" delete cluster --name="${K8S_CLUSTER}" 59 | } 60 | 61 | trap cleanup EXIT 62 | fi 63 | 64 | # setup package cache 65 | echo_step "setting up local package cache" 66 | CACHE_PATH="${projectdir}/.work/inttest-package-cache" 67 | mkdir -p "${CACHE_PATH}" 68 | echo "created cache dir at ${CACHE_PATH}" 69 | "${UP}" alpha xpkg xp-extract --from-xpkg "${OUTPUT_DIR}"/xpkg/"${HOSTOS}"_"${SAFEHOSTARCH}"/"${PACKAGE_NAME}"-"${VERSION}".xpkg -o "${CACHE_PATH}/${PACKAGE_NAME}.gz" && chmod 644 "${CACHE_PATH}/${PACKAGE_NAME}.gz" 70 | 71 | # create kind cluster with extra mounts 72 | KIND_NODE_IMAGE="kindest/node:${KIND_NODE_IMAGE_TAG}" 73 | echo_step "creating k8s cluster using kind ${KIND_VERSION} and node image ${KIND_NODE_IMAGE}" 74 | KIND_CONFIG="$( cat < $current ]]; then 180 | echo_error "timeout of ${timeout}s has been reached" 181 | fi 182 | sleep $step; 183 | done 184 | 185 | echo_success "Integration tests succeeded!" 186 | -------------------------------------------------------------------------------- /cmd/provider/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package main implements a Crossplane provider that does nothing. 18 | package main 19 | 20 | import ( 21 | "os" 22 | "path/filepath" 23 | "time" 24 | 25 | "gopkg.in/alecthomas/kingpin.v2" 26 | extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 27 | "k8s.io/apimachinery/pkg/runtime/schema" 28 | "k8s.io/client-go/tools/leaderelection/resourcelock" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/cache" 31 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 32 | "sigs.k8s.io/controller-runtime/pkg/metrics" 33 | "sigs.k8s.io/controller-runtime/pkg/webhook" 34 | 35 | "github.com/crossplane/crossplane-runtime/v2/pkg/controller" 36 | "github.com/crossplane/crossplane-runtime/v2/pkg/feature" 37 | "github.com/crossplane/crossplane-runtime/v2/pkg/gate" 38 | "github.com/crossplane/crossplane-runtime/v2/pkg/logging" 39 | "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" 40 | "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/customresourcesgate" 41 | "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" 42 | "github.com/crossplane/crossplane-runtime/v2/pkg/statemetrics" 43 | 44 | "github.com/crossplane-contrib/provider-nop/apis" 45 | nop "github.com/crossplane-contrib/provider-nop/internal/controller" 46 | ) 47 | 48 | const ( 49 | webhookTLSCertDirEnvVar = "WEBHOOK_TLS_CERT_DIR" 50 | tlsServerCertDirEnvVar = "TLS_SERVER_CERTS_DIR" 51 | tlsServerCertDir = "/tls/server" 52 | ) 53 | 54 | func main() { 55 | var ( 56 | app = kingpin.New(filepath.Base(os.Args[0]), "Doing-nothing support for Crossplane. :)").DefaultEnvars() 57 | debug = app.Flag("debug", "Run with debug logging.").Short('d').Bool() 58 | syncInterval = app.Flag("sync", "Sync interval controls how often all resources will be double checked for drift.").Short('s').Default("1h").Duration() 59 | pollInterval = app.Flag("poll", "Poll interval controls how often an individual resource should be checked for drift.").Default("10s").Duration() 60 | pollStateMetricInterval = app.Flag("poll-state-metric", "State metric recording interval").Default("5s").Duration() 61 | leaderElection = app.Flag("leader-election", "Use leader election for the controller manager.").Short('l').Default("false").Envar("LEADER_ELECTION").Bool() 62 | maxReconcileRate = app.Flag("max-reconcile-rate", "The maximum number of concurrent reconciliation operations.").Default("1").Int() 63 | ) 64 | kingpin.MustParse(app.Parse(os.Args[1:])) 65 | 66 | zl := zap.New(zap.UseDevMode(*debug)) 67 | ctrl.SetLogger(zl) 68 | log := logging.NewLogrLogger(zl.WithName("provider-nop")) 69 | 70 | log.Debug("Starting", "sync-period", syncInterval.String()) 71 | 72 | cfg, err := ctrl.GetConfig() 73 | kingpin.FatalIfError(err, "Cannot get API server rest config") 74 | 75 | // Get the TLS certs directory from the environment variable if set 76 | // In older XP versions we used WEBHOOK_TLS_CERT_DIR, in newer versions 77 | // we use TLS_SERVER_CERTS_DIR. If neither are set, use the default. 78 | var certDir string 79 | certDir = os.Getenv(webhookTLSCertDirEnvVar) 80 | if certDir == "" { 81 | certDir = os.Getenv(tlsServerCertDirEnvVar) 82 | if certDir == "" { 83 | certDir = tlsServerCertDir 84 | } 85 | } 86 | 87 | mgr, err := ctrl.NewManager(cfg, ctrl.Options{ 88 | Cache: cache.Options{ 89 | SyncPeriod: syncInterval, 90 | }, 91 | 92 | // controller-runtime uses both ConfigMaps and Leases for leader 93 | // election by default. Leases expire after 15 seconds, with a 94 | // 10 second renewal deadline. We've observed leader loss due to 95 | // renewal deadlines being exceeded when under high load - i.e. 96 | // hundreds of reconciles per second and ~200rps to the API 97 | // server. Switching to Leases only and longer leases appears to 98 | // alleviate this. 99 | LeaderElection: *leaderElection, 100 | LeaderElectionID: "crossplane-leader-election-provider-nop", 101 | LeaderElectionResourceLock: resourcelock.LeasesResourceLock, 102 | LeaseDuration: func() *time.Duration { d := 60 * time.Second; return &d }(), 103 | RenewDeadline: func() *time.Duration { d := 50 * time.Second; return &d }(), 104 | WebhookServer: webhook.NewServer( 105 | webhook.Options{ 106 | CertDir: certDir, 107 | }), 108 | }) 109 | kingpin.FatalIfError(err, "Cannot create controller manager") 110 | 111 | mm := managed.NewMRMetricRecorder() 112 | sm := statemetrics.NewMRStateMetrics() 113 | 114 | metrics.Registry.MustRegister(mm) 115 | mo := controller.MetricOptions{ 116 | PollStateMetricInterval: *pollStateMetricInterval, 117 | MRMetrics: mm, 118 | MRStateMetrics: sm, 119 | } 120 | 121 | o := controller.Options{ 122 | Logger: log, 123 | MaxConcurrentReconciles: *maxReconcileRate, 124 | PollInterval: *pollInterval, 125 | GlobalRateLimiter: ratelimiter.NewGlobal(*maxReconcileRate), 126 | Features: &feature.Flags{}, 127 | MetricOptions: &mo, 128 | Gate: new(gate.Gate[schema.GroupVersionKind]), 129 | } 130 | 131 | kingpin.FatalIfError(apis.AddToScheme(mgr.GetScheme()), "Cannot add Nop APIs to scheme") 132 | kingpin.FatalIfError(extv1.AddToScheme(mgr.GetScheme()), "Cannot add Core API Extensions to scheme") 133 | kingpin.FatalIfError(nop.Setup(mgr, o), "Cannot setup Nop controllers") 134 | kingpin.FatalIfError(customresourcesgate.Setup(mgr, o), "Cannot setup CustomResourceGate controller") 135 | kingpin.FatalIfError(mgr.Start(ctrl.SetupSignalHandler()), "Cannot start controller manager") 136 | } 137 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # ==================================================================================== 2 | # Setup Project 3 | 4 | PROJECT_NAME := provider-nop 5 | PROJECT_REPO := github.com/crossplane-contrib/$(PROJECT_NAME) 6 | 7 | PLATFORMS ?= linux_amd64 linux_arm64 8 | # -include will silently skip missing files, which allows us 9 | # to load those files with a target in the Makefile. If only 10 | # "include" was used, the make command would fail and refuse 11 | # to run a target until the include commands succeeded. 12 | -include build/makelib/common.mk 13 | 14 | # ==================================================================================== 15 | # Setup Output 16 | 17 | -include build/makelib/output.mk 18 | 19 | # ==================================================================================== 20 | # Setup Go 21 | 22 | # Set a sane default so that the nprocs calculation below is less noisy on the initial 23 | # loading of this file 24 | NPROCS ?= 1 25 | 26 | # each of our test suites starts a kube-apiserver and running many test suites in 27 | # parallel can lead to high CPU utilization. by default we reduce the parallelism 28 | # to half the number of CPU cores. 29 | GO_TEST_PARALLEL := $(shell echo $$(( $(NPROCS) / 2 ))) 30 | 31 | # If you change this, make sure to update .github/workflows/ci.yml as well, since it 32 | # uses its own linter config. 33 | GOLANGCILINT_VERSION ?= 2.3.0 34 | GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/provider 35 | GO_SUBDIRS += cmd internal apis 36 | GO111MODULE = on 37 | -include build/makelib/golang.mk 38 | 39 | # ==================================================================================== 40 | # Setup Kubernetes tools 41 | 42 | KIND_VERSION = v0.22.0 43 | CROSSPLANE_CLI_VERSION = v2.0.0 44 | -include build/makelib/k8s_tools.mk 45 | 46 | # ==================================================================================== 47 | # Setup Images 48 | 49 | IMAGES = provider-nop 50 | -include build/makelib/imagelight.mk 51 | 52 | # ==================================================================================== 53 | # Setup XPKG 54 | 55 | XPKG_REG_ORGS ?= ghcr.io/crossplane-contrib xpkg.upbound.io/crossplane-contrib 56 | # NOTE(hasheddan): skip promoting on xpkg.upbound.io as channel tags are 57 | # inferred. 58 | XPKG_REG_ORGS_NO_PROMOTE ?= xpkg.upbound.io/crossplane-contrib 59 | XPKGS = provider-nop 60 | -include build/makelib/xpkg.mk 61 | 62 | # We force image building to happen prior to xpkg build so that we ensure image 63 | # is present in daemon. 64 | xpkg.build.provider-nop: do.build.images 65 | 66 | # ==================================================================================== 67 | # Targets 68 | 69 | # run `make help` to see the targets and options 70 | 71 | # We want submodules to be set up the first time `make` is run. 72 | # We manage the build/ folder and its Makefiles as a submodule. 73 | # The first time `make` is run, the includes of build/*.mk files will 74 | # all fail, and this target will be run. The next time, the default as defined 75 | # by the includes will be run instead. 76 | fallthrough: submodules 77 | @echo Initial setup complete. Running make again . . . 78 | @make 79 | 80 | # Generate a coverage report for cobertura applying exclusions on 81 | # - generated file 82 | cobertura: 83 | @cat $(GO_TEST_OUTPUT)/coverage.txt | \ 84 | grep -v zz_generated.deepcopy | \ 85 | $(GOCOVER_COBERTURA) > $(GO_TEST_OUTPUT)/cobertura-coverage.xml 86 | 87 | crds.clean: 88 | @$(INFO) cleaning generated CRDs 89 | @find package/crds -name *.yaml -exec sed -i.sed -e '1,2d' {} \; || $(FAIL) 90 | @find package/crds -name *.yaml.sed -delete || $(FAIL) 91 | @$(OK) cleaned generated CRDs 92 | 93 | generate: crds.clean 94 | 95 | # integration tests 96 | e2e.run: test-integration 97 | 98 | # Run integration tests. 99 | test-integration: $(KIND) $(KUBECTL) $(HELM3) 100 | @$(INFO) running integration tests using kind $(KIND_VERSION) 101 | @KIND_NODE_IMAGE_TAG=${KIND_NODE_IMAGE_TAG} $(ROOT_DIR)/cluster/local/integration_tests.sh || $(FAIL) 102 | @$(OK) integration tests passed 103 | 104 | # Update the submodules, such as the common build scripts. 105 | submodules: 106 | @git submodule sync 107 | @git submodule update --init --recursive 108 | 109 | # We must ensure up is installed in tool cache prior to build as including the 110 | # k8s_tools machinery prior to the xpkg machinery sets crossplane CLI to point 111 | # to tool cache. 112 | build.init: $(CROSSPLANE_CLI) 113 | 114 | # NOTE(hasheddan): the build submodule currently overrides XDG_CACHE_HOME in 115 | # order to force the Helm 3 to use the .work/helm directory. This causes Go on 116 | # Linux machines to use that directory as the build cache as well. We should 117 | # adjust this behavior in the build submodule because it is also causing Linux 118 | # users to duplicate their build cache, but for now we just make it easier to 119 | # identify its location in CI so that we cache between builds. 120 | go.cachedir: 121 | @go env GOCACHE 122 | 123 | # This is for running out-of-cluster locally, and is for convenience. Running 124 | # this make target will print out the command which was used. For more control, 125 | # try running the binary directly with different arguments. 126 | run: go.build 127 | @$(INFO) Running Crossplane locally out-of-cluster . . . 128 | @# To see other arguments that can be provided, run the command with --help instead 129 | $(GO_OUT_DIR)/provider --debug 130 | 131 | dev: generate 132 | kubectl apply -f package/crds/ -R 133 | go run cmd/provider/main.go -d 134 | 135 | .PHONY: cobertura reviewable submodules fallthrough test-integration run crds.clean 136 | 137 | # ==================================================================================== 138 | # Special Targets 139 | 140 | define CROSSPLANE_MAKE_HELP 141 | Crossplane Targets: 142 | cobertura Generate a coverage report for cobertura applying exclusions on generated files. 143 | reviewable Ensure a PR is ready for review. 144 | submodules Update the submodules, such as the common build scripts. 145 | run Run crossplane locally, out-of-cluster. Useful for development. 146 | 147 | endef 148 | # The reason CROSSPLANE_MAKE_HELP is used instead of CROSSPLANE_HELP is because the crossplane 149 | # binary will try to use CROSSPLANE_HELP if it is set, and this is for something different. 150 | export CROSSPLANE_MAKE_HELP 151 | 152 | crossplane.help: 153 | @echo "$$CROSSPLANE_MAKE_HELP" 154 | 155 | help-special: crossplane.help 156 | 157 | .PHONY: crossplane.help help-special 158 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release-* 8 | pull_request: {} 9 | workflow_dispatch: {} 10 | 11 | env: 12 | # Common versions 13 | GO_VERSION: '1.24' 14 | GOLANGCI_VERSION: 'v2.3.0' 15 | DOCKER_BUILDX_VERSION: 'v0.9.1' 16 | 17 | # Common users. We can't run a step 'if secrets.XXX != ""' but we can run a 18 | # step 'if env.XXX' != ""', so we copy these to succinctly test whether 19 | # credentials have been provided before trying to run steps that need them. 20 | UPBOUND_MARKETPLACE_PUSH_ROBOT_USR: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }} 21 | jobs: 22 | detect-noop: 23 | runs-on: ubuntu-22.04 24 | outputs: 25 | noop: ${{ steps.noop.outputs.should_skip }} 26 | steps: 27 | - name: Detect No-op Changes 28 | id: noop 29 | uses: fkirc/skip-duplicate-actions@v5.2.0 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | paths_ignore: '["**.md", "**.png", "**.jpg"]' 33 | do_not_skip: '["workflow_dispatch", "schedule", "push"]' 34 | 35 | 36 | lint: 37 | runs-on: ubuntu-22.04 38 | needs: detect-noop 39 | if: needs.detect-noop.outputs.noop != 'true' 40 | 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v3 44 | with: 45 | submodules: true 46 | 47 | - name: Setup Go 48 | uses: actions/setup-go@v3 49 | with: 50 | go-version: ${{ env.GO_VERSION }} 51 | 52 | - name: Find the Go Build Cache 53 | id: go 54 | run: echo "cachedir=$(make go.cachedir)" >> $GITHUB_ENV 55 | 56 | - name: Cache the Go Build Cache 57 | uses: actions/cache@v3 58 | with: 59 | path: ${{ env.cachedir }} 60 | key: ${{ runner.os }}-build-lint-${{ hashFiles('**/go.sum') }} 61 | restore-keys: ${{ runner.os }}-build-lint- 62 | 63 | - name: Cache Go Dependencies 64 | uses: actions/cache@v3 65 | with: 66 | path: .work/pkg 67 | key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} 68 | restore-keys: ${{ runner.os }}-pkg- 69 | 70 | - name: Download Go Modules 71 | run: make modules.download modules.check 72 | 73 | # We could run 'make lint' but we prefer this action because it leaves 74 | # 'annotations' (i.e. it comments on PRs to point out linter violations). 75 | - name: Lint 76 | uses: golangci/golangci-lint-action@v8 77 | with: 78 | version: ${{ env.GOLANGCI_VERSION }} 79 | 80 | check-diff: 81 | runs-on: ubuntu-22.04 82 | needs: detect-noop 83 | if: needs.detect-noop.outputs.noop != 'true' 84 | 85 | steps: 86 | - name: Checkout 87 | uses: actions/checkout@v3 88 | with: 89 | submodules: true 90 | 91 | - name: Setup Go 92 | uses: actions/setup-go@v3 93 | with: 94 | go-version: ${{ env.GO_VERSION }} 95 | 96 | - name: Find the Go Build Cache 97 | id: go 98 | run: echo "cachedir=$(make go.cachedir)" >> $GITHUB_ENV 99 | 100 | - name: Cache the Go Build Cache 101 | uses: actions/cache@v3 102 | with: 103 | path: ${{ env.cachedir }} 104 | key: ${{ runner.os }}-build-check-diff-${{ hashFiles('**/go.sum') }} 105 | restore-keys: ${{ runner.os }}-build-check-diff- 106 | 107 | - name: Cache Go Dependencies 108 | uses: actions/cache@v3 109 | with: 110 | path: .work/pkg 111 | key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} 112 | restore-keys: ${{ runner.os }}-pkg- 113 | 114 | - name: Download Go Modules 115 | run: make modules.download modules.check 116 | 117 | - name: Check Diff 118 | id: check-diff 119 | run: | 120 | mkdir _output 121 | make check-diff 122 | 123 | - name: Show diff 124 | if: failure() && steps.check-diff.outcome == 'failure' 125 | run: git diff 126 | 127 | unit-tests: 128 | runs-on: ubuntu-22.04 129 | needs: detect-noop 130 | if: needs.detect-noop.outputs.noop != 'true' 131 | 132 | steps: 133 | - name: Checkout 134 | uses: actions/checkout@v3 135 | with: 136 | submodules: true 137 | 138 | - name: Fetch History 139 | run: git fetch --prune --unshallow 140 | 141 | - name: Setup Go 142 | uses: actions/setup-go@v3 143 | with: 144 | go-version: ${{ env.GO_VERSION }} 145 | 146 | - name: Find the Go Build Cache 147 | id: go 148 | run: echo "cachedir=$(make go.cachedir)" >> $GITHUB_ENV 149 | 150 | - name: Cache the Go Build Cache 151 | uses: actions/cache@v3 152 | with: 153 | path: ${{ env.cachedir }} 154 | key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }} 155 | restore-keys: ${{ runner.os }}-build-unit-tests- 156 | 157 | - name: Cache Go Dependencies 158 | uses: actions/cache@v3 159 | with: 160 | path: .work/pkg 161 | key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} 162 | restore-keys: ${{ runner.os }}-pkg- 163 | 164 | - name: Download Go Modules 165 | run: make modules.download modules.check 166 | 167 | - name: Run Unit Tests 168 | run: make -j2 test 169 | 170 | - name: Publish Unit Test Coverage 171 | uses: codecov/codecov-action@v3 172 | with: 173 | flags: unittests 174 | file: _output/tests/linux_amd64/coverage.txt 175 | 176 | publish-artifacts: 177 | runs-on: ubuntu-22.04 178 | needs: detect-noop 179 | if: needs.detect-noop.outputs.noop != 'true' 180 | 181 | steps: 182 | - name: Setup QEMU 183 | uses: docker/setup-qemu-action@v2 184 | with: 185 | platforms: all 186 | 187 | - name: Setup Docker Buildx 188 | uses: docker/setup-buildx-action@v2 189 | with: 190 | version: ${{ env.DOCKER_BUILDX_VERSION }} 191 | install: true 192 | 193 | - name: Login to ghcr.io 194 | uses: docker/login-action@v3 195 | with: 196 | registry: ghcr.io 197 | username: ${{ github.repository_owner }} 198 | password: ${{ secrets.GITHUB_TOKEN }} 199 | 200 | - name: Login to Upbound 201 | uses: docker/login-action@v3 202 | if: env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != '' 203 | with: 204 | registry: xpkg.upbound.io 205 | username: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }} 206 | password: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_PSW }} 207 | 208 | - name: Checkout 209 | uses: actions/checkout@v3 210 | with: 211 | submodules: true 212 | 213 | - name: Fetch History 214 | run: git fetch --prune --unshallow 215 | 216 | - name: Setup Go 217 | uses: actions/setup-go@v3 218 | with: 219 | go-version: ${{ env.GO_VERSION }} 220 | 221 | - name: Find the Go Build Cache 222 | id: go 223 | run: echo "cachedir=$(make go.cachedir)" >> $GITHUB_ENV 224 | 225 | - name: Cache the Go Build Cache 226 | uses: actions/cache@v3 227 | with: 228 | path: ${{ env.cachedir }} 229 | key: ${{ runner.os }}-build-publish-artifacts-${{ hashFiles('**/go.sum') }} 230 | restore-keys: ${{ runner.os }}-build-publish-artifacts- 231 | 232 | - name: Cache Go Dependencies 233 | uses: actions/cache@v3 234 | with: 235 | path: .work/pkg 236 | key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} 237 | restore-keys: ${{ runner.os }}-pkg- 238 | 239 | - name: Download Go Modules 240 | run: make modules.download modules.check 241 | 242 | - name: Build Artifacts 243 | run: make -j2 build.all 244 | env: 245 | # We're using docker buildx, which doesn't actually load the images it 246 | # builds by default. Specifying --load does so. 247 | BUILD_ARGS: "--load" 248 | 249 | - name: Publish Artifacts to GitHub 250 | uses: actions/upload-artifact@v4 251 | with: 252 | name: output 253 | path: _output/** 254 | 255 | - name: Publish Artifacts 256 | if: env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != '' 257 | run: make publish BRANCH_NAME=${GITHUB_REF##*/} 258 | -------------------------------------------------------------------------------- /internal/controller/nopresource/nopresource_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package nopresource 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | "time" 23 | 24 | "github.com/google/go-cmp/cmp" 25 | corev1 "k8s.io/api/core/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | 28 | xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 29 | "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 30 | "github.com/crossplane/crossplane-runtime/v2/pkg/test" 31 | 32 | "github.com/crossplane-contrib/provider-nop/apis/v1alpha1" 33 | ) 34 | 35 | // Unlike many Kubernetes projects Crossplane does not use third party testing 36 | // libraries, per the common Go test review comments. Crossplane encourages the 37 | // use of table driven unit tests. The tests of the crossplane-runtime project 38 | // are representative of the testing style Crossplane encourages. 39 | // 40 | // https://github.com/golang/go/wiki/TestComments 41 | // https://github.com/crossplane/crossplane/blob/master/CONTRIBUTING.md#contributing-code 42 | 43 | func TestReconcileLogic(t *testing.T) { 44 | c := []v1alpha1.ConditionAfter{ 45 | {Time: metav1.Duration{Duration: 10 * time.Second}, ConditionType: xpv1.TypeReady, ConditionStatus: corev1.ConditionFalse}, 46 | {Time: metav1.Duration{Duration: 5 * time.Second}, ConditionType: xpv1.TypeReady, ConditionStatus: corev1.ConditionFalse}, 47 | {Time: metav1.Duration{Duration: 7 * time.Second}, ConditionType: xpv1.TypeReady, ConditionStatus: corev1.ConditionTrue}, 48 | {Time: metav1.Duration{Duration: 5 * time.Second}, ConditionType: xpv1.TypeSynced, ConditionStatus: corev1.ConditionFalse}, 49 | {Time: metav1.Duration{Duration: 10 * time.Second}, ConditionType: xpv1.TypeSynced, ConditionStatus: corev1.ConditionTrue}, 50 | {Time: metav1.Duration{Duration: 2 * time.Second}, ConditionType: xpv1.TypeReady, ConditionStatus: corev1.ConditionFalse}, 51 | } 52 | 53 | now := time.Now() 54 | 55 | cases := map[string]struct { 56 | reason string 57 | mg resource.Managed 58 | want resource.Managed 59 | }{ 60 | "NoDesiredConditionsYet": { 61 | reason: "No conditions should be set if not enough time has passed for any desired conditions to be applied.", 62 | mg: &v1alpha1.NopResource{ 63 | ObjectMeta: metav1.ObjectMeta{ 64 | CreationTimestamp: metav1.NewTime(now.Add(-1 * time.Second)), 65 | Generation: 42, 66 | }, 67 | Spec: v1alpha1.NopSpec{ 68 | ForProvider: v1alpha1.NopParameters{ 69 | ConditionAfter: c, 70 | }, 71 | }, 72 | }, 73 | want: &v1alpha1.NopResource{ 74 | ObjectMeta: metav1.ObjectMeta{ 75 | CreationTimestamp: metav1.NewTime(now.Add(-1 * time.Second)), 76 | Generation: 42, 77 | }, 78 | Spec: v1alpha1.NopSpec{ 79 | ForProvider: v1alpha1.NopParameters{ 80 | ConditionAfter: c, 81 | }, 82 | }, 83 | }, 84 | }, 85 | "ReadyForOneDesiredCondition": { 86 | reason: "Only one condition should be set if enough time has passed for only one desired condition.", 87 | mg: &v1alpha1.NopResource{ 88 | ObjectMeta: metav1.ObjectMeta{ 89 | // The earliest condition (5) should be set at two seconds. 90 | CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Second)), 91 | Generation: 42, 92 | }, 93 | Spec: v1alpha1.NopSpec{ 94 | ForProvider: v1alpha1.NopParameters{ 95 | ConditionAfter: c, 96 | }, 97 | }, 98 | }, 99 | want: &v1alpha1.NopResource{ 100 | ObjectMeta: metav1.ObjectMeta{ 101 | CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Second)), 102 | Generation: 42, 103 | }, 104 | Spec: v1alpha1.NopSpec{ 105 | ForProvider: v1alpha1.NopParameters{ 106 | ConditionAfter: c, 107 | }, 108 | }, 109 | Status: v1alpha1.NopStatus{ 110 | ResourceStatus: xpv1.ResourceStatus{ 111 | ConditionedStatus: xpv1.ConditionedStatus{ 112 | Conditions: []xpv1.Condition{ 113 | { 114 | Type: c[5].ConditionType, 115 | Status: c[5].ConditionStatus, 116 | LastTransitionTime: metav1.Now(), 117 | ObservedGeneration: 42, 118 | }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | "OnlyLatestConditionsAreSet": { 126 | reason: "When there are many conditions of the same time, only the latest eligible conditions should be set.", 127 | mg: &v1alpha1.NopResource{ 128 | ObjectMeta: metav1.ObjectMeta{ 129 | // After 8 seconds conditions 2 (Ready=True) and 3 130 | // (Synced=False) should be set. Condition 2 supercedes 131 | // conditions 1 and 5 (both Ready=False), which happen 132 | // earlier. 133 | CreationTimestamp: metav1.NewTime(now.Add(-8 * time.Second)), 134 | Generation: 42, 135 | }, 136 | Spec: v1alpha1.NopSpec{ 137 | ForProvider: v1alpha1.NopParameters{ 138 | ConditionAfter: c, 139 | }, 140 | }, 141 | }, 142 | want: &v1alpha1.NopResource{ 143 | ObjectMeta: metav1.ObjectMeta{ 144 | CreationTimestamp: metav1.NewTime(now.Add(-8 * time.Second)), 145 | Generation: 42, 146 | }, 147 | Spec: v1alpha1.NopSpec{ 148 | ForProvider: v1alpha1.NopParameters{ 149 | ConditionAfter: c, 150 | }, 151 | }, 152 | Status: v1alpha1.NopStatus{ 153 | ResourceStatus: xpv1.ResourceStatus{ 154 | ConditionedStatus: xpv1.ConditionedStatus{ 155 | Conditions: []xpv1.Condition{ 156 | { 157 | Type: c[2].ConditionType, 158 | Status: c[2].ConditionStatus, 159 | LastTransitionTime: metav1.Now(), 160 | ObservedGeneration: 42, 161 | }, 162 | { 163 | Type: c[3].ConditionType, 164 | Status: c[3].ConditionStatus, 165 | LastTransitionTime: metav1.Now(), 166 | ObservedGeneration: 42, 167 | }, 168 | }, 169 | }, 170 | }, 171 | }, 172 | }, 173 | }, 174 | "LongTimeReconcileBehaviour": { 175 | reason: "Indexes with last set status of each condition type should be returned till given time elapsed.", 176 | mg: &v1alpha1.NopResource{ 177 | ObjectMeta: metav1.ObjectMeta{ 178 | // After 8 seconds conditions 2 (Ready=True) and 3 179 | // (Synced=False) should be set. Condition 2 supercedes 180 | // conditions 1 and 5 (both Ready=False), which happen 181 | // earlier. 182 | CreationTimestamp: metav1.NewTime(now.Add(-50 * time.Second)), 183 | Generation: 42, 184 | }, 185 | Spec: v1alpha1.NopSpec{ 186 | ForProvider: v1alpha1.NopParameters{ 187 | ConditionAfter: c, 188 | }, 189 | }, 190 | }, 191 | want: &v1alpha1.NopResource{ 192 | ObjectMeta: metav1.ObjectMeta{ 193 | CreationTimestamp: metav1.NewTime(now.Add(-50 * time.Second)), 194 | Generation: 42, 195 | }, 196 | Spec: v1alpha1.NopSpec{ 197 | ForProvider: v1alpha1.NopParameters{ 198 | ConditionAfter: c, 199 | }, 200 | }, 201 | Status: v1alpha1.NopStatus{ 202 | ResourceStatus: xpv1.ResourceStatus{ 203 | ConditionedStatus: xpv1.ConditionedStatus{ 204 | Conditions: []xpv1.Condition{ 205 | { 206 | Type: c[0].ConditionType, 207 | Status: c[0].ConditionStatus, 208 | LastTransitionTime: metav1.Now(), 209 | ObservedGeneration: 42, 210 | }, 211 | { 212 | Type: c[4].ConditionType, 213 | Status: c[4].ConditionStatus, 214 | LastTransitionTime: metav1.Now(), 215 | ObservedGeneration: 42, 216 | }, 217 | }, 218 | }, 219 | }, 220 | }, 221 | }, 222 | }, 223 | } 224 | 225 | for name, tc := range cases { 226 | t.Run(name, func(t *testing.T) { 227 | _, _ = Observe(context.Background(), tc.mg) 228 | if diff := cmp.Diff(tc.want, tc.mg, test.EquateConditions()); diff != "" { 229 | t.Errorf("Observe(...): -want, +got:\n%s\n%s\n", tc.reason, diff) 230 | } 231 | }) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /internal/controller/clusternopresource/clusternopresource_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package clusternopresource 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | "time" 23 | 24 | "github.com/google/go-cmp/cmp" 25 | corev1 "k8s.io/api/core/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | 28 | xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 29 | "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 30 | "github.com/crossplane/crossplane-runtime/v2/pkg/test" 31 | 32 | "github.com/crossplane-contrib/provider-nop/apis/v1alpha1" 33 | ) 34 | 35 | // Unlike many Kubernetes projects Crossplane does not use third party testing 36 | // libraries, per the common Go test review comments. Crossplane encourages the 37 | // use of table driven unit tests. The tests of the crossplane-runtime project 38 | // are representative of the testing style Crossplane encourages. 39 | // 40 | // https://github.com/golang/go/wiki/TestComments 41 | // https://github.com/crossplane/crossplane/blob/master/CONTRIBUTING.md#contributing-code 42 | 43 | func TestReconcileLogic(t *testing.T) { 44 | c := []v1alpha1.ConditionAfter{ 45 | {Time: metav1.Duration{Duration: 10 * time.Second}, ConditionType: xpv1.TypeReady, ConditionStatus: corev1.ConditionFalse}, 46 | {Time: metav1.Duration{Duration: 5 * time.Second}, ConditionType: xpv1.TypeReady, ConditionStatus: corev1.ConditionFalse}, 47 | {Time: metav1.Duration{Duration: 7 * time.Second}, ConditionType: xpv1.TypeReady, ConditionStatus: corev1.ConditionTrue}, 48 | {Time: metav1.Duration{Duration: 5 * time.Second}, ConditionType: xpv1.TypeSynced, ConditionStatus: corev1.ConditionFalse}, 49 | {Time: metav1.Duration{Duration: 10 * time.Second}, ConditionType: xpv1.TypeSynced, ConditionStatus: corev1.ConditionTrue}, 50 | {Time: metav1.Duration{Duration: 2 * time.Second}, ConditionType: xpv1.TypeReady, ConditionStatus: corev1.ConditionFalse}, 51 | } 52 | 53 | now := time.Now() 54 | 55 | cases := map[string]struct { 56 | reason string 57 | mg resource.Managed 58 | want resource.Managed 59 | }{ 60 | "NoDesiredConditionsYet": { 61 | reason: "No conditions should be set if not enough time has passed for any desired conditions to be applied.", 62 | mg: &v1alpha1.ClusterNopResource{ 63 | ObjectMeta: metav1.ObjectMeta{ 64 | CreationTimestamp: metav1.NewTime(now.Add(-1 * time.Second)), 65 | Generation: 42, 66 | }, 67 | Spec: v1alpha1.NopSpec{ 68 | ForProvider: v1alpha1.NopParameters{ 69 | ConditionAfter: c, 70 | }, 71 | }, 72 | }, 73 | want: &v1alpha1.ClusterNopResource{ 74 | ObjectMeta: metav1.ObjectMeta{ 75 | CreationTimestamp: metav1.NewTime(now.Add(-1 * time.Second)), 76 | Generation: 42, 77 | }, 78 | Spec: v1alpha1.NopSpec{ 79 | ForProvider: v1alpha1.NopParameters{ 80 | ConditionAfter: c, 81 | }, 82 | }, 83 | }, 84 | }, 85 | "ReadyForOneDesiredCondition": { 86 | reason: "Only one condition should be set if enough time has passed for only one desired condition.", 87 | mg: &v1alpha1.ClusterNopResource{ 88 | ObjectMeta: metav1.ObjectMeta{ 89 | // The earliest condition (5) should be set at two seconds. 90 | CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Second)), 91 | Generation: 42, 92 | }, 93 | Spec: v1alpha1.NopSpec{ 94 | ForProvider: v1alpha1.NopParameters{ 95 | ConditionAfter: c, 96 | }, 97 | }, 98 | }, 99 | want: &v1alpha1.ClusterNopResource{ 100 | ObjectMeta: metav1.ObjectMeta{ 101 | CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Second)), 102 | Generation: 42, 103 | }, 104 | Spec: v1alpha1.NopSpec{ 105 | ForProvider: v1alpha1.NopParameters{ 106 | ConditionAfter: c, 107 | }, 108 | }, 109 | Status: v1alpha1.NopStatus{ 110 | ResourceStatus: xpv1.ResourceStatus{ 111 | ConditionedStatus: xpv1.ConditionedStatus{ 112 | Conditions: []xpv1.Condition{ 113 | { 114 | Type: c[5].ConditionType, 115 | Status: c[5].ConditionStatus, 116 | LastTransitionTime: metav1.Now(), 117 | ObservedGeneration: 42, 118 | }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | "OnlyLatestConditionsAreSet": { 126 | reason: "When there are many conditions of the same time, only the latest eligible conditions should be set.", 127 | mg: &v1alpha1.ClusterNopResource{ 128 | ObjectMeta: metav1.ObjectMeta{ 129 | // After 8 seconds conditions 2 (Ready=True) and 3 130 | // (Synced=False) should be set. Condition 2 supercedes 131 | // conditions 1 and 5 (both Ready=False), which happen 132 | // earlier. 133 | CreationTimestamp: metav1.NewTime(now.Add(-8 * time.Second)), 134 | Generation: 42, 135 | }, 136 | Spec: v1alpha1.NopSpec{ 137 | ForProvider: v1alpha1.NopParameters{ 138 | ConditionAfter: c, 139 | }, 140 | }, 141 | }, 142 | want: &v1alpha1.ClusterNopResource{ 143 | ObjectMeta: metav1.ObjectMeta{ 144 | CreationTimestamp: metav1.NewTime(now.Add(-8 * time.Second)), 145 | Generation: 42, 146 | }, 147 | Spec: v1alpha1.NopSpec{ 148 | ForProvider: v1alpha1.NopParameters{ 149 | ConditionAfter: c, 150 | }, 151 | }, 152 | Status: v1alpha1.NopStatus{ 153 | ResourceStatus: xpv1.ResourceStatus{ 154 | ConditionedStatus: xpv1.ConditionedStatus{ 155 | Conditions: []xpv1.Condition{ 156 | { 157 | Type: c[2].ConditionType, 158 | Status: c[2].ConditionStatus, 159 | LastTransitionTime: metav1.Now(), 160 | ObservedGeneration: 42, 161 | }, 162 | { 163 | Type: c[3].ConditionType, 164 | Status: c[3].ConditionStatus, 165 | LastTransitionTime: metav1.Now(), 166 | ObservedGeneration: 42, 167 | }, 168 | }, 169 | }, 170 | }, 171 | }, 172 | }, 173 | }, 174 | "LongTimeReconcileBehaviour": { 175 | reason: "Indexes with last set status of each condition type should be returned till given time elapsed.", 176 | mg: &v1alpha1.ClusterNopResource{ 177 | ObjectMeta: metav1.ObjectMeta{ 178 | // After 8 seconds conditions 2 (Ready=True) and 3 179 | // (Synced=False) should be set. Condition 2 supercedes 180 | // conditions 1 and 5 (both Ready=False), which happen 181 | // earlier. 182 | CreationTimestamp: metav1.NewTime(now.Add(-50 * time.Second)), 183 | Generation: 42, 184 | }, 185 | Spec: v1alpha1.NopSpec{ 186 | ForProvider: v1alpha1.NopParameters{ 187 | ConditionAfter: c, 188 | }, 189 | }, 190 | }, 191 | want: &v1alpha1.ClusterNopResource{ 192 | ObjectMeta: metav1.ObjectMeta{ 193 | CreationTimestamp: metav1.NewTime(now.Add(-50 * time.Second)), 194 | Generation: 42, 195 | }, 196 | Spec: v1alpha1.NopSpec{ 197 | ForProvider: v1alpha1.NopParameters{ 198 | ConditionAfter: c, 199 | }, 200 | }, 201 | Status: v1alpha1.NopStatus{ 202 | ResourceStatus: xpv1.ResourceStatus{ 203 | ConditionedStatus: xpv1.ConditionedStatus{ 204 | Conditions: []xpv1.Condition{ 205 | { 206 | Type: c[0].ConditionType, 207 | Status: c[0].ConditionStatus, 208 | LastTransitionTime: metav1.Now(), 209 | ObservedGeneration: 42, 210 | }, 211 | { 212 | Type: c[4].ConditionType, 213 | Status: c[4].ConditionStatus, 214 | LastTransitionTime: metav1.Now(), 215 | ObservedGeneration: 42, 216 | }, 217 | }, 218 | }, 219 | }, 220 | }, 221 | }, 222 | }, 223 | } 224 | 225 | for name, tc := range cases { 226 | t.Run(name, func(t *testing.T) { 227 | _, _ = Observe(context.Background(), tc.mg) 228 | if diff := cmp.Diff(tc.want, tc.mg, test.EquateConditions()); diff != "" { 229 | t.Errorf("Observe(...): -want, +got:\n%s\n%s\n", tc.reason, diff) 230 | } 231 | }) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /apis/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2025 The Crossplane Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *ClusterNopResource) DeepCopyInto(out *ClusterNopResource) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | in.Status.DeepCopyInto(&out.Status) 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterNopResource. 38 | func (in *ClusterNopResource) DeepCopy() *ClusterNopResource { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(ClusterNopResource) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *ClusterNopResource) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *ClusterNopResourceList) DeepCopyInto(out *ClusterNopResourceList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]ClusterNopResource, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | } 68 | 69 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterNopResourceList. 70 | func (in *ClusterNopResourceList) DeepCopy() *ClusterNopResourceList { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(ClusterNopResourceList) 75 | in.DeepCopyInto(out) 76 | return out 77 | } 78 | 79 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 80 | func (in *ClusterNopResourceList) DeepCopyObject() runtime.Object { 81 | if c := in.DeepCopy(); c != nil { 82 | return c 83 | } 84 | return nil 85 | } 86 | 87 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 88 | func (in *ConditionAfter) DeepCopyInto(out *ConditionAfter) { 89 | *out = *in 90 | out.Time = in.Time 91 | if in.ConditionReason != nil { 92 | in, out := &in.ConditionReason, &out.ConditionReason 93 | *out = new(v1.ConditionReason) 94 | **out = **in 95 | } 96 | } 97 | 98 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionAfter. 99 | func (in *ConditionAfter) DeepCopy() *ConditionAfter { 100 | if in == nil { 101 | return nil 102 | } 103 | out := new(ConditionAfter) 104 | in.DeepCopyInto(out) 105 | return out 106 | } 107 | 108 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 109 | func (in *ConnectionDetail) DeepCopyInto(out *ConnectionDetail) { 110 | *out = *in 111 | } 112 | 113 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionDetail. 114 | func (in *ConnectionDetail) DeepCopy() *ConnectionDetail { 115 | if in == nil { 116 | return nil 117 | } 118 | out := new(ConnectionDetail) 119 | in.DeepCopyInto(out) 120 | return out 121 | } 122 | 123 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 124 | func (in *NopObservation) DeepCopyInto(out *NopObservation) { 125 | *out = *in 126 | in.Fields.DeepCopyInto(&out.Fields) 127 | } 128 | 129 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NopObservation. 130 | func (in *NopObservation) DeepCopy() *NopObservation { 131 | if in == nil { 132 | return nil 133 | } 134 | out := new(NopObservation) 135 | in.DeepCopyInto(out) 136 | return out 137 | } 138 | 139 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 140 | func (in *NopParameters) DeepCopyInto(out *NopParameters) { 141 | *out = *in 142 | if in.ConditionAfter != nil { 143 | in, out := &in.ConditionAfter, &out.ConditionAfter 144 | *out = make([]ConditionAfter, len(*in)) 145 | for i := range *in { 146 | (*in)[i].DeepCopyInto(&(*out)[i]) 147 | } 148 | } 149 | if in.ConnectionDetails != nil { 150 | in, out := &in.ConnectionDetails, &out.ConnectionDetails 151 | *out = make([]ConnectionDetail, len(*in)) 152 | copy(*out, *in) 153 | } 154 | in.Fields.DeepCopyInto(&out.Fields) 155 | } 156 | 157 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NopParameters. 158 | func (in *NopParameters) DeepCopy() *NopParameters { 159 | if in == nil { 160 | return nil 161 | } 162 | out := new(NopParameters) 163 | in.DeepCopyInto(out) 164 | return out 165 | } 166 | 167 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 168 | func (in *NopResource) DeepCopyInto(out *NopResource) { 169 | *out = *in 170 | out.TypeMeta = in.TypeMeta 171 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 172 | in.Spec.DeepCopyInto(&out.Spec) 173 | in.Status.DeepCopyInto(&out.Status) 174 | } 175 | 176 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NopResource. 177 | func (in *NopResource) DeepCopy() *NopResource { 178 | if in == nil { 179 | return nil 180 | } 181 | out := new(NopResource) 182 | in.DeepCopyInto(out) 183 | return out 184 | } 185 | 186 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 187 | func (in *NopResource) DeepCopyObject() runtime.Object { 188 | if c := in.DeepCopy(); c != nil { 189 | return c 190 | } 191 | return nil 192 | } 193 | 194 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 195 | func (in *NopResourceList) DeepCopyInto(out *NopResourceList) { 196 | *out = *in 197 | out.TypeMeta = in.TypeMeta 198 | in.ListMeta.DeepCopyInto(&out.ListMeta) 199 | if in.Items != nil { 200 | in, out := &in.Items, &out.Items 201 | *out = make([]NopResource, len(*in)) 202 | for i := range *in { 203 | (*in)[i].DeepCopyInto(&(*out)[i]) 204 | } 205 | } 206 | } 207 | 208 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NopResourceList. 209 | func (in *NopResourceList) DeepCopy() *NopResourceList { 210 | if in == nil { 211 | return nil 212 | } 213 | out := new(NopResourceList) 214 | in.DeepCopyInto(out) 215 | return out 216 | } 217 | 218 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 219 | func (in *NopResourceList) DeepCopyObject() runtime.Object { 220 | if c := in.DeepCopy(); c != nil { 221 | return c 222 | } 223 | return nil 224 | } 225 | 226 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 227 | func (in *NopSpec) DeepCopyInto(out *NopSpec) { 228 | *out = *in 229 | in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) 230 | in.ForProvider.DeepCopyInto(&out.ForProvider) 231 | } 232 | 233 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NopSpec. 234 | func (in *NopSpec) DeepCopy() *NopSpec { 235 | if in == nil { 236 | return nil 237 | } 238 | out := new(NopSpec) 239 | in.DeepCopyInto(out) 240 | return out 241 | } 242 | 243 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 244 | func (in *NopStatus) DeepCopyInto(out *NopStatus) { 245 | *out = *in 246 | in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) 247 | in.AtProvider.DeepCopyInto(&out.AtProvider) 248 | } 249 | 250 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NopStatus. 251 | func (in *NopStatus) DeepCopy() *NopStatus { 252 | if in == nil { 253 | return nil 254 | } 255 | out := new(NopStatus) 256 | in.DeepCopyInto(out) 257 | return out 258 | } 259 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | output: 4 | formats: 5 | text: 6 | path: stderr 7 | 8 | linters: 9 | default: all 10 | disable: 11 | # These are linters we'd like to enable, but that will be labor intensive to 12 | # make existing code compliant. 13 | - wrapcheck 14 | - varnamelen 15 | - testpackage 16 | - paralleltest 17 | - nilnil 18 | - funcorder 19 | 20 | # Below are linters that lint for things we don't value. Each entry below 21 | # this line must have a comment explaining the rationale. 22 | 23 | # These linters add whitespace in an attempt to make code more readable. 24 | # This isn't a widely accepted Go best practice, and would be laborious to 25 | # apply to existing code. 26 | - wsl 27 | - wsl_v5 28 | - nlreturn 29 | 30 | # Warns about uses of fmt.Sprintf that are less performant than alternatives 31 | # such as string concatenation. We value readability more than performance 32 | # unless performance is measured to be an issue. 33 | - perfsprint 34 | 35 | # This linter: 36 | # 37 | # 1. Requires errors.Is/errors.As to test equality. 38 | # 2. Requires all errors be wrapped with fmt.Errorf specifically. 39 | # 3. Disallows errors.New inline - requires package level errors. 40 | # 41 | # 1 is covered by other linters. 2 is covered by wrapcheck, which can also 42 | # handle our use of crossplane-runtime's errors package. 3 is more strict 43 | # than we need. Not every error needs to be tested for equality. 44 | - err113 45 | 46 | # These linters duplicate gocognit, but calculate complexity differently. 47 | - gocyclo 48 | - cyclop 49 | - nestif 50 | - funlen 51 | - maintidx 52 | 53 | # Enforces max line length. It's not idiomatic to enforce a strict limit on 54 | # line length in Go. We'd prefer to lint for things that often cause long 55 | # lines, like functions with too many parameters or long parameter names 56 | # that duplicate their types. 57 | - lll 58 | 59 | # Warns about struct instantiations that don't specify every field. Could be 60 | # useful in theory to catch fields that are accidentally omitted. Seems like 61 | # it would have many more false positives than useful catches, though. 62 | - exhaustruct 63 | 64 | # Warns about TODO comments. The rationale being they should be issues 65 | # instead. We're okay with using TODO to track minor cleanups for next time 66 | # we touch a particular file. 67 | - godox 68 | 69 | # Warns about duplicated code blocks within the same file. Could be useful 70 | # to prompt folks to think about whether code should be broken out into a 71 | # function, but generally we're less worried about DRY and fine with a 72 | # little copying. We don't want to give folks the impression that we require 73 | # every duplicated code block to be factored out into a function. 74 | - dupl 75 | 76 | # Warns about returning interfaces rather than concrete types. We do think 77 | # it's best to avoid returning interfaces where possible. However, at the 78 | # time of writing enabling this linter would only catch the (many) cases 79 | # where we must return an interface. 80 | - ireturn 81 | 82 | # Warns about returning named variables. We do think it's best to avoid 83 | # returning named variables where possible. However, at the time of writing 84 | # enabling this linter would only catch the (many) cases where returning 85 | # named variables is useful to document what the variables are. For example 86 | # we believe it makes sense to return (ready bool) rather than just (bool) 87 | # to communicate what the bool means. 88 | - nonamedreturns 89 | 90 | # Warns about using magic numbers. We do think it's best to avoid magic 91 | # numbers, but we should not be strict about it. 92 | - mnd 93 | 94 | # Warns about if err := Foo(); err != nil style error checks. Seems to go 95 | # against idiomatic Go programming, which encourages this approach - e.g. 96 | # to scope errors. 97 | - noinlineerr 98 | settings: 99 | depguard: 100 | rules: 101 | no_third_party_test_libraries: 102 | list-mode: lax 103 | files: 104 | - $test 105 | deny: 106 | - pkg: github.com/stretchr/testify 107 | desc: See https://go.dev/wiki/TestComments#assert-libraries 108 | - pkg: github.com/onsi/ginkgo 109 | desc: See https://go.dev/wiki/TestComments#assert-libraries 110 | - pkg: github.com/onsi/gomega 111 | desc: See https://go.dev/wiki/TestComments#assert-libraries 112 | dupl: 113 | threshold: 100 114 | errcheck: 115 | check-type-assertions: false 116 | check-blank: false 117 | goconst: 118 | min-len: 3 119 | min-occurrences: 5 120 | gocritic: 121 | enabled-tags: 122 | - performance 123 | settings: 124 | captLocal: 125 | paramsOnly: true 126 | rangeValCopy: 127 | sizeThreshold: 32 128 | govet: 129 | disable: 130 | - shadow 131 | interfacebloat: 132 | max: 5 133 | lll: 134 | tab-width: 1 135 | nakedret: 136 | max-func-lines: 30 137 | nolintlint: 138 | require-explanation: true 139 | require-specific: true 140 | prealloc: 141 | simple: true 142 | range-loops: true 143 | for-loops: false 144 | tagliatelle: 145 | case: 146 | rules: 147 | json: goCamel 148 | unparam: 149 | check-exported: false 150 | unused: 151 | exported-fields-are-used: true 152 | exclusions: 153 | generated: lax 154 | rules: 155 | - linters: 156 | - containedctx 157 | - errcheck 158 | - forcetypeassert 159 | - gochecknoglobals 160 | - gochecknoinits 161 | - gocognit 162 | - gosec 163 | - scopelint 164 | - unparam 165 | - embeddedstructfieldcheck 166 | path: _test(ing)?\.go 167 | 168 | - linters: 169 | - gocritic 170 | path: _test\.go 171 | text: (unnamedResult|exitAfterDefer) 172 | 173 | # It's idiomatic to register Kubernetes types with a package scoped 174 | # SchemeBuilder using an init function. 175 | - linters: 176 | - gochecknoglobals 177 | - gochecknoinits 178 | path: apis/ 179 | 180 | # These are performance optimisations rather than style issues per se. 181 | # They warn when function arguments or range values copy a lot of memory 182 | # rather than using a pointer. 183 | - linters: 184 | - gocritic 185 | text: '(hugeParam|rangeValCopy):' 186 | 187 | # This "TestMain should call os.Exit to set exit code" warning is not clever 188 | # enough to notice that we call a helper method that calls os.Exit. 189 | - linters: 190 | - staticcheck 191 | text: 'SA3000:' 192 | 193 | # This is a "potential hardcoded credentials" warning. It's triggered by 194 | # any variable with 'secret' in the same, and thus hits a lot of false 195 | # positives in Kubernetes land where a Secret is an object type. 196 | - linters: 197 | - gosec 198 | text: 'G101:' 199 | 200 | # This is an 'errors unhandled' warning that duplicates errcheck. 201 | - linters: 202 | - gosec 203 | text: 'G104:' 204 | 205 | # This is about implicit memory aliasing in a range loop. 206 | # This is a false positive with Go v1.22 and above. 207 | - linters: 208 | - gosec 209 | text: 'G601:' 210 | 211 | # Some k8s dependencies do not have JSON tags on all fields in structs. 212 | - linters: 213 | - musttag 214 | path: k8s.io/ 215 | 216 | # Various fields related to native patch and transform Composition are 217 | # deprecated, but we can't drop support from Crossplane 1.x. We ignore the 218 | # warnings globally instead of suppressing them with comments everywhere. 219 | - linters: 220 | - staticcheck 221 | text: 'SA1019: .+ is deprecated: Use Composition Functions instead.' 222 | 223 | paths: 224 | - zz_generated\..+\.go$ 225 | - .+\.pb.go$ 226 | - third_party$ 227 | - builtin$ 228 | - examples$ 229 | 230 | issues: 231 | max-issues-per-linter: 0 232 | max-same-issues: 0 233 | new: false 234 | 235 | formatters: 236 | enable: 237 | - gci 238 | - gofmt 239 | - gofumpt 240 | - goimports 241 | settings: 242 | gci: 243 | sections: 244 | - standard 245 | - default 246 | - prefix(github.com/crossplane/crossplane-runtime) 247 | - prefix(github.com/crossplane/crossplane) 248 | - prefix(github.com/crossplane-contrib/provider-nop) 249 | - blank 250 | - dot 251 | custom-order: true 252 | gofmt: 253 | simplify: true 254 | exclusions: 255 | generated: lax 256 | paths: 257 | - zz_generated\..+\.go$ 258 | - .+\.pb.go$ 259 | - third_party$ 260 | - builtin$ 261 | - examples$ 262 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 The Crossplane Authors. All rights reserved. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /package/crds/nop.crossplane.io_nopresources.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.18.0 7 | name: nopresources.nop.crossplane.io 8 | spec: 9 | group: nop.crossplane.io 10 | names: 11 | categories: 12 | - crossplane 13 | - managed 14 | - nop 15 | kind: NopResource 16 | listKind: NopResourceList 17 | plural: nopresources 18 | singular: nopresource 19 | scope: Namespaced 20 | versions: 21 | - additionalPrinterColumns: 22 | - jsonPath: .status.conditions[?(@.type=='Ready')].status 23 | name: READY 24 | type: string 25 | - jsonPath: .status.conditions[?(@.type=='Synced')].status 26 | name: SYNCED 27 | type: string 28 | - jsonPath: .metadata.creationTimestamp 29 | name: AGE 30 | type: date 31 | name: v1alpha1 32 | schema: 33 | openAPIV3Schema: 34 | description: A NopResource is an example API type. 35 | properties: 36 | apiVersion: 37 | description: |- 38 | APIVersion defines the versioned schema of this representation of an object. 39 | Servers should convert recognized schemas to the latest internal value, and 40 | may reject unrecognized values. 41 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 42 | type: string 43 | kind: 44 | description: |- 45 | Kind is a string value representing the REST resource this object represents. 46 | Servers may infer this from the endpoint the client submits requests to. 47 | Cannot be updated. 48 | In CamelCase. 49 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 50 | type: string 51 | metadata: 52 | type: object 53 | spec: 54 | description: A NopSpec defines the desired state of a NopResource. 55 | properties: 56 | deletionPolicy: 57 | default: Delete 58 | description: |- 59 | DeletionPolicy specifies what will happen to the underlying external 60 | when this managed resource is deleted - either "Delete" or "Orphan" the 61 | external resource. 62 | This field is planned to be deprecated in favor of the ManagementPolicies 63 | field in a future release. Currently, both could be set independently and 64 | non-default values would be honored if the feature flag is enabled. 65 | See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 66 | enum: 67 | - Orphan 68 | - Delete 69 | type: string 70 | forProvider: 71 | description: NopParameters are the configurable fields of a NopResource. 72 | properties: 73 | conditionAfter: 74 | description: |- 75 | ConditionAfter can be used to set status conditions after a specified 76 | time. By default, a NopResource will only have a status condition of 77 | Type: Synced. It will never have a status condition of Type: Ready 78 | unless one is configured here. 79 | items: 80 | description: |- 81 | ConditionAfter specifies a condition of a NopResource that should be 82 | set after a certain duration. 83 | properties: 84 | conditionReason: 85 | description: ConditionReason to set - e.g. Available. 86 | type: string 87 | conditionStatus: 88 | description: ConditionStatus to set - e.g. True. 89 | type: string 90 | conditionType: 91 | description: ConditionType to set - e.g. Ready. 92 | type: string 93 | time: 94 | description: Time is the duration after which the condition 95 | should be set. 96 | type: string 97 | required: 98 | - conditionStatus 99 | - conditionType 100 | - time 101 | type: object 102 | type: array 103 | connectionDetails: 104 | description: ConnectionDetails that this NopResource should emit 105 | on each reconcile. 106 | items: 107 | description: |- 108 | ConnectionDetail specifies a connection detail a NopResource should 109 | emit. 110 | properties: 111 | name: 112 | description: Name of the connection detail. 113 | type: string 114 | value: 115 | description: Value of the connection detail. 116 | type: string 117 | required: 118 | - name 119 | - value 120 | type: object 121 | type: array 122 | fields: 123 | description: |- 124 | Fields is an arbitrary object you can patch to and from. It has no 125 | schema, is not validated, and is not used by the NopResource controller. 126 | type: object 127 | x-kubernetes-preserve-unknown-fields: true 128 | type: object 129 | managementPolicies: 130 | default: 131 | - '*' 132 | description: |- 133 | THIS IS A BETA FIELD. It is on by default but can be opted out 134 | through a Crossplane feature flag. 135 | ManagementPolicies specify the array of actions Crossplane is allowed to 136 | take on the managed and external resources. 137 | This field is planned to replace the DeletionPolicy field in a future 138 | release. Currently, both could be set independently and non-default 139 | values would be honored if the feature flag is enabled. If both are 140 | custom, the DeletionPolicy field will be ignored. 141 | See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 142 | and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md 143 | items: 144 | description: |- 145 | A ManagementAction represents an action that the Crossplane controllers 146 | can take on an external resource. 147 | enum: 148 | - Observe 149 | - Create 150 | - Update 151 | - Delete 152 | - LateInitialize 153 | - '*' 154 | type: string 155 | type: array 156 | providerConfigRef: 157 | default: 158 | name: default 159 | description: |- 160 | ProviderConfigReference specifies how the provider that will be used to 161 | create, observe, update, and delete this managed resource should be 162 | configured. 163 | properties: 164 | name: 165 | description: Name of the referenced object. 166 | type: string 167 | policy: 168 | description: Policies for referencing. 169 | properties: 170 | resolution: 171 | default: Required 172 | description: |- 173 | Resolution specifies whether resolution of this reference is required. 174 | The default is 'Required', which means the reconcile will fail if the 175 | reference cannot be resolved. 'Optional' means this reference will be 176 | a no-op if it cannot be resolved. 177 | enum: 178 | - Required 179 | - Optional 180 | type: string 181 | resolve: 182 | description: |- 183 | Resolve specifies when this reference should be resolved. The default 184 | is 'IfNotPresent', which will attempt to resolve the reference only when 185 | the corresponding field is not present. Use 'Always' to resolve the 186 | reference on every reconcile. 187 | enum: 188 | - Always 189 | - IfNotPresent 190 | type: string 191 | type: object 192 | required: 193 | - name 194 | type: object 195 | writeConnectionSecretToRef: 196 | description: |- 197 | WriteConnectionSecretToReference specifies the namespace and name of a 198 | Secret to which any connection details for this managed resource should 199 | be written. Connection details frequently include the endpoint, username, 200 | and password required to connect to the managed resource. 201 | properties: 202 | name: 203 | description: Name of the secret. 204 | type: string 205 | namespace: 206 | description: Namespace of the secret. 207 | type: string 208 | required: 209 | - name 210 | - namespace 211 | type: object 212 | required: 213 | - forProvider 214 | type: object 215 | status: 216 | description: A NopStatus represents the observed state of a NopResource. 217 | properties: 218 | atProvider: 219 | description: NopObservation are the observable fields of a NopResource. 220 | properties: 221 | fields: 222 | description: |- 223 | Fields is an arbitrary object you can patch to and from. It has no 224 | schema, is not validated, and is not used by the NopResource controller. 225 | type: object 226 | x-kubernetes-preserve-unknown-fields: true 227 | type: object 228 | conditions: 229 | description: Conditions of the resource. 230 | items: 231 | description: A Condition that may apply to a resource. 232 | properties: 233 | lastTransitionTime: 234 | description: |- 235 | LastTransitionTime is the last time this condition transitioned from one 236 | status to another. 237 | format: date-time 238 | type: string 239 | message: 240 | description: |- 241 | A Message containing details about this condition's last transition from 242 | one status to another, if any. 243 | type: string 244 | observedGeneration: 245 | description: |- 246 | ObservedGeneration represents the .metadata.generation that the condition was set based upon. 247 | For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 248 | with respect to the current state of the instance. 249 | format: int64 250 | type: integer 251 | reason: 252 | description: A Reason for this condition's last transition from 253 | one status to another. 254 | type: string 255 | status: 256 | description: Status of this condition; is it currently True, 257 | False, or Unknown? 258 | type: string 259 | type: 260 | description: |- 261 | Type of this condition. At most one of each condition type may apply to 262 | a resource at any point in time. 263 | type: string 264 | required: 265 | - lastTransitionTime 266 | - reason 267 | - status 268 | - type 269 | type: object 270 | type: array 271 | x-kubernetes-list-map-keys: 272 | - type 273 | x-kubernetes-list-type: map 274 | observedGeneration: 275 | description: |- 276 | ObservedGeneration is the latest metadata.generation 277 | which resulted in either a ready state, or stalled due to error 278 | it can not recover from without human intervention. 279 | format: int64 280 | type: integer 281 | type: object 282 | required: 283 | - spec 284 | type: object 285 | served: true 286 | storage: true 287 | subresources: 288 | status: {} 289 | -------------------------------------------------------------------------------- /package/crds/nop.crossplane.io_clusternopresources.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.18.0 7 | name: clusternopresources.nop.crossplane.io 8 | spec: 9 | group: nop.crossplane.io 10 | names: 11 | categories: 12 | - crossplane 13 | - managed 14 | - nop 15 | kind: ClusterNopResource 16 | listKind: ClusterNopResourceList 17 | plural: clusternopresources 18 | singular: clusternopresource 19 | scope: Cluster 20 | versions: 21 | - additionalPrinterColumns: 22 | - jsonPath: .status.conditions[?(@.type=='Ready')].status 23 | name: READY 24 | type: string 25 | - jsonPath: .status.conditions[?(@.type=='Synced')].status 26 | name: SYNCED 27 | type: string 28 | - jsonPath: .metadata.creationTimestamp 29 | name: AGE 30 | type: date 31 | name: v1alpha1 32 | schema: 33 | openAPIV3Schema: 34 | description: A ClusterNopResource is an example API type. 35 | properties: 36 | apiVersion: 37 | description: |- 38 | APIVersion defines the versioned schema of this representation of an object. 39 | Servers should convert recognized schemas to the latest internal value, and 40 | may reject unrecognized values. 41 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 42 | type: string 43 | kind: 44 | description: |- 45 | Kind is a string value representing the REST resource this object represents. 46 | Servers may infer this from the endpoint the client submits requests to. 47 | Cannot be updated. 48 | In CamelCase. 49 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 50 | type: string 51 | metadata: 52 | type: object 53 | spec: 54 | description: A NopSpec defines the desired state of a NopResource. 55 | properties: 56 | deletionPolicy: 57 | default: Delete 58 | description: |- 59 | DeletionPolicy specifies what will happen to the underlying external 60 | when this managed resource is deleted - either "Delete" or "Orphan" the 61 | external resource. 62 | This field is planned to be deprecated in favor of the ManagementPolicies 63 | field in a future release. Currently, both could be set independently and 64 | non-default values would be honored if the feature flag is enabled. 65 | See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 66 | enum: 67 | - Orphan 68 | - Delete 69 | type: string 70 | forProvider: 71 | description: NopParameters are the configurable fields of a NopResource. 72 | properties: 73 | conditionAfter: 74 | description: |- 75 | ConditionAfter can be used to set status conditions after a specified 76 | time. By default, a NopResource will only have a status condition of 77 | Type: Synced. It will never have a status condition of Type: Ready 78 | unless one is configured here. 79 | items: 80 | description: |- 81 | ConditionAfter specifies a condition of a NopResource that should be 82 | set after a certain duration. 83 | properties: 84 | conditionReason: 85 | description: ConditionReason to set - e.g. Available. 86 | type: string 87 | conditionStatus: 88 | description: ConditionStatus to set - e.g. True. 89 | type: string 90 | conditionType: 91 | description: ConditionType to set - e.g. Ready. 92 | type: string 93 | time: 94 | description: Time is the duration after which the condition 95 | should be set. 96 | type: string 97 | required: 98 | - conditionStatus 99 | - conditionType 100 | - time 101 | type: object 102 | type: array 103 | connectionDetails: 104 | description: ConnectionDetails that this NopResource should emit 105 | on each reconcile. 106 | items: 107 | description: |- 108 | ConnectionDetail specifies a connection detail a NopResource should 109 | emit. 110 | properties: 111 | name: 112 | description: Name of the connection detail. 113 | type: string 114 | value: 115 | description: Value of the connection detail. 116 | type: string 117 | required: 118 | - name 119 | - value 120 | type: object 121 | type: array 122 | fields: 123 | description: |- 124 | Fields is an arbitrary object you can patch to and from. It has no 125 | schema, is not validated, and is not used by the NopResource controller. 126 | type: object 127 | x-kubernetes-preserve-unknown-fields: true 128 | type: object 129 | managementPolicies: 130 | default: 131 | - '*' 132 | description: |- 133 | THIS IS A BETA FIELD. It is on by default but can be opted out 134 | through a Crossplane feature flag. 135 | ManagementPolicies specify the array of actions Crossplane is allowed to 136 | take on the managed and external resources. 137 | This field is planned to replace the DeletionPolicy field in a future 138 | release. Currently, both could be set independently and non-default 139 | values would be honored if the feature flag is enabled. If both are 140 | custom, the DeletionPolicy field will be ignored. 141 | See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 142 | and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md 143 | items: 144 | description: |- 145 | A ManagementAction represents an action that the Crossplane controllers 146 | can take on an external resource. 147 | enum: 148 | - Observe 149 | - Create 150 | - Update 151 | - Delete 152 | - LateInitialize 153 | - '*' 154 | type: string 155 | type: array 156 | providerConfigRef: 157 | default: 158 | name: default 159 | description: |- 160 | ProviderConfigReference specifies how the provider that will be used to 161 | create, observe, update, and delete this managed resource should be 162 | configured. 163 | properties: 164 | name: 165 | description: Name of the referenced object. 166 | type: string 167 | policy: 168 | description: Policies for referencing. 169 | properties: 170 | resolution: 171 | default: Required 172 | description: |- 173 | Resolution specifies whether resolution of this reference is required. 174 | The default is 'Required', which means the reconcile will fail if the 175 | reference cannot be resolved. 'Optional' means this reference will be 176 | a no-op if it cannot be resolved. 177 | enum: 178 | - Required 179 | - Optional 180 | type: string 181 | resolve: 182 | description: |- 183 | Resolve specifies when this reference should be resolved. The default 184 | is 'IfNotPresent', which will attempt to resolve the reference only when 185 | the corresponding field is not present. Use 'Always' to resolve the 186 | reference on every reconcile. 187 | enum: 188 | - Always 189 | - IfNotPresent 190 | type: string 191 | type: object 192 | required: 193 | - name 194 | type: object 195 | writeConnectionSecretToRef: 196 | description: |- 197 | WriteConnectionSecretToReference specifies the namespace and name of a 198 | Secret to which any connection details for this managed resource should 199 | be written. Connection details frequently include the endpoint, username, 200 | and password required to connect to the managed resource. 201 | properties: 202 | name: 203 | description: Name of the secret. 204 | type: string 205 | namespace: 206 | description: Namespace of the secret. 207 | type: string 208 | required: 209 | - name 210 | - namespace 211 | type: object 212 | required: 213 | - forProvider 214 | type: object 215 | status: 216 | description: A NopStatus represents the observed state of a NopResource. 217 | properties: 218 | atProvider: 219 | description: NopObservation are the observable fields of a NopResource. 220 | properties: 221 | fields: 222 | description: |- 223 | Fields is an arbitrary object you can patch to and from. It has no 224 | schema, is not validated, and is not used by the NopResource controller. 225 | type: object 226 | x-kubernetes-preserve-unknown-fields: true 227 | type: object 228 | conditions: 229 | description: Conditions of the resource. 230 | items: 231 | description: A Condition that may apply to a resource. 232 | properties: 233 | lastTransitionTime: 234 | description: |- 235 | LastTransitionTime is the last time this condition transitioned from one 236 | status to another. 237 | format: date-time 238 | type: string 239 | message: 240 | description: |- 241 | A Message containing details about this condition's last transition from 242 | one status to another, if any. 243 | type: string 244 | observedGeneration: 245 | description: |- 246 | ObservedGeneration represents the .metadata.generation that the condition was set based upon. 247 | For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 248 | with respect to the current state of the instance. 249 | format: int64 250 | type: integer 251 | reason: 252 | description: A Reason for this condition's last transition from 253 | one status to another. 254 | type: string 255 | status: 256 | description: Status of this condition; is it currently True, 257 | False, or Unknown? 258 | type: string 259 | type: 260 | description: |- 261 | Type of this condition. At most one of each condition type may apply to 262 | a resource at any point in time. 263 | type: string 264 | required: 265 | - lastTransitionTime 266 | - reason 267 | - status 268 | - type 269 | type: object 270 | type: array 271 | x-kubernetes-list-map-keys: 272 | - type 273 | x-kubernetes-list-type: map 274 | observedGeneration: 275 | description: |- 276 | ObservedGeneration is the latest metadata.generation 277 | which resulted in either a ready state, or stalled due to error 278 | it can not recover from without human intervention. 279 | format: int64 280 | type: integer 281 | type: object 282 | required: 283 | - spec 284 | type: object 285 | served: true 286 | storage: true 287 | subresources: 288 | status: {} 289 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 4 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 5 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 7 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= 8 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= 9 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 12 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 13 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 14 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 15 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 16 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 17 | github.com/crossplane/crossplane-runtime/v2 v2.0.0-20250730220209-c306b1c8b181 h1:yU9+BtCiiMmymYt499egG5FZ1IQ7WddSi1V6H0h0DTk= 18 | github.com/crossplane/crossplane-runtime/v2 v2.0.0-20250730220209-c306b1c8b181/go.mod h1:pkd5UzmE8esaZAApevMutR832GjJ1Qgc5Ngr78ByxrI= 19 | github.com/crossplane/crossplane-tools v0.0.0-20250731192036-00d407d8b7ec h1:+51Et4UW8XrvGne8RAqn9qEIfhoqPXYqIp/kQvpMaAo= 20 | github.com/crossplane/crossplane-tools v0.0.0-20250731192036-00d407d8b7ec/go.mod h1:8etxwmP4cZwJDwen4+PQlnc1tggltAhEfyyigmdHulQ= 21 | github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= 22 | github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 28 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 29 | github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= 30 | github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 31 | github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= 32 | github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 33 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 34 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 35 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 36 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 37 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 38 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 39 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 40 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 41 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 42 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 43 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 44 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 45 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 46 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 47 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 48 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 49 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 50 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 51 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 52 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 53 | github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= 54 | github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= 55 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 56 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 57 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 58 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 59 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 60 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 61 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 62 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 63 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 64 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 65 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 66 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 67 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 68 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 69 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 70 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 71 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 72 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 73 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 74 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 75 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 76 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 77 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 78 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 79 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 80 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 81 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 82 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 83 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 84 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 85 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 86 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 87 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 88 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 89 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 90 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 91 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 92 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 93 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 94 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 95 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 96 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 97 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 98 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 99 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 100 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 101 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 102 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 103 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 104 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 105 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 106 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 107 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 108 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 109 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 110 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 111 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 112 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 113 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 114 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 115 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 116 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 117 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 118 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 119 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 120 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 121 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 122 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 123 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 124 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 125 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 126 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 127 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 128 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 129 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 130 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 131 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 132 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 133 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 134 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 135 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 136 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 137 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 138 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 139 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 140 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 141 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 142 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 143 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 144 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 145 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 146 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 147 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 148 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 149 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 150 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 151 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 152 | go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= 153 | go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= 154 | go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= 155 | go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= 156 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 157 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 158 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 159 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 160 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 161 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 162 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 163 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 164 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 165 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 166 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 167 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 168 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 169 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 170 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 171 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 172 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 173 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 175 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 176 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 177 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 178 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 179 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 183 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 184 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 185 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 189 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 190 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 191 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 192 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 193 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 194 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 195 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 196 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 197 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 198 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 199 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 200 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 201 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 202 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 203 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 204 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 205 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 206 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 207 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 208 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 209 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 210 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 211 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= 212 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= 213 | google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= 214 | google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= 215 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 216 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 217 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 218 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 219 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 220 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 221 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 222 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 223 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 224 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 225 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 226 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 227 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 228 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 229 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 230 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 231 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 232 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 233 | k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= 234 | k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= 235 | k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= 236 | k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= 237 | k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= 238 | k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 239 | k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= 240 | k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= 241 | k8s.io/code-generator v0.33.0 h1:B212FVl6EFqNmlgdOZYWNi77yBv+ed3QgQsMR8YQCw4= 242 | k8s.io/code-generator v0.33.0/go.mod h1:KnJRokGxjvbBQkSJkbVuBbu6z4B0rC7ynkpY5Aw6m9o= 243 | k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= 244 | k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= 245 | k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 h1:2OX19X59HxDprNCVrWi6jb7LW1PoqTlYqEq5H2oetog= 246 | k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= 247 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 248 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 249 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 250 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 251 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 252 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 253 | sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= 254 | sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= 255 | sigs.k8s.io/controller-tools v0.18.0 h1:rGxGZCZTV2wJreeRgqVoWab/mfcumTMmSwKzoM9xrsE= 256 | sigs.k8s.io/controller-tools v0.18.0/go.mod h1:gLKoiGBriyNh+x1rWtUQnakUYEujErjXs9pf+x/8n1U= 257 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 258 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 259 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 260 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 261 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 262 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 263 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 264 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 265 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 266 | --------------------------------------------------------------------------------