├── bin └── .gitignore ├── pkg ├── test │ ├── doc.go │ ├── golden │ │ └── doc.go │ └── testreconciler │ │ └── simpletest │ │ ├── testdata │ │ ├── channels │ │ │ ├── stable │ │ │ └── packages │ │ │ │ └── simpletest │ │ │ │ └── 0.1.0 │ │ │ │ └── manifest.yaml │ │ └── reconcile │ │ │ ├── ssa │ │ │ └── create │ │ │ │ └── before.yaml │ │ │ └── direct │ │ │ └── create │ │ │ └── before.yaml │ │ ├── with_kubectl_test.go │ │ ├── without_kubectl_test.go │ │ └── v1alpha1 │ │ ├── groupversion_info.go │ │ └── types.go ├── patterns │ ├── declarative │ │ ├── pkg │ │ │ └── applier │ │ │ │ ├── global.go │ │ │ │ ├── testdata │ │ │ │ ├── applylib │ │ │ │ │ ├── simple2 │ │ │ │ │ │ ├── before.yaml │ │ │ │ │ │ └── manifest.yaml │ │ │ │ │ ├── simple1 │ │ │ │ │ │ └── manifest.yaml │ │ │ │ │ └── simple3 │ │ │ │ │ │ ├── manifest.yaml │ │ │ │ │ │ └── before.yaml │ │ │ │ ├── direct │ │ │ │ │ ├── simple1 │ │ │ │ │ │ └── manifest.yaml │ │ │ │ │ ├── simple2 │ │ │ │ │ │ ├── before.yaml │ │ │ │ │ │ └── manifest.yaml │ │ │ │ │ └── simple3 │ │ │ │ │ │ ├── manifest.yaml │ │ │ │ │ │ └── before.yaml │ │ │ │ └── kubectl │ │ │ │ │ ├── simple1 │ │ │ │ │ ├── manifest.yaml │ │ │ │ │ └── expected.yaml │ │ │ │ │ ├── simple2 │ │ │ │ │ ├── before.yaml │ │ │ │ │ ├── manifest.yaml │ │ │ │ │ └── expected.yaml │ │ │ │ │ └── simple3 │ │ │ │ │ ├── manifest.yaml │ │ │ │ │ ├── expected.yaml │ │ │ │ │ └── before.yaml │ │ │ │ ├── global_without_kubectl.go │ │ │ │ └── type.go │ │ ├── kustomize │ │ │ ├── disabled.go │ │ │ └── enabled.go │ │ ├── annotations.go │ │ ├── doc.go │ │ ├── ownerref.go │ │ ├── statusinfo.go │ │ ├── labels.go │ │ ├── hooks.go │ │ ├── sort.go │ │ └── application.go │ ├── addon │ │ ├── pkg │ │ │ ├── doc.go │ │ │ ├── status │ │ │ │ ├── doc.go │ │ │ │ ├── conditions.go │ │ │ │ ├── accessors.go │ │ │ │ ├── basic.go │ │ │ │ └── version.go │ │ │ ├── loaders │ │ │ │ ├── doc.go │ │ │ │ ├── git_test.go │ │ │ │ └── versions_test.go │ │ │ ├── apis │ │ │ │ └── v1alpha1 │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── conditions.go │ │ │ │ │ ├── common_types.go │ │ │ │ │ └── zz_generated.deepcopy.go │ │ │ └── utils │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ ├── doc.go │ │ ├── init.go │ │ ├── application.go │ │ └── patch.go │ └── doc.go ├── doc.go └── restmapper │ ├── controllerrestmapper_cr11.go │ ├── controllerrestmapper_cr15.go │ └── controllerrestmapper_test.go ├── version ├── TAG_VERSION └── README.md ├── .gitignore ├── mockkubeapiserver ├── tools │ └── generate-typeinfo │ │ ├── .gitignore │ │ └── README.md ├── tests │ └── testdata │ │ ├── configmap │ │ └── manifest.yaml │ │ └── secrets_stringdata │ │ └── manifest.yaml ├── storage │ ├── hook.go │ ├── memorystorage │ │ ├── schema.go │ │ ├── resourceinfo.go │ │ ├── schema_test.go │ │ └── crd.go │ ├── clock.go │ ├── uid.go │ └── interface.go ├── schemas │ ├── Makefile │ └── kubernetes_builtin_schema.go ├── hooks.go ├── forked │ └── README.md ├── apiversions.go ├── openapi_request.go ├── deleteresource.go ├── getresource.go ├── testhelpers.go ├── request.go ├── apiresourcelist.go ├── hooks │ ├── namespace.go │ └── deployment.go ├── go.mod └── apigrouplist.go ├── examples └── guestbook-operator │ ├── config │ ├── prometheus │ │ ├── kustomization.yaml │ │ └── monitor.yaml │ ├── certmanager │ │ ├── kustomization.yaml │ │ ├── kustomizeconfig.yaml │ │ └── certificate.yaml │ ├── webhook │ │ ├── kustomization.yaml │ │ ├── service.yaml │ │ └── kustomizeconfig.yaml │ ├── rbac │ │ ├── service_account.yaml │ │ ├── auth_proxy_client_clusterrole.yaml │ │ ├── role_binding.yaml │ │ ├── auth_proxy_role_binding.yaml │ │ ├── auth_proxy_service.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── auth_proxy_role.yaml │ │ ├── guestbook_viewer_role.yaml │ │ ├── guestbook_editor_role.yaml │ │ ├── leader_election_role.yaml │ │ ├── kustomization.yaml │ │ └── role.yaml │ ├── samples │ │ └── addons_v1alpha1_guestbook.yaml │ ├── manager │ │ ├── controller_manager_config.yaml │ │ ├── kustomization.yaml │ │ └── manager.yaml │ ├── crd │ │ ├── patches │ │ │ ├── cainjection_in_guestbooks.yaml │ │ │ └── webhook_in_guestbooks.yaml │ │ ├── kustomizeconfig.yaml │ │ └── kustomization.yaml │ └── default │ │ ├── manager_resource_patch.yaml │ │ ├── manager_webhook_patch.yaml │ │ ├── webhookcainjection_patch.yaml │ │ ├── manager_auth_proxy_patch.yaml │ │ └── kustomization.yaml │ ├── channels │ ├── packages │ │ └── guestbook │ │ │ └── 0.1.0 │ │ │ └── kustomization.yaml │ └── stable │ ├── controllers │ ├── testdata │ │ └── golden │ │ │ ├── simple │ │ │ └── input.yaml │ │ │ └── patches │ │ │ └── input.yaml │ └── guestbook_controller_test.go │ ├── .gitignore │ ├── PROJECT │ ├── hack │ └── boilerplate.go.txt │ ├── Dockerfile │ └── api │ └── v1alpha1 │ ├── groupversion_info.go │ └── guestbook_types.go ├── tools.go ├── .github ├── workflows │ ├── kind-config.yaml │ ├── tag-release.yaml │ └── main.yml ├── ISSUE_TEMPLATE │ ├── enhancement.md │ └── bug-report.md └── PULL_REQUEST_TEMPLATE.md ├── OWNERS ├── code-of-conduct.md ├── go.work ├── commonclient ├── doc.go ├── metrics_cr11.go ├── metrics_cr16.go ├── restmapper_cr11.go ├── restmapper_cr15.go ├── factory_cr15.go └── factory_cr11.go ├── docs ├── releases │ └── release-0.12.md └── project-management │ ├── deprecations.md │ └── branching-and-versioning.md ├── RELEASE.md ├── applylib ├── applyset │ ├── testdata │ │ └── testapplyset │ │ │ └── expected.yaml │ ├── interfaces.go │ ├── health.go │ └── parentref.go ├── forked │ └── k8s.io │ │ └── apimachinery │ │ └── pkg │ │ └── util │ │ └── sets │ │ ├── empty.go │ │ └── ordered.go ├── doc.go └── go.mod ├── hack ├── boilerplate.go.txt └── ci │ ├── e2e.sh │ └── test.sh ├── dev ├── update-golden ├── ci │ └── presubmits │ │ ├── verify-goimports │ │ └── verify-gomod ├── format-gomod ├── test ├── bots │ └── projectbots │ │ └── tag-release └── codebots │ └── update-golang-version ├── SECURITY_CONTACTS ├── doc.go ├── ktest ├── testharness │ ├── harness.go │ ├── manifest.go │ └── golden.go ├── httprecorder │ └── http_recorder.go └── go.mod ├── README.md ├── CONTRIBUTING.md ├── utils └── utils.go └── reconciler-options.md /bin/.gitignore: -------------------------------------------------------------------------------- 1 | kubectl 2 | -------------------------------------------------------------------------------- /pkg/test/doc.go: -------------------------------------------------------------------------------- 1 | package test 2 | -------------------------------------------------------------------------------- /version/TAG_VERSION: -------------------------------------------------------------------------------- 1 | 0.20.0-beta.1 2 | -------------------------------------------------------------------------------- /pkg/test/golden/doc.go: -------------------------------------------------------------------------------- 1 | package golden 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Emacs 2 | *~ 3 | \#*\# 4 | .idea -------------------------------------------------------------------------------- /mockkubeapiserver/tools/generate-typeinfo/.gitignore: -------------------------------------------------------------------------------- 1 | openapi.json 2 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /examples/guestbook-operator/channels/packages/guestbook/0.1.0/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifest.yaml 3 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package main 5 | 6 | import ( 7 | _ "golang.org/x/tools/cmd/goimports" 8 | ) 9 | -------------------------------------------------------------------------------- /examples/guestbook-operator/channels/stable: -------------------------------------------------------------------------------- 1 | # Versions for the stable channel 2 | manifests: 3 | - name: guestbook 4 | version: 0.1.0 5 | -------------------------------------------------------------------------------- /.github/workflows/kind-config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | - role: worker 6 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - justinsb 5 | - johnsonj 6 | - atoato88 7 | - tomasaschan 8 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /pkg/test/testreconciler/simpletest/testdata/channels/stable: -------------------------------------------------------------------------------- 1 | # Versions for the stable channel 2 | manifests: 3 | - name: simpletest 4 | version: 0.1.0 5 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.23.0 2 | 3 | toolchain go1.24.1 4 | 5 | use ( 6 | . 7 | ./applylib 8 | ./examples/guestbook-operator 9 | ./ktest 10 | ./mockkubeapiserver 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/global.go: -------------------------------------------------------------------------------- 1 | //go:build !without_direct_applier 2 | // +build !without_direct_applier 3 | 4 | package applier 5 | 6 | var DefaultApplier = NewDirectApplier() 7 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/samples/addons_v1alpha1_guestbook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.example.org/v1alpha1 2 | kind: Guestbook 3 | metadata: 4 | name: guestbook-sample 5 | spec: 6 | # Add fields here 7 | -------------------------------------------------------------------------------- /examples/guestbook-operator/controllers/testdata/golden/simple/input.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.example.org/v1alpha1 2 | kind: Guestbook 3 | metadata: 4 | name: guestbook-sample 5 | namespace: default 6 | spec: 7 | channel: stable -------------------------------------------------------------------------------- /commonclient/doc.go: -------------------------------------------------------------------------------- 1 | // Package commonclient provides version-independent wrappers over controller-runtime and client-go. 2 | // This enables one codebase to work with multiple versions of controller-runtime. 3 | package commonclient 4 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/applylib/simple2/before.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/direct/simple1/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/direct/simple2/before.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/direct/simple2/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/direct/simple3/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/before.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/applylib/simple1/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/applylib/simple2/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/applylib/simple3/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | 6 | --- 7 | 8 | kind: Namespace 9 | apiVersion: v1 10 | metadata: 11 | name: ns2 12 | -------------------------------------------------------------------------------- /version/README.md: -------------------------------------------------------------------------------- 1 | The files in this directory allow for github repo tagging to be controlled by Pull Request (and go through approval). 2 | 3 | These files are watched by the github actions defined in [tag-release.yaml](/.github/workflows/tag-release.yaml). -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /mockkubeapiserver/tests/testdata/configmap/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: default 5 | 6 | --- 7 | 8 | kind: ConfigMap 9 | apiVersion: v1 10 | metadata: 11 | name: config 12 | namespace: default 13 | data: 14 | foo: bar 15 | -------------------------------------------------------------------------------- /mockkubeapiserver/storage/hook.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | // A Hook implements a lightweight watch on all objects, intended for use to mock controller behaviour. 4 | type Hook interface { 5 | // OnWatchEvent is called whenever a watch event is created 6 | OnWatchEvent(ev *WatchEvent) 7 | } 8 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/expected.yaml: -------------------------------------------------------------------------------- 1 | GET https://kube-apiserver/api/v1 2 | Accept: application/json, */* 3 | 4 | 5 | 200 OK 6 | Cache-Control: no-cache, private 7 | Content-Type: application/json 8 | 9 | // discovery response removed for length 10 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/expected.yaml: -------------------------------------------------------------------------------- 1 | GET https://kube-apiserver/api/v1 2 | Accept: application/json, */* 3 | 4 | 5 | 200 OK 6 | Cache-Control: no-cache, private 7 | Content-Type: application/json 8 | 9 | // discovery response removed for length 10 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/expected.yaml: -------------------------------------------------------------------------------- 1 | GET https://kube-apiserver/api/v1 2 | Accept: application/json, */* 3 | 4 | 5 | 200 OK 6 | Cache-Control: no-cache, private 7 | Content-Type: application/json 8 | 9 | // discovery response removed for length 10 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /pkg/test/testreconciler/simpletest/testdata/reconcile/ssa/create/before.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: ns1 5 | --- 6 | apiVersion: addons.example.org/v1alpha1 7 | kind: SimpleTest 8 | metadata: 9 | name: simple1 10 | namespace: ns1 11 | spec: 12 | channel: stable -------------------------------------------------------------------------------- /pkg/test/testreconciler/simpletest/testdata/reconcile/direct/create/before.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: ns1 5 | --- 6 | apiVersion: addons.example.org/v1alpha1 7 | kind: SimpleTest 8 | metadata: 9 | name: simple1 10 | namespace: ns1 11 | spec: 12 | channel: stable 13 | -------------------------------------------------------------------------------- /mockkubeapiserver/tests/testdata/secrets_stringdata/manifest.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: default 5 | 6 | --- 7 | 8 | kind: Secret 9 | apiVersion: v1 10 | metadata: 11 | name: secret 12 | namespace: default 13 | type: Opaque 14 | stringData: 15 | foo: bar 16 | foo2: bar2 -------------------------------------------------------------------------------- /mockkubeapiserver/schemas/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: update-schema 2 | update-schema: 3 | curl https://raw.githubusercontent.com/kubernetes/kubernetes/master/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go | \ 4 | awk '/typed.YAMLObject/,/`)/' | sed 's/`)//g' | sed 's/var schemaYAML = typed.YAMLObject(`//g' > kubernetes_builtin_schema.yaml 5 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/global_without_kubectl.go: -------------------------------------------------------------------------------- 1 | //go:build without_direct_applier 2 | // +build without_direct_applier 3 | 4 | package applier 5 | 6 | import ( 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | var DefaultApplier = NewApplySetApplier(metav1.PatchOptions{}, metav1.DeleteOptions{}, ApplysetOptions{}) 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement Request 3 | about: Suggest an enhancement to the kubebuilder-declarative-pettern project 4 | labels: kind/feature 5 | 6 | --- 7 | 8 | 9 | **What would you like to be added**: 10 | 11 | **Why is this needed**: 12 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: c3cffa1a.example.org 12 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/crd/patches/cainjection_in_guestbooks.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: guestbooks.addons.example.org 8 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /examples/guestbook-operator/controllers/testdata/golden/patches/input.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.example.org/v1alpha1 2 | kind: Guestbook 3 | metadata: 4 | name: guestbook-sample 5 | namespace: default 6 | spec: 7 | channel: stable 8 | patches: 9 | - apiVersion: apps/v1 10 | kind: Deployment 11 | metadata: 12 | name: frontend 13 | spec: 14 | replicas: 5 -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | 7 | configMapGenerator: 8 | - files: 9 | - controller_manager_config.yaml 10 | name: manager-config 11 | apiVersion: kustomize.config.k8s.io/v1beta1 12 | kind: Kustomization 13 | images: 14 | - name: controller 15 | newName: controller 16 | newTag: latest 17 | -------------------------------------------------------------------------------- /mockkubeapiserver/tools/generate-typeinfo/README.md: -------------------------------------------------------------------------------- 1 | generate-typeinfo is a simple tool to generate the kubernetes_builtin_schema.meta.yaml from a kubernetes OpenAPI definition. 2 | 3 | The openapi definition should be saved as `openapi.json`, using a command like `kubectl get --raw /openapi/v2 > openapi.json`. 4 | 5 | The output path is currently hard-coded to `../../kubernetes_builtin_schema.meta.yaml`, so should be run from this directory. -------------------------------------------------------------------------------- /docs/releases/release-0.12.md: -------------------------------------------------------------------------------- 1 | # Release notes for 0.12 release 2 | 3 | Note: the 0.12 release is pending. This serves to accumulate breaking/important changes prior to release. 4 | 5 | * Name and Namespace are not longer exported by manifest.Object; use GetName() and GetNamespace() instead. 6 | This change allows setting the values. 7 | It also makes `declarative.Object` more similar to `unstructured.Unstructured`, which we model after. 8 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/default/manager_resource_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | resources: 12 | limits: 13 | cpu: 100m 14 | memory: 150Mi 15 | requests: 16 | cpu: 100m 17 | memory: 20Mi 18 | -------------------------------------------------------------------------------- /mockkubeapiserver/storage/memorystorage/schema.go: -------------------------------------------------------------------------------- 1 | package memorystorage 2 | 3 | import ( 4 | "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver/schemas" 5 | ) 6 | 7 | type mockSchema struct { 8 | builtin *schemas.Schema 9 | resources []*memoryResourceInfo 10 | } 11 | 12 | func (s *mockSchema) Init() error { 13 | schema, err := schemas.KubernetesBuiltInSchema() 14 | if err != nil { 15 | return err 16 | } 17 | s.builtin = schema 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/guestbook_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view guestbooks. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: guestbook-viewer-role 6 | rules: 7 | - apiGroups: 8 | - addons.example.org 9 | resources: 10 | - guestbooks 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - addons.example.org 17 | resources: 18 | - guestbooks/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /examples/guestbook-operator/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ 25 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/kustomize/disabled.go: -------------------------------------------------------------------------------- 1 | //go:build without_kustomize 2 | // +build without_kustomize 3 | 4 | package kustomize 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "sigs.k8s.io/kustomize/kyaml/filesys" 11 | ) 12 | 13 | // Run ignore the `kustomization.yaml` file and won't run `kustomize build`. 14 | func Run(_ context.Context, _ filesys.FileSystem, _ string) ([]byte, error) { 15 | return nil, fmt.Errorf("kustomize support is not compiled in (built with tag `without_kustomize`)") 16 | } 17 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /mockkubeapiserver/hooks.go: -------------------------------------------------------------------------------- 1 | package mockkubeapiserver 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Hook is the base interface implemented by a hook 8 | type Hook interface { 9 | } 10 | 11 | // HTTPOperation contains the details of an HTTP operation 12 | type HTTPOperation struct { 13 | Request *http.Request 14 | } 15 | 16 | // BeforeHTTPOperation is implemented by hooks that want to be called before every HTTP operation 17 | type BeforeHTTPOperation interface { 18 | BeforeHTTPOperation(op *HTTPOperation) 19 | } 20 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/crd/patches/webhook_in_guestbooks.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: guestbooks.addons.example.org 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /mockkubeapiserver/forked/README.md: -------------------------------------------------------------------------------- 1 | This directory contains code that we copy from various places in the 2 | kubernetes source code that are generally hard to import otherwise. 3 | 4 | The intent here is that we demonstrate the need & utility of exporting 5 | these functions in a stable location, and then if/when k/k exports 6 | them we can (reasonably) easily switch, because this is only 7 | test code. 8 | 9 | As far as possible, we try to keep the same function names and not 10 | change the code. We want to reunify in the long-term. -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/guestbook_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit guestbooks. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: guestbook-editor-role 6 | rules: 7 | - apiGroups: 8 | - addons.example.org 9 | resources: 10 | - guestbooks 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - addons.example.org 21 | resources: 22 | - guestbooks/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | The Kubernetes Template Project is released on an as-needed basis. The process is as follows: 4 | 5 | 1. An issue is proposing a new release with a changelog since the last release 6 | 1. All [OWNERS](OWNERS) must LGTM this release 7 | 1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` 8 | 1. The release issue is closed 9 | 1. An announcement email is sent to `kubernetes-dev@googlegroups.com` with the subject `[ANNOUNCE] kubernetes-template-project $VERSION is released` 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug encountered while using kubebuilder-declarative-pattern 4 | labels: kind/bug 5 | 6 | --- 7 | 8 | **What happened**: 9 | 10 | **What you expected to happen**: 11 | 12 | **How to reproduce it (as minimally and precisely as possible)**: 13 | 14 | **Anything else we need to know?**: 15 | 16 | **Environment**: 17 | - Kubernetes version (use `kubectl version`): 18 | - OS (e.g: `cat /etc/os-release`): 19 | - Kernel (e.g. `uname -a`): 20 | - Go version (e.g. `go version`): 21 | - Others: 22 | -------------------------------------------------------------------------------- /pkg/test/testreconciler/simpletest/testdata/channels/packages/simpletest/0.1.0/manifest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: foo 5 | labels: 6 | l1: v1 7 | data: 8 | k1: v1 9 | --- 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: mydeployment 14 | spec: 15 | replicas: 3 16 | selector: 17 | matchLabels: 18 | app: bar 19 | template: 20 | metadata: 21 | labels: 22 | app: bar 23 | spec: 24 | containers: 25 | - name: main 26 | image: registry.k8s.io/pause:3.9 27 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/direct/simple3/before.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | annotations: 6 | kubectl.kubernetes.io/last-applied-configuration: "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" 7 | 8 | --- 9 | 10 | kind: Namespace 11 | apiVersion: v1 12 | metadata: 13 | name: ns2 14 | annotations: 15 | kubectl.kubernetes.io/last-applied-configuration: "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" 16 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/before.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | annotations: 6 | kubectl.kubernetes.io/last-applied-configuration: "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" 7 | 8 | --- 9 | 10 | kind: Namespace 11 | apiVersion: v1 12 | metadata: 13 | name: ns2 14 | annotations: 15 | kubectl.kubernetes.io/last-applied-configuration: "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" 16 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/testdata/applylib/simple3/before.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: ns1 5 | annotations: 6 | kubectl.kubernetes.io/last-applied-configuration: "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" 7 | 8 | --- 9 | 10 | kind: Namespace 11 | apiVersion: v1 12 | metadata: 13 | name: ns2 14 | annotations: 15 | kubectl.kubernetes.io/last-applied-configuration: "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | **What this PR does / why we need it**: 7 | 8 | **Which issue(s) this PR fixes**: 9 | 13 | Fixes # 14 | 15 | **Special notes for your reviewer**: 16 | 17 | **Additional documentation**: 18 | 19 | -------------------------------------------------------------------------------- /applylib/applyset/testdata/testapplyset/expected.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | applyset.kubernetes.io/part-of: applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1 6 | kubernetes.io/metadata.name: test-applyset 7 | name: test-applyset 8 | spec: 9 | finalizers: 10 | - kubernetes 11 | status: 12 | phase: Active 13 | 14 | --- 15 | apiVersion: v1 16 | data: 17 | foo: bar 18 | kind: ConfigMap 19 | metadata: 20 | labels: 21 | applyset.kubernetes.io/part-of: applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1 22 | name: foo 23 | namespace: test-applyset 24 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes 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 | -------------------------------------------------------------------------------- /examples/guestbook-operator/PROJECT: -------------------------------------------------------------------------------- 1 | domain: example.org 2 | layout: 3 | - go.kubebuilder.io/v3 4 | - declarative.go.kubebuilder.io/v1 5 | plugins: 6 | declarative.go.kubebuilder.io/v1: 7 | resources: 8 | - domain: example.org 9 | group: addons 10 | kind: Guestbook 11 | version: v1alpha1 12 | projectName: guestbook-operator 13 | repo: example.org/guestbook-operator 14 | resources: 15 | - api: 16 | crdVersion: v1 17 | namespaced: true 18 | controller: true 19 | domain: example.org 20 | group: addons 21 | kind: Guestbook 22 | path: example.org/guestbook-operator/api/v1alpha1 23 | version: v1alpha1 24 | version: "3" 25 | -------------------------------------------------------------------------------- /pkg/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 pkg 18 | -------------------------------------------------------------------------------- /commonclient/metrics_cr11.go: -------------------------------------------------------------------------------- 1 | //go:build controllerruntime_11 || controllerruntime_12 || controllerruntime_13 || controllerruntime_14 || controllerruntime_15 2 | 3 | package commonclient 4 | 5 | import ( 6 | "fmt" 7 | 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | ) 10 | 11 | // SetMetricsBindAddress sets the metrics address on options independent of 12 | // manager options version 13 | func SetMetricsBindAddress(options *ctrl.Options, bindAddress string) error { 14 | if options == nil { 15 | return fmt.Errorf("unable to set metrics bind address on non-existent manager options") 16 | } 17 | options.MetricsBindAddress = bindAddress 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /commonclient/metrics_cr16.go: -------------------------------------------------------------------------------- 1 | //go:build !(controllerruntime_11 || controllerruntime_12 || controllerruntime_13 || controllerruntime_14 || controllerruntime_15) 2 | 3 | package commonclient 4 | 5 | import ( 6 | "fmt" 7 | 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | ) 10 | 11 | // SetMetricsBindAddress sets the metrics address on options independent of 12 | // manager options version 13 | func SetMetricsBindAddress(options *ctrl.Options, bindAddress string) error { 14 | if options == nil { 15 | return fmt.Errorf("unable to set metrics bind address on non-existent manager options") 16 | } 17 | options.Metrics.BindAddress = bindAddress 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /examples/guestbook-operator/hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes 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 | */ -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 pkg 18 | -------------------------------------------------------------------------------- /.github/workflows/tag-release.yaml: -------------------------------------------------------------------------------- 1 | name: 'Tag Release' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - 'release-*' 8 | paths: 9 | - version/* 10 | 11 | jobs: 12 | tag-release: 13 | if: ${{ github.repository == 'kubernetes-sigs/kubebuilder-declarative-pattern' }} 14 | runs-on: ubuntu-24.04 15 | 16 | permissions: 17 | contents: write 18 | 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 21 | - run: /usr/bin/git config --global user.email actions@github.com 22 | - run: /usr/bin/git config --global user.name 'GitHub Actions Release Tagger' 23 | - run: dev/bots/projectbots/tag-release 24 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /mockkubeapiserver/storage/clock.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "time" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | type Clock interface { 10 | Now() metav1.Time 11 | } 12 | 13 | var _ Clock = &RealClock{} 14 | 15 | type RealClock struct { 16 | } 17 | 18 | func (c *RealClock) Now() metav1.Time { 19 | return metav1.Now() 20 | } 21 | 22 | var _ Clock = &TestClock{} 23 | 24 | type TestClock struct { 25 | t time.Time 26 | } 27 | 28 | func NewTestClock() *TestClock { 29 | t := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) 30 | return &TestClock{t: t} 31 | } 32 | 33 | func (c *TestClock) Now() metav1.Time { 34 | t := c.t 35 | c.t = t.Add(time.Second) 36 | return metav1.NewTime(t) 37 | } 38 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1beta1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/annotations.go: -------------------------------------------------------------------------------- 1 | package declarative 2 | 3 | import ( 4 | "context" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/log" 7 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 8 | ) 9 | 10 | // AddAnnotations returns an ObjectTransform that adds annotations to all the objects 11 | func AddAnnotations(annotations map[string]string) ObjectTransform { 12 | return func(ctx context.Context, o DeclarativeObject, manifest *manifest.Objects) error { 13 | log := log.Log 14 | for _, o := range manifest.Items { 15 | log.WithValues("object", o).WithValues("annotations", annotations).V(1).Info("add annotations to object") 16 | o.AddAnnotations(annotations) 17 | } 18 | 19 | return nil 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/status/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 | /* 18 | The status package provides implementations for discovering and updating 19 | the CommonStatus on an Addon. 20 | */ 21 | package status 22 | -------------------------------------------------------------------------------- /dev/update-golden: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # CI script to run tests in the mode where they write the golden output files 4 | set -o errexit 5 | set -o nounset 6 | set -o pipefail 7 | 8 | # cd to the repo root 9 | REPO_ROOT=$(git rev-parse --show-toplevel) 10 | cd "${REPO_ROOT}" 11 | 12 | # Ensure we run with a known version of kubectl 13 | if [[ ! -f "bin/kubectl" ]]; then 14 | echo "Downloading kubectl to bin/kubectl" 15 | mkdir -p bin/ 16 | curl -L -o bin/kubectl https://dl.k8s.io/release/v1.32.2/bin/linux/amd64/kubectl 17 | fi 18 | chmod +x bin/kubectl 19 | export PATH="${REPO_ROOT}/bin:$PATH" 20 | echo "kubectl version is $(kubectl version --client)" 21 | 22 | WRITE_GOLDEN_OUTPUT=1 go test -count=1 -v ./... 23 | 24 | cd "${REPO_ROOT}/mockkubeapiserver" 25 | WRITE_GOLDEN_OUTPUT=1 go test -count=1 -v ./... 26 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/loaders/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 | /* 18 | The loaders package implements loading of raw kubernetes manifests based off 19 | of the CommonSpec of an Addon object. 20 | */ 21 | package loaders 22 | -------------------------------------------------------------------------------- /pkg/test/testreconciler/simpletest/with_kubectl_test.go: -------------------------------------------------------------------------------- 1 | //go:build !without_exec_applier || !without_direct_applier 2 | // +build !without_exec_applier !without_direct_applier 3 | 4 | package simpletest 5 | 6 | import ( 7 | "testing" 8 | 9 | "sigs.k8s.io/kubebuilder-declarative-pattern/ktest/testharness" 10 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/status" 11 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/applier" 12 | ) 13 | 14 | func TestDirectSimpleReconciler(t *testing.T) { 15 | Key := "direct" 16 | t.Run(Key, func(t *testing.T) { 17 | testharness.RunGoldenTests(t, "testdata/reconcile/"+Key+"/", func(h *testharness.Harness, testdir string) { 18 | testSimpleReconciler(h, testdir, applier.NewDirectApplier(), status.NewBasic(nil)) 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /hack/ci/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2019 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | REPO_ROOT=$(dirname "${BASH_SOURCE}")/../.. 22 | cd "${REPO_ROOT}/hack" 23 | 24 | go run smoketest.go 25 | -------------------------------------------------------------------------------- /pkg/patterns/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 | /* 18 | The patterns package contains tools for building operators with 19 | http://sigs.k8s.io/controller-runtime/ 20 | 21 | See the subpackages for specific pattern implementations. 22 | */ 23 | package patterns 24 | -------------------------------------------------------------------------------- /mockkubeapiserver/storage/uid.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | 6 | "k8s.io/apimachinery/pkg/types" 7 | "k8s.io/apimachinery/pkg/util/uuid" 8 | ) 9 | 10 | type UIDGenerator interface { 11 | NewUID() types.UID 12 | } 13 | 14 | var _ UIDGenerator = &RandomUIDGenerator{} 15 | 16 | type RandomUIDGenerator struct { 17 | } 18 | 19 | func (c *RandomUIDGenerator) NewUID() types.UID { 20 | uid := uuid.NewUUID() 21 | return uid 22 | } 23 | 24 | var _ UIDGenerator = &TestUIDGenerator{} 25 | 26 | type TestUIDGenerator struct { 27 | next int64 28 | } 29 | 30 | func NewTestUIDGenerator() *TestUIDGenerator { 31 | return &TestUIDGenerator{next: 0x1} 32 | } 33 | 34 | func (c *TestUIDGenerator) NewUID() types.UID { 35 | v := c.next 36 | c.next++ 37 | s := fmt.Sprintf("%012x", v) 38 | s = "00000000-0000-0000-0000-" + s 39 | return types.UID(s) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/test/testreconciler/simpletest/without_kubectl_test.go: -------------------------------------------------------------------------------- 1 | package simpletest 2 | 3 | import ( 4 | "testing" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "sigs.k8s.io/kubebuilder-declarative-pattern/ktest/testharness" 8 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/status" 9 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/applier" 10 | ) 11 | 12 | func TestSSASimpleReconciler(t *testing.T) { 13 | Key := "ssa" 14 | a := applier.NewApplySetApplier( 15 | metav1.PatchOptions{FieldManager: "kdp-test"}, metav1.DeleteOptions{}, applier.ApplysetOptions{}) 16 | t.Run(Key, func(t *testing.T) { 17 | testharness.RunGoldenTests(t, "testdata/reconcile/"+Key+"/", func(h *testharness.Harness, testdir string) { 18 | testSimpleReconciler(h, testdir, a, status.NewKstatusCheck(nil, nil)) 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Team to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | DirectXMan12 14 | droot 15 | grodrigues3 16 | # johnsonj # TODO: uncomment once a member, ref: https://github.com/kubernetes/org/issues/418#issuecomment-465357261 17 | justinsb 18 | Liujingfang1 19 | mengqiy 20 | monopole 21 | pmorie 22 | pwittrock 23 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 | /* 18 | The declarative package contains tools and a controller compatible with 19 | http://sigs.k8s.io/controller-runtime to manage a Kubernetes deployment 20 | based off of a instance of a CustomResource in the cluster. 21 | */ 22 | package declarative 23 | -------------------------------------------------------------------------------- /applylib/forked/k8s.io/apimachinery/pkg/util/sets/empty.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 sets 18 | 19 | // Empty is public since it is used by some internal API objects for conversions between external 20 | // string arrays and internal sets, and conversion logic requires public types today. 21 | type Empty struct{} 22 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /commonclient/restmapper_cr11.go: -------------------------------------------------------------------------------- 1 | //go:build controllerruntime_11 || controllerruntime_12 || controllerruntime_13 || controllerruntime_14 2 | 3 | package commonclient 4 | 5 | import ( 6 | "net/http" 7 | 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/client-go/rest" 10 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 11 | ) 12 | 13 | // NewDiscoveryRESTMapper is a version-independent wrapper around creating a meta.RESTMapper 14 | // It calls NewDynamicRESTMapper as of kubebuilder-declarative-pattern 0.17. 15 | // Deprecated: prefer NewDynamicRESTMapper 16 | func NewDiscoveryRESTMapper(c *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) { 17 | return NewDynamicRESTMapper(c) 18 | } 19 | 20 | // NewDynamicRESTMapper is a version-independent wrapper around apiutil.NewDynamicRESTMapper 21 | func NewDynamicRESTMapper(c *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) { 22 | return apiutil.NewDynamicRESTMapper(c) 23 | } 24 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--health-probe-bind-address=:8081" 25 | - "--metrics-bind-address=127.0.0.1:8080" 26 | - "--leader-elect" 27 | -------------------------------------------------------------------------------- /applylib/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 applylib 18 | 19 | // Package applylib implements a helper library for applying a set of objects to a cluster. 20 | // This functionality is needed across multiple projects, and we are pursuing a "copy-and-paste" 21 | // reuse strategy while we decide what functionality is needed and where this library should live. 22 | -------------------------------------------------------------------------------- /examples/guestbook-operator/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.23.5 as builder 3 | 4 | # Copy in the go src 5 | WORKDIR /workspace 6 | # Copy the Go Modules manifests 7 | COPY go.mod go.mod 8 | COPY go.sum go.sum 9 | # cache deps before building and copying source so that we don't need to re-download as much 10 | # and so that source changes don't invalidate our downloaded layer 11 | RUN go mod download 12 | COPY vendor/ vendor/ 13 | 14 | COPY main.go main.go 15 | COPY api/ api/ 16 | COPY controllers/ controllers/ 17 | COPY channels/ channels/ 18 | RUN chmod -R a+rx channels/ 19 | 20 | # Build 21 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go 22 | 23 | # Copy the operator and dependencies into a thin image 24 | FROM gcr.io/distroless/static:latest 25 | WORKDIR / 26 | COPY --from=builder /workspace/manager . 27 | COPY --from=builder /workspace/channels/ channels/ 28 | 29 | USER 65532:65532 30 | 31 | ENTRYPOINT ["./manager"] 32 | -------------------------------------------------------------------------------- /commonclient/restmapper_cr15.go: -------------------------------------------------------------------------------- 1 | //go:build !(controllerruntime_11 || controllerruntime_12 || controllerruntime_13 || controllerruntime_14) 2 | 3 | package commonclient 4 | 5 | import ( 6 | "net/http" 7 | 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/client-go/rest" 10 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 11 | ) 12 | 13 | // NewDiscoveryRESTMapper is a version-independent wrapper around creating a meta.RESTMapper 14 | // It calls NewDynamicRESTMapper as of kubebuilder-declarative-pattern 0.17. 15 | // Deprecated: prefer NewDynamicRESTMapper 16 | func NewDiscoveryRESTMapper(c *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) { 17 | return NewDynamicRESTMapper(c, httpClient) 18 | } 19 | 20 | // NewDynamicRESTMapper is a version-independent wrapper around apiutil.NewDynamicRESTMapper 21 | func NewDynamicRESTMapper(c *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) { 22 | return apiutil.NewDynamicRESTMapper(c, httpClient) 23 | } 24 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - services 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - '*' 21 | resources: 22 | - '*' 23 | verbs: 24 | - list 25 | - apiGroups: 26 | - addons.example.org 27 | resources: 28 | - guestbooks 29 | verbs: 30 | - create 31 | - delete 32 | - get 33 | - list 34 | - patch 35 | - update 36 | - watch 37 | - apiGroups: 38 | - addons.example.org 39 | resources: 40 | - guestbooks/status 41 | verbs: 42 | - get 43 | - patch 44 | - update 45 | - apiGroups: 46 | - apps 47 | - extensions 48 | resources: 49 | - deployments 50 | verbs: 51 | - create 52 | - delete 53 | - get 54 | - list 55 | - patch 56 | - update 57 | - watch 58 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 kubebuilderdeclarativepattern provides tools to construct 18 | // declarative Kubernetes operators to manage the lifecycle of a 19 | // deployment. 20 | // 21 | // # Getting Started 22 | // 23 | // Follow the project documentation for a getting started guide: 24 | // - https://github.com/kubernetes-sigs/kubebuilder-declarative-pattern/tree/master/docs 25 | package kubebuilderdeclarativepattern 26 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/addons.example.org_guestbooks.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_guestbooks.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_guestbooks.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for breaking changes 4 | apiVersion: cert-manager.io/v1alpha2 5 | kind: Issuer 6 | metadata: 7 | name: selfsigned-issuer 8 | namespace: system 9 | spec: 10 | selfSigned: {} 11 | --- 12 | apiVersion: cert-manager.io/v1alpha2 13 | kind: Certificate 14 | metadata: 15 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 16 | namespace: system 17 | spec: 18 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 19 | dnsNames: 20 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 22 | issuerRef: 23 | kind: Issuer 24 | name: selfsigned-issuer 25 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 26 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/apis/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 | /* 18 | The v1alpha1 API provides a set of common fields for addon objects. 19 | 20 | What is the purpose of this API? 21 | 22 | The API provides a common set of spec and status fields that are required 23 | to manage addons consistently. 24 | 25 | How stable is this API? 26 | 27 | This is an evolving API and will change without bumping the version number 28 | until it is promoted to a beta API. 29 | */ 30 | package v1alpha1 31 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/loaders/git_test.go: -------------------------------------------------------------------------------- 1 | package loaders 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseGitURL(t *testing.T) { 8 | tests := []struct { 9 | rawURL string 10 | baseURL string 11 | subDir string 12 | }{ 13 | { 14 | rawURL: "https://github.com/testRepository.git", 15 | baseURL: "https://github.com/testRepository.git", 16 | subDir: "", 17 | }, 18 | { 19 | rawURL: "git::https://github.com/testRepository.git", 20 | baseURL: "https://github.com/testRepository.git", 21 | subDir: "", 22 | }, 23 | { 24 | rawURL: "git::https://github.com/testRepository.git//subDir/package", 25 | baseURL: "https://github.com/testRepository.git", 26 | subDir: "subDir/package", 27 | }, 28 | } 29 | 30 | for _, tt := range tests { 31 | gitRepo := parseGitURL(tt.rawURL) 32 | if gitRepo.baseURL != tt.baseURL { 33 | t.Errorf("Expected base url: %v, got %v", tt.baseURL, gitRepo.baseURL) 34 | } 35 | 36 | if gitRepo.subDir != tt.subDir { 37 | t.Errorf("Expected base url: %v, got %v", tt.subDir, gitRepo.subDir) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ktest/testharness/harness.go: -------------------------------------------------------------------------------- 1 | package testharness 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | type Harness struct { 9 | *testing.T 10 | } 11 | 12 | func New(t *testing.T) *Harness { 13 | h := &Harness{T: t} 14 | t.Cleanup(h.Cleanup) 15 | return h 16 | } 17 | 18 | func (h *Harness) Cleanup() { 19 | 20 | } 21 | 22 | func (h *Harness) TempDir() string { 23 | tmpdir, err := os.MkdirTemp("", "test") 24 | if err != nil { 25 | h.Fatalf("failed to make temp directory: %v", err) 26 | } 27 | h.T.Cleanup(func() { 28 | if err := os.RemoveAll(tmpdir); err != nil { 29 | h.Errorf("error cleaning up temp directory %q: %v", tmpdir, err) 30 | } 31 | }) 32 | return tmpdir 33 | } 34 | 35 | func (h *Harness) MustReadFile(p string) []byte { 36 | b, err := os.ReadFile(p) 37 | if err != nil { 38 | h.Fatalf("error from ReadFile(%q): %v", p, err) 39 | } 40 | return b 41 | } 42 | 43 | func (h *Harness) FileExists(p string) bool { 44 | _, err := os.Stat(p) 45 | if err == nil { 46 | return true 47 | } 48 | if !os.IsNotExist(err) { 49 | h.Fatalf("error from os.Stat(%q): %v", p, err) 50 | } 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/ownerref.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 declarative 18 | 19 | import ( 20 | "context" 21 | 22 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 23 | ) 24 | 25 | // SourceAsOwner is a OwnerSelector that selects the source DeclarativeObject as the owner 26 | func SourceAsOwner(ctx context.Context, src DeclarativeObject, obj manifest.Object, objs manifest.Objects) (DeclarativeObject, error) { 27 | return src, nil 28 | } 29 | 30 | var _ OwnerSelector = SourceAsOwner 31 | -------------------------------------------------------------------------------- /docs/project-management/deprecations.md: -------------------------------------------------------------------------------- 1 | # Deprecations and new functionality 2 | 3 | As kubernetes and the controller ecosystem continues to evolve, 4 | we will sometimes want to add new functionality or behaviour to kdp. 5 | 6 | However, as a library, we do not want to break the functionality of controllers that upgrade their kdp version, 7 | particularly as the coupling to kubernetes versions means that users must upgrade periodically. 8 | 9 | We will aim to follow these rules therefore: 10 | 11 | * Do not remove functionality or fundamentally change the behaviour in a way that will break users. 12 | 13 | * Bug fixes are good, but we should think about existing users even for bug fixes. 14 | 15 | * Consider introducing a new method or type instead of breaking existing functionality. 16 | 17 | * Use the go `// Deprecated:` [convention](https://go.dev/wiki/Deprecated) to discourage usage of "old" methods, fields or types. 18 | 19 | * Prefer errors at compilation time to errors at run time. Reasonable code changes are acceptable (e.g. adding a context method). 20 | Crashing at run time because a field is not populated is not acceptable. 21 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/kustomize/enabled.go: -------------------------------------------------------------------------------- 1 | //go:build !without_kustomize 2 | // +build !without_kustomize 3 | 4 | package kustomize 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "sigs.k8s.io/controller-runtime/pkg/log" 11 | "sigs.k8s.io/kustomize/api/krusty" 12 | "sigs.k8s.io/kustomize/kyaml/filesys" 13 | ) 14 | 15 | // Run calls the kustomize/api library to run `kustomize build`. This method is differentiated by go build 16 | // tag `without_kustomize` 17 | func Run(ctx context.Context, fs filesys.FileSystem, manifestPath string) ([]byte, error) { 18 | log := log.FromContext(ctx) 19 | log.WithValues("manifestPath", manifestPath).Info("running kustomize") 20 | // run kustomize to create final manifest 21 | opts := krusty.MakeDefaultOptions() 22 | kustomizer := krusty.MakeKustomizer(opts) 23 | m, err := kustomizer.Run(fs, manifestPath) 24 | if err != nil { 25 | return nil, fmt.Errorf("error running kustomize: %v", err) 26 | } 27 | 28 | manifestYaml, err := m.AsYaml() 29 | if err != nil { 30 | return nil, fmt.Errorf("error converting kustomize output to yaml: %v", err) 31 | } 32 | return manifestYaml, nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/restmapper/controllerrestmapper_cr11.go: -------------------------------------------------------------------------------- 1 | //go:build controllerruntime_11 || controllerruntime_12 || controllerruntime_13 || controllerruntime_14 2 | 3 | package restmapper 4 | 5 | import ( 6 | "fmt" 7 | 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/client-go/discovery" 10 | "k8s.io/client-go/rest" 11 | ) 12 | 13 | // NewControllerRESTMapper is the constructor for a ControllerRESTMapper. 14 | func NewControllerRESTMapper(restConfig *rest.Config) (meta.RESTMapper, error) { 15 | httpClient, err := rest.HTTPClientFor(restConfig) 16 | if err != nil { 17 | return nil, fmt.Errorf("error from HTTPClientFor: %w", err) 18 | } 19 | 20 | discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restConfig, httpClient) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return &ControllerRESTMapper{ 26 | uncached: discoveryClient, 27 | cache: newCache(), 28 | }, nil 29 | } 30 | 31 | // NewForTest creates a ControllerRESTMapper, but is intended to be a common interface for use by tests. 32 | func NewForTest(cfg *rest.Config) (meta.RESTMapper, error) { 33 | return NewControllerRESTMapper(cfg) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/apis/v1alpha1/conditions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 | // StatusConditions defines the fields to support status.conditions. 24 | type StatusConditions struct { 25 | // Conditions follows the API specification "Conditions" properties. 26 | // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties 27 | Conditions []metav1.Condition `json:"conditions,omitempty"` 28 | } 29 | -------------------------------------------------------------------------------- /pkg/restmapper/controllerrestmapper_cr15.go: -------------------------------------------------------------------------------- 1 | //go:build !(controllerruntime_11 || controllerruntime_12 || controllerruntime_13 || controllerruntime_14) 2 | 3 | package restmapper 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | 9 | "k8s.io/apimachinery/pkg/api/meta" 10 | "k8s.io/client-go/discovery" 11 | "k8s.io/client-go/rest" 12 | ) 13 | 14 | // NewControllerRESTMapper is the constructor for a ControllerRESTMapper 15 | func NewControllerRESTMapper(cfg *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) { 16 | discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(cfg, httpClient) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return &ControllerRESTMapper{ 22 | uncached: discoveryClient, 23 | cache: newCache(), 24 | }, nil 25 | } 26 | 27 | // NewForTest creates a ControllerRESTMapper, but is intended to be a common interface for use by tests. 28 | func NewForTest(restConfig *rest.Config) (meta.RESTMapper, error) { 29 | client, err := rest.HTTPClientFor(restConfig) 30 | if err != nil { 31 | return nil, fmt.Errorf("error from rest.HTTPClientFor: %w", err) 32 | } 33 | return NewControllerRESTMapper(restConfig, client) 34 | } 35 | -------------------------------------------------------------------------------- /dev/ci/presubmits/verify-goimports: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2025 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | # CI script to keep all files formatted with goimporrts 19 | 20 | set -o errexit 21 | set -o nounset 22 | set -o pipefail 23 | 24 | # cd to the repo root 25 | REPO_ROOT=$(git rev-parse --show-toplevel) 26 | cd "${REPO_ROOT}" 27 | 28 | files=$(go run golang.org/x/tools/cmd/goimports -format-only -l .) 29 | # Sadly goimports doesn't use exit codes 30 | if [[ -n "${files}" ]]; then 31 | echo "::error ::goimports should be run on these files:" 32 | echo "${files}" 33 | exit 1 34 | fi 35 | -------------------------------------------------------------------------------- /dev/ci/presubmits/verify-gomod: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2025 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | # CI script to keep all our go.mod/go.sum updated 19 | 20 | set -o errexit 21 | set -o nounset 22 | set -o pipefail 23 | 24 | # cd to the repo root 25 | REPO_ROOT=$(git rev-parse --show-toplevel) 26 | cd "${REPO_ROOT}" 27 | 28 | dev/format-gomod 29 | 30 | changes=$(git status --porcelain) 31 | if [[ -n "${changes}" ]]; then 32 | echo "::error Changes detected from dev/format-gomod:" 33 | echo "::error (You may need to run go clean -cache -modcache)" 34 | git diff | head -n60 35 | echo "${changes}" 36 | exit 1 37 | fi 38 | -------------------------------------------------------------------------------- /mockkubeapiserver/apiversions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 | package mockkubeapiserver 17 | 18 | import ( 19 | "context" 20 | 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // apiVersionsRequest is a wrapper around a request for core api version, such as GET /api 25 | type apiVersionsRequest struct { 26 | baseRequest 27 | } 28 | 29 | // Run serves the GET /api endpoint 30 | func (r *apiVersionsRequest) Run(ctx context.Context, s *MockKubeAPIServer) error { 31 | versions := &metav1.APIVersions{} 32 | versions.Kind = "APIVersions" 33 | versions.Versions = []string{"v1"} 34 | 35 | return r.writeResponse(versions) 36 | } 37 | -------------------------------------------------------------------------------- /commonclient/factory_cr15.go: -------------------------------------------------------------------------------- 1 | //go:build !(controllerruntime_11 || controllerruntime_12 || controllerruntime_13 || controllerruntime_14) 2 | 3 | package commonclient 4 | 5 | import ( 6 | "net/http" 7 | 8 | "sigs.k8s.io/controller-runtime/pkg/cache" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | "sigs.k8s.io/controller-runtime/pkg/cluster" 11 | "sigs.k8s.io/controller-runtime/pkg/handler" 12 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 13 | "sigs.k8s.io/controller-runtime/pkg/source" 14 | ) 15 | 16 | // SourceKind is a version-indendenent abstraction over calling source.Kind 17 | func SourceKind(cache cache.Cache, obj client.Object) source.Source { 18 | return source.Kind(cache, obj, &handler.TypedEnqueueRequestForObject[client.Object]{}) 19 | } 20 | 21 | // SourceKind is a version-indendenent abstraction over calling source.Kind 22 | func SourceKindWithHandler(cache cache.Cache, obj client.Object, handler handler.TypedEventHandler[client.Object, reconcile.Request]) source.Source { 23 | return source.Kind(cache, obj, handler) 24 | } 25 | 26 | // GetHTTPClient returns the http.Client associated with the Cluster 27 | func GetHTTPClient(c cluster.Cluster) (*http.Client, error) { 28 | return c.GetHTTPClient(), nil 29 | } 30 | 31 | type EventHandler = handler.EventHandler 32 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/statusinfo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package declarative 16 | 17 | import "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 18 | 19 | type StatusInfo struct { 20 | Subject DeclarativeObject 21 | 22 | // Manifest contains the set of desired-state for objects that we applied (or tried to). 23 | Manifest *manifest.Objects 24 | 25 | LiveObjects LiveObjectReader 26 | KnownError KnownErrorCode 27 | Err error 28 | } 29 | 30 | type KnownErrorCode string 31 | 32 | const ( 33 | KnownErrorApplyFailed KnownErrorCode = "FailedToApply" 34 | KnownErrorVersionCheckFailed KnownErrorCode = "VersionCheckFailed" 35 | ) 36 | -------------------------------------------------------------------------------- /dev/format-gomod: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2022 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | # CI script to keep all our go.mod/go.sum updated 19 | 20 | set -o errexit 21 | set -o nounset 22 | set -o pipefail 23 | 24 | # cd to the repo root 25 | REPO_ROOT=$(git rev-parse --show-toplevel) 26 | cd "${REPO_ROOT}" 27 | 28 | # Print the go version for diagnostics 29 | go version 30 | 31 | # Updates the go.mod in each go module 32 | # Note: go work sync is not entirely deterministic; I had to clear my modcache with `go clean -cache -modcache` 33 | go work sync 34 | 35 | # Tidy each individual go.mod 36 | for gomod_file in $(find "${REPO_ROOT}" -name "go.mod"); do 37 | dir=$(dirname ${gomod_file}) 38 | cd "${dir}" 39 | go mod tidy 40 | done -------------------------------------------------------------------------------- /mockkubeapiserver/storage/interface.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | // Storage is a pluggable store of objects 11 | type Storage interface { 12 | // FindResource returns the ResourceInfo for a group-resource. 13 | // The ResourceInfo allows CRUD operations on that resource. 14 | FindResource(gr schema.GroupResource) ResourceInfo 15 | 16 | // AllResources returns the metadata for all resources. 17 | AllResources() []metav1.APIResource 18 | 19 | // AddObject can be called to "sideload" an object, useful for testing. 20 | AddObject(obj *unstructured.Unstructured) error 21 | 22 | // RegisterType is used to register a built-in type. 23 | RegisterType(gvk schema.GroupVersionKind, resource string, scope meta.RESTScope) 24 | 25 | // AddStorageHook registers a hook, that will be called whenever any object changes. 26 | AddStorageHook(hook Hook) 27 | 28 | // UpdateCRD should be called whenever a CRD changes (likely by a hook). 29 | UpdateCRD(ev *WatchEvent) error 30 | } 31 | 32 | // WatchCallback is the function signature for the callback function when objects are changed. 33 | type WatchCallback func(ev *WatchEvent) error 34 | -------------------------------------------------------------------------------- /applylib/applyset/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 applyset 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | ) 24 | 25 | // ApplyableObject is implemented by objects that can be applied to the cluster. 26 | // We don't need much, so this might allow for more efficient implementations in future. 27 | type ApplyableObject interface { 28 | // GroupVersionKind returns the GroupVersionKind structure describing the type of the object 29 | GroupVersionKind() schema.GroupVersionKind 30 | // GetNamespace returns the namespace of the object 31 | GetNamespace() string 32 | // GetName returns the name of the object 33 | GetName() string 34 | 35 | // The object should implement json marshalling 36 | json.Marshaler 37 | } 38 | -------------------------------------------------------------------------------- /hack/ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2019 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | CI_ROOT=$(dirname "${BASH_SOURCE}") 22 | REPO_ROOT=$(dirname "${BASH_SOURCE}")/../.. 23 | 24 | source "${CI_ROOT}/fetch_kubebuilder_release_bin.sh" 25 | 26 | cd "${REPO_ROOT}" 27 | # Make sure REPO_ROOT is an absolute path 28 | REPO_ROOT=$(pwd) 29 | 30 | # Ensure we run with a known version of kubectl 31 | if [[ ! -f "bin/kubectl" ]]; then 32 | echo "Downloading kubectl to bin/kubectl" 33 | mkdir -p bin/ 34 | curl -L -o bin/kubectl https://dl.k8s.io/release/v1.32.2/bin/linux/amd64/kubectl 35 | fi 36 | chmod +x bin/kubectl 37 | export PATH="${REPO_ROOT}/bin:$PATH" 38 | echo "kubectl version is $(kubectl version --client)" 39 | 40 | dev/test 41 | -------------------------------------------------------------------------------- /examples/guestbook-operator/api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes 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 API Schema definitions for the addons v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=addons.example.org 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "addons.example.org", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /mockkubeapiserver/openapi_request.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 mockkubeapiserver 18 | 19 | import ( 20 | "context" 21 | 22 | "k8s.io/klog/v2" 23 | ) 24 | 25 | // openapiRequest is a request to patch a single resource 26 | type openapiRequest struct { 27 | resourceRequestBase 28 | } 29 | 30 | // Run serves the http request 31 | func (req *openapiRequest) Run(ctx context.Context, s *MockKubeAPIServer) error { 32 | klog.Warningf("returning empty information for openapi/v2 request") 33 | 34 | b := []byte{} 35 | w := req.baseRequest.w 36 | w.Header().Add("Content-Type", "application/com.github.proto-openapi.spec.v2") 37 | w.Header().Add("Cache-Control", "no-cache, private") 38 | 39 | if _, err := w.Write(b); err != nil { 40 | // Too late to send error response 41 | klog.Warningf("error writing http response: %v", err) 42 | return nil 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | spec: 25 | securityContext: 26 | runAsNonRoot: true 27 | containers: 28 | - command: 29 | - /manager 30 | args: 31 | - --leader-elect 32 | image: controller:latest 33 | name: manager 34 | securityContext: 35 | allowPrivilegeEscalation: false 36 | livenessProbe: 37 | httpGet: 38 | path: /healthz 39 | port: 8081 40 | initialDelaySeconds: 15 41 | periodSeconds: 20 42 | readinessProbe: 43 | httpGet: 44 | path: /readyz 45 | port: 8081 46 | initialDelaySeconds: 5 47 | periodSeconds: 10 48 | resources: 49 | limits: 50 | cpu: 100m 51 | memory: 30Mi 52 | requests: 53 | cpu: 100m 54 | memory: 20Mi 55 | serviceAccountName: controller-manager 56 | terminationGracePeriodSeconds: 10 57 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | 'on': 4 | - push 5 | - pull_request 6 | 7 | env: 8 | GOPROXY: https://proxy.golang.org 9 | 10 | jobs: 11 | verify-goimports: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version-file: 'go.mod' 20 | 21 | - name: verify-goimports 22 | run: dev/ci/presubmits/verify-goimports 23 | 24 | 25 | verify-gomod: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | 30 | - name: Set up go 31 | uses: actions/setup-go@v5 32 | with: 33 | go-version-file: 'go.mod' 34 | 35 | - name: verify-gomod 36 | run: dev/ci/presubmits/verify-gomod 37 | 38 | kind-e2e: 39 | name: Test with a Kind cluster 40 | runs-on: [ubuntu-latest] 41 | steps: 42 | - uses: actions/checkout@v1 43 | - uses: actions/setup-go@v5 44 | with: 45 | go-version-file: 'go.mod' 46 | - name: Install latest version of Kind 47 | run: | 48 | go get sigs.k8s.io/kind 49 | - name: Create Kind cluster 50 | run: | 51 | kind create cluster --config .github/workflows/kind-config.yaml 52 | - name: Run some sanity checks 53 | # kubectl is already installed on the Github Ubuntu worker 54 | run: | 55 | kubectl get nodes -o wide 56 | kubectl get pods --all-namespaces -o wide 57 | kubectl get services --all-namespaces -o wide 58 | -------------------------------------------------------------------------------- /mockkubeapiserver/storage/memorystorage/resourceinfo.go: -------------------------------------------------------------------------------- 1 | package memorystorage 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver/storage" 7 | "sigs.k8s.io/structured-merge-diff/v4/typed" 8 | ) 9 | 10 | type memoryResourceInfo struct { 11 | api metav1.APIResource 12 | gvr schema.GroupVersionResource 13 | gvk schema.GroupVersionKind 14 | listGVK schema.GroupVersionKind 15 | 16 | parseableType *typed.ParseableType 17 | 18 | parent *MemoryStorage 19 | 20 | storage *resourceStorage 21 | } 22 | 23 | var _ storage.ResourceInfo = &memoryResourceInfo{} 24 | 25 | func (r *memoryResourceInfo) GVK() schema.GroupVersionKind { 26 | return r.gvk 27 | } 28 | 29 | func (r *memoryResourceInfo) ListGVK() schema.GroupVersionKind { 30 | return r.listGVK 31 | } 32 | 33 | func (r *memoryResourceInfo) ParseableType() *typed.ParseableType { 34 | return r.parseableType 35 | } 36 | 37 | func (r *memoryResourceInfo) SetsGeneration() bool { 38 | // Not all resources support metadata.generation; it looks like only those with status do (?) 39 | // For now, exclude some well-known types that do not set metadata.generation. 40 | switch r.gvk.GroupKind() { 41 | case schema.GroupKind{Group: "", Kind: "ConfigMap"}: 42 | return false 43 | case schema.GroupKind{Group: "", Kind: "Secret"}: 44 | return false 45 | case schema.GroupKind{Group: "", Kind: "Namespace"}: 46 | return false 47 | 48 | default: 49 | return true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/patterns/addon/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 | /* 18 | The addon package contains tools opinionated for managing Kubernetes cluster addons. 19 | The declarative.DeclarativeObject must be castable to a addonsv1alpha1.CommonObject 20 | in order for this pattern to be used. 21 | 22 | What is an Addon Object? 23 | 24 | An Addon Object is an instance of a type defined as a CustomResourceDefinition that 25 | implements the addonsv1alpha1.CommonObject interface. The object represents the intent 26 | to deploy an instance of a specific Addon in the cluster. This pattern manages a 27 | Kubernetes deployment for the specific addon based on the Addon Object. 28 | 29 | # Writing an Addon Operator 30 | 31 | Follow the dashboard walkthrough to stand up an addon operator[1]. Then dig into the 32 | declarative and addon patterns to extend it. 33 | 34 | [1] https://github.com/kubernetes-sigs/kubebuilder-declarative-pattern/tree/master/docs/addon/walkthrough 35 | */ 36 | package addon 37 | -------------------------------------------------------------------------------- /ktest/testharness/manifest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Kubernetes 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 testharness 18 | 19 | import ( 20 | "bufio" 21 | "context" 22 | "fmt" 23 | "io" 24 | "strings" 25 | 26 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 | k8syaml "k8s.io/apimachinery/pkg/util/yaml" 28 | ) 29 | 30 | func ParseObjects(ctx context.Context, manifest string) ([]*unstructured.Unstructured, error) { 31 | var objects []*unstructured.Unstructured 32 | reader := k8syaml.NewYAMLReader(bufio.NewReader(strings.NewReader(manifest))) 33 | for { 34 | raw, err := reader.Read() 35 | if err != nil { 36 | if err == io.EOF { 37 | return objects, nil 38 | } 39 | 40 | return nil, fmt.Errorf("reading YAML doc: %w", err) 41 | } 42 | 43 | u := &unstructured.Unstructured{} 44 | if err := k8syaml.Unmarshal(raw, &u); err != nil { 45 | return nil, fmt.Errorf("parsing object to unstructured: %w", err) 46 | } 47 | 48 | objects = append(objects, u) 49 | } 50 | 51 | return objects, nil 52 | } 53 | -------------------------------------------------------------------------------- /docs/project-management/branching-and-versioning.md: -------------------------------------------------------------------------------- 1 | # Branching and versioning strategy 2 | 3 | ## Versioning 4 | 5 | kubebuilder-declarative-pattern is a library, that works with controller-runtime and kubernetes. 6 | 7 | We follow semantic versioning, similar to other tooling in the kubernetes ecosystem. 8 | 9 | Because both controller-runtime and client-go introduce changes that require code changes, we follow their versioning. 10 | Specifically we align with the controller-runtime version, which itself aligns with kubernetes versioning. 11 | 12 | Thus: 13 | 14 | | kdp version | controller-runtime version | client-go version | 15 | |---|---|---| 16 | | v0.20 | v0.20 | v0.32 | 17 | | v0.19 | v0.19 | v0.31 | 18 | | v0.18 | v0.18 | v0.30 | 19 | | ... | ... | ... | 20 | | v0.x | v0.x | v0.x+12 | 21 | 22 | If we need to release multiple versions of kdp for a single version of controller-runtime (for example if we want to fix a bug), 23 | we use patch versions for that. 24 | We want to avoid breaking changes, and we will only make breaking changes on minor version bumps (as far as possible). 25 | 26 | ## Branches 27 | 28 | We maintain a `release-0.x` branch for the kdp major version `0.x`. Patch versions are cherry-picked to the branch and released. 29 | 30 | We cut the release branch with the beta release (i.e. we create `release-1.100` when we tag `1.100.0-beta.1`). 31 | 32 | Before beta releases, the master branch is for the next minor version. If we tag a version, it would be (for example) `1.101.0-alpha.1` (and then `1.101.0-alpha.2` etc), assuming the most recent release branch was `1.100` 33 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/pkg/applier/type.go: -------------------------------------------------------------------------------- 1 | package applier 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/api/meta" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/client-go/dynamic" 9 | "k8s.io/client-go/rest" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/kubebuilder-declarative-pattern/applylib/applyset" 12 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 13 | ) 14 | 15 | type Applier interface { 16 | Apply(ctx context.Context, options ApplierOptions) error 17 | } 18 | 19 | type ApplierOptions struct { 20 | Objects []*manifest.Object 21 | 22 | RESTConfig *rest.Config 23 | RESTMapper meta.RESTMapper 24 | Namespace string 25 | Validate bool 26 | 27 | CascadingStrategy metav1.DeletionPropagation 28 | 29 | PruneWhitelist []string 30 | Prune bool 31 | 32 | // Force is set if we should "force" the apply. 33 | // For server-side-apply, this corresponds to setting the force option, which ensures we take ownership 34 | // even when another field manager owns a field. 35 | Force bool 36 | 37 | // ExtraArgs holds additional arguments that should be passed to kubectl. 38 | // @deprecated: prefer using explicit arguments (Force etc) 39 | ExtraArgs []string 40 | 41 | ParentRef applyset.Parent 42 | Client client.Client 43 | 44 | // DynamicClient, if set, will be used for applying additional objects. 45 | // If not set, a dynamic client will be built from RESTConfig. 46 | // If the caller can provide a cached DynamicClient, that is more efficient. 47 | DynamicClient dynamic.Interface 48 | } 49 | -------------------------------------------------------------------------------- /pkg/test/testreconciler/simpletest/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes 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 API Schema definitions for the addons v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=addons.example.org 20 | package v1alpha1 21 | 22 | //go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0 object object:headerFile="../../../../../hack/boilerplate.go.txt" paths="./..." 23 | 24 | import ( 25 | "k8s.io/apimachinery/pkg/runtime/schema" 26 | "sigs.k8s.io/controller-runtime/pkg/scheme" 27 | ) 28 | 29 | var ( 30 | // GroupVersion is group version used to register these objects 31 | GroupVersion = schema.GroupVersion{Group: "addons.example.org", Version: "v1alpha1"} 32 | 33 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 34 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 35 | 36 | // AddToScheme adds the types in this group-version to the given scheme. 37 | AddToScheme = SchemeBuilder.AddToScheme 38 | ) 39 | -------------------------------------------------------------------------------- /mockkubeapiserver/deleteresource.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 mockkubeapiserver 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net/http" 23 | 24 | "k8s.io/apimachinery/pkg/runtime/schema" 25 | "k8s.io/apimachinery/pkg/types" 26 | ) 27 | 28 | // deleteResource is a request to delete a single resource 29 | type deleteResource struct { 30 | resourceRequestBase 31 | } 32 | 33 | // Run serves the http request 34 | func (req *deleteResource) Run(ctx context.Context, s *MockKubeAPIServer) error { 35 | gr := schema.GroupResource{Group: req.Group, Resource: req.Resource} 36 | resource := s.storage.FindResource(gr) 37 | if resource == nil { 38 | return req.writeErrorResponse(http.StatusNotFound) 39 | } 40 | 41 | id := types.NamespacedName{Namespace: req.Namespace, Name: req.Name} 42 | 43 | if req.SubResource != "" { 44 | return fmt.Errorf("unexpected subresource on delete %q", req.SubResource) 45 | } 46 | 47 | deletedObject, err := resource.DeleteObject(ctx, id) 48 | if err != nil { 49 | return err 50 | } 51 | return req.writeResponse(deletedObject) 52 | } 53 | -------------------------------------------------------------------------------- /dev/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2022 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | # CI script to run all the test commands 19 | 20 | set -o errexit 21 | set -o nounset 22 | set -o pipefail 23 | 24 | # cd to the repo root 25 | REPO_ROOT=$(git rev-parse --show-toplevel) 26 | cd "${REPO_ROOT}" 27 | 28 | set -x 29 | 30 | # Download the kubebuilder assets for envtest 31 | export KUBEBUILDER_ASSETS=$(go run sigs.k8s.io/controller-runtime/tools/setup-envtest@latest use -p path) 32 | 33 | pushd mockkubeapiserver 34 | CGO_ENABLED=0 go test -count=1 -v ./... 35 | popd 36 | 37 | pushd applylib 38 | CGO_ENABLED=0 go test -count=1 -v ./... 39 | popd 40 | 41 | # default, test direct kubectl applier 42 | CGO_ENABLED=0 go test -count=1 -v ./... 43 | # test applyset applier, without kubectl direct and exec dependencies 44 | CGO_ENABLED=0 go test -tags without_exec_applier,without_direct_applier -count=1 -v ./... 45 | # test exec kubectl applier, without direct_applier dependencies 46 | CGO_ENABLED=0 go test -tags without_direct_applier -count=1 -v ./... 47 | 48 | pushd examples/guestbook-operator 49 | CGO_ENABLED=0 go test -count=1 -v ./... 50 | popd -------------------------------------------------------------------------------- /ktest/testharness/golden.go: -------------------------------------------------------------------------------- 1 | package testharness 2 | 3 | import ( 4 | "bytes" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func RunGoldenTests(t *testing.T, basedir string, fn func(h *Harness, dir string)) { 14 | entries, err := os.ReadDir(basedir) 15 | if err != nil { 16 | t.Fatalf("ReadDir(%q) failed: %v", basedir, err) 17 | } 18 | files := make([]fs.FileInfo, 0, len(entries)) 19 | for _, entry := range entries { 20 | info, err := entry.Info() 21 | if err != nil { 22 | t.Fatalf("failed to get FileInfo %v: %v", info, err) 23 | } 24 | files = append(files, info) 25 | } 26 | count := 0 27 | for _, file := range files { 28 | name := file.Name() 29 | absPath := filepath.Join(basedir, name) 30 | count++ 31 | t.Run(name, func(t *testing.T) { 32 | h := New(t) 33 | fn(h, absPath) 34 | }) 35 | } 36 | // Likely a typo in basedir (?) 37 | if count == 0 { 38 | t.Errorf("no golden tests found in %q", basedir) 39 | } 40 | } 41 | 42 | func ShouldWriteGoldenOutput() bool { 43 | return os.Getenv("WRITE_GOLDEN_OUTPUT") != "" 44 | } 45 | 46 | func (h *Harness) CompareGoldenFile(p string, got string) { 47 | if ShouldWriteGoldenOutput() { 48 | // Short-circuit when the output is correct 49 | b, err := os.ReadFile(p) 50 | if err == nil && bytes.Equal(b, []byte(got)) { 51 | return 52 | } 53 | 54 | if err := os.WriteFile(p, []byte(got), 0644); err != nil { 55 | h.Fatalf("failed to write golden output %s: %v", p, err) 56 | } 57 | h.Errorf("wrote output to %s", p) 58 | } else { 59 | want := string(h.MustReadFile(p)) 60 | if diff := cmp.Diff(want, got); diff != "" { 61 | h.Errorf("unexpected diff in %s: %s", p, diff) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/utils/helpers_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | addonsv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" 9 | ) 10 | 11 | type mockCommonObject struct { 12 | addonsv1alpha1.CommonObject 13 | name string 14 | } 15 | 16 | func (m *mockCommonObject) ComponentName() string { 17 | return m.name 18 | } 19 | 20 | func TestGetCommonName(t *testing.T) { 21 | type args struct { 22 | instance runtime.Object 23 | } 24 | tests := []struct { 25 | name string 26 | args args 27 | want string 28 | wantErr bool 29 | }{ 30 | { 31 | name: "CommonObject instance", 32 | args: args{ 33 | instance: &mockCommonObject{name: "test-component"}, 34 | }, 35 | want: "test-component", 36 | wantErr: false, 37 | }, 38 | { 39 | name: "Unstructured instance", 40 | args: args{ 41 | instance: &unstructured.Unstructured{ 42 | Object: map[string]interface{}{ 43 | "kind": "TestKind", 44 | }, 45 | }, 46 | }, 47 | want: "testkind", 48 | wantErr: false, 49 | }, 50 | { 51 | name: "Invalid instance", 52 | args: args{ 53 | instance: &runtime.Unknown{}, 54 | }, 55 | want: "", 56 | wantErr: true, 57 | }, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | got, err := GetCommonName(tt.args.instance) 62 | if (err != nil) != tt.wantErr { 63 | t.Errorf("GetCommonName() error = %v, wantErr %v", err, tt.wantErr) 64 | return 65 | } 66 | if got != tt.want { 67 | t.Errorf("GetCommonName() = %v, want %v", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /dev/bots/projectbots/tag-release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2025 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | # Helper script to tag a release when the version/TAG_VERSION file is updated. 19 | 20 | set -o errexit 21 | set -o nounset 22 | set -o pipefail 23 | 24 | VERSION=$(cat version/TAG_VERSION) 25 | 26 | if [[ ! "${VERSION}" =~ ^([0-9]+[.][0-9]+)[.]([0-9]+)(-(alpha|beta)[.]([0-9]+))?$ ]]; then 27 | echo "Version ${VERSION} must be 'X.Y.Z', 'X.Y.Z-alpha.N', or 'X.Y.Z-beta.N'" 28 | exit 1 29 | fi 30 | 31 | # Store the minor version for use in the branch creation. 32 | # We want to capture this value now before we do another regex match. 33 | MINOR=${BASH_REMATCH[1]} 34 | RELEASE_BRANCH="release-${MINOR}" 35 | 36 | if [ "$(git tag -l "v${VERSION}")" ]; then 37 | echo "Tag v${VERSION} already exists" 38 | exit 0 39 | fi 40 | 41 | echo "Tagging Release ${VERSION}" 42 | git tag -a -m "Release ${VERSION}" "v${VERSION}" 43 | git push origin "v${VERSION}" 44 | 45 | # Create the release branch automatically on the first beta release. 46 | if [[ ! "${VERSION}" =~ .0-beta.1$ ]]; then 47 | exit 0 48 | fi 49 | 50 | echo "Creating Release Branch ${RELEASE_BRANCH}" 51 | git branch "${RELEASE_BRANCH}" 52 | git push origin "${RELEASE_BRANCH}" 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubebuilder-declarative-pattern 2 | 3 | kubebuilder-declarative-pattern provides a set of tools for building declarative cluster operators with kubebuilder. Declarative operators provide a fast path to orchestrating Kubernetes deployments to enable domain experts to focus on their component instead of re-answering questions like 'How do I get this YAML into the cluster?' or 'How do I update it?'. 4 | 5 | ## Development 6 | 7 | ### Running Smoke Tests 8 | 9 | Smoke tests are provided to ensure basic functionality of the framework against example operators. They should be run as part of significant code changes. The tests require a running Kubernetes cluster to be targeted from the local machine and write access to a GCR bucket. 10 | 11 | ```bash 12 | cd hack 13 | IMG= go run smoketest.go 14 | ``` 15 | 16 | ## Documentation 17 | 18 | - [Building an Operator (walkthrough)](./docs/addon/walkthrough/README.md) 19 | - [Pattern Documentation](https://godoc.org/sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns) 20 | - [Managing Addons with Operators (Video, KubeCon'18)](https://www.youtube.com/watch?v=LPejvfBR5_w) 21 | 22 | ## Community, discussion, contribution, and support 23 | 24 | Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). 25 | 26 | You can reach the maintainers of this project at: 27 | 28 | - [Slack](http://slack.k8s.io/) 29 | - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-dev) 30 | 31 | ### Code of conduct 32 | 33 | Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). 34 | 35 | [owners]: https://git.k8s.io/community/contributors/guide/owners.md 36 | [Creative Commons 4.0]: https://git.k8s.io/website/LICENSE 37 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/status/conditions.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "strings" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "sigs.k8s.io/cli-utils/pkg/kstatus/status" 8 | ) 9 | 10 | const ( 11 | AbnormalReason = "ManifestsNotReady" 12 | NormalReason = "Normal" 13 | ReadyType = "Ready" 14 | ) 15 | 16 | // buildReadyCondition returns a Condition object with human-readable message and reason. 17 | // The "reason" should be "Normal" if no deployment manifests have abnormal conditions, or "ManifestsNotReady" 18 | // as long as one deployment manifest has an abnormal condition. 19 | // The "message" contains each abnormal condition's "reason" and "message". 20 | // e.g. 21 | // 22 | // conditions: 23 | // - reason: ManifestsNotReady 24 | // message: |- 25 | // apps/v1, Kind=Deployment/argocd/argocd-repo-server:Deployment does not have minimum availability. 26 | // apps/v1, Kind=Deployment/argocd/argocd-server:Deployment does not have minimum availability. 27 | func buildReadyCondition(isReady bool, abnormalConditions []status.Condition) metav1.Condition { 28 | var readyCondition metav1.Condition 29 | readyCondition.Type = ReadyType 30 | 31 | if isReady { 32 | readyCondition.Status = metav1.ConditionTrue 33 | } else { 34 | readyCondition.Status = metav1.ConditionFalse 35 | } 36 | 37 | if len(abnormalConditions) == 0 { 38 | readyCondition.Reason = NormalReason 39 | readyCondition.Message = "all manifests are reconciled." 40 | } else { 41 | var messages []string 42 | for _, cond := range abnormalConditions { 43 | if cond.Message != "" { 44 | messages = append(messages, cond.Message) 45 | } 46 | } 47 | 48 | readyCondition.Reason = AbnormalReason 49 | 50 | readyCondition.Message = strings.Join(messages, "\n") 51 | } 52 | 53 | return readyCondition 54 | } 55 | -------------------------------------------------------------------------------- /mockkubeapiserver/getresource.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 mockkubeapiserver 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "k8s.io/apimachinery/pkg/types" 25 | ) 26 | 27 | // resourceRequestBase holds the common field for single-resource requests 28 | type resourceRequestBase struct { 29 | baseRequest 30 | 31 | Group string 32 | Version string 33 | Resource string 34 | Namespace string 35 | Name string 36 | 37 | SubResource string 38 | } 39 | 40 | // getResource is a request to get a single resource 41 | type getResource struct { 42 | resourceRequestBase 43 | } 44 | 45 | // Run serves the http request 46 | func (req *getResource) Run(ctx context.Context, s *MockKubeAPIServer) error { 47 | gr := schema.GroupResource{Group: req.Group, Resource: req.Resource} 48 | resource := s.storage.FindResource(gr) 49 | if resource == nil { 50 | return req.writeErrorResponse(http.StatusNotFound) 51 | } 52 | 53 | id := types.NamespacedName{Namespace: req.Namespace, Name: req.Name} 54 | 55 | object, found, err := resource.GetObject(ctx, id) 56 | if err != nil { 57 | return err 58 | } 59 | if !found { 60 | return req.writeErrorResponse(http.StatusNotFound) 61 | } 62 | 63 | return req.writeResponse(object) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/restmapper/controllerrestmapper_test.go: -------------------------------------------------------------------------------- 1 | package restmapper 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "k8s.io/apimachinery/pkg/api/meta" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/klog/v2" 13 | "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver" 14 | ) 15 | 16 | func TestRESTMapping(t *testing.T) { 17 | k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0") 18 | if err != nil { 19 | t.Fatalf("error building mock kube-apiserver: %v", err) 20 | } 21 | 22 | k8s.RegisterType(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, "namespaces", meta.RESTScopeRoot) 23 | 24 | defer func() { 25 | if err := k8s.Stop(); err != nil { 26 | t.Fatalf("error closing mock kube-apiserver: %v", err) 27 | } 28 | }() 29 | 30 | addr, err := k8s.StartServing() 31 | if err != nil { 32 | t.Errorf("error starting mock kube-apiserver: %v", err) 33 | } 34 | 35 | klog.Infof("mock kubeapiserver will listen on %v", addr) 36 | 37 | restConfig := &rest.Config{ 38 | Host: addr.String(), 39 | } 40 | 41 | restMapper, err := NewForTest(restConfig) 42 | if err != nil { 43 | t.Fatalf("error from NewForTest: %v", err) 44 | } 45 | gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"} 46 | restMapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) 47 | if err != nil { 48 | t.Fatalf("error from RESTMapping(%v): %v", gvk, err) 49 | } 50 | 51 | got := fmt.Sprintf("resource:%v\ngvk:%v\nscope:%v", restMapping.Resource, restMapping.GroupVersionKind, restMapping.Scope.Name()) 52 | want := ` 53 | resource:/v1, Resource=namespaces 54 | gvk:/v1, Kind=Namespace 55 | scope:root 56 | ` 57 | got = strings.TrimSpace(got) 58 | want = strings.TrimSpace(want) 59 | 60 | if diff := cmp.Diff(want, got); diff != "" { 61 | t.Errorf("RESTMapping(%v) diff (-want +got):\n%s", gvk, diff) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /mockkubeapiserver/testhelpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 mockkubeapiserver 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "k8s.io/apimachinery/pkg/api/meta" 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | "k8s.io/apimachinery/pkg/runtime/schema" 26 | "k8s.io/klog/v2" 27 | "sigs.k8s.io/yaml" 28 | ) 29 | 30 | // RegisterType registers a type with the schema for the mock kubeapiserver 31 | func (s *MockKubeAPIServer) RegisterType(gvk schema.GroupVersionKind, resource string, scope meta.RESTScope) { 32 | s.storage.RegisterType(gvk, resource, scope) 33 | } 34 | 35 | // AddObject pre-creates an object 36 | func (s *MockKubeAPIServer) AddObject(obj *unstructured.Unstructured) error { 37 | klog.Infof("precreating %s object %s/%s", obj.GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName()) 38 | return s.storage.AddObject(obj) 39 | } 40 | 41 | // AddObjectsFromManifest pre-creates the objects in the manifest 42 | func (s *MockKubeAPIServer) AddObjectsFromManifest(y string) error { 43 | for _, obj := range strings.Split(y, "\n---\n") { 44 | u := &unstructured.Unstructured{} 45 | if err := yaml.Unmarshal([]byte(obj), &u.Object); err != nil { 46 | return fmt.Errorf("failed to unmarshal object %q: %w", obj, err) 47 | } 48 | if err := s.AddObject(u); err != nil { 49 | return err 50 | } 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/status/accessors.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/klog/v2" 10 | ) 11 | 12 | // GetConditions pulls out the `status.conditions` field from runtime.Object 13 | func GetConditions(instance runtime.Object) ([]metav1.Condition, error) { 14 | statusVal := reflect.ValueOf(instance).Elem().FieldByName("Status") 15 | if !statusVal.IsValid() { 16 | return nil, fmt.Errorf("status field not found") 17 | } 18 | conditionsVal := statusVal.FieldByName("Conditions") 19 | if !conditionsVal.IsValid() { 20 | klog.Errorf("unable to find `status.condition` in %T", instance) 21 | return nil, nil 22 | } 23 | 24 | v := conditionsVal.Interface() 25 | conditions, ok := v.([]metav1.Condition) 26 | if !ok { 27 | return nil, fmt.Errorf("unexpecetd type for status.conditions; got %T, want []metav1.Condition", v) 28 | } 29 | return conditions, nil 30 | } 31 | 32 | // SetConditions sets the newConditions to runtime.Object `status.conditions` field. 33 | func SetConditions(instance runtime.Object, newConditions []metav1.Condition) error { 34 | statusVal := reflect.ValueOf(instance).Elem().FieldByName("Status") 35 | if !statusVal.IsValid() { 36 | // Status not ready. 37 | return fmt.Errorf("status field not found") 38 | } 39 | conditionsVal := statusVal.FieldByName("Conditions") 40 | if !conditionsVal.IsValid() { 41 | klog.Errorf("unable to find `status.condition` in %T", instance) 42 | return nil 43 | } 44 | 45 | newConditionsVal := reflect.ValueOf(newConditions) 46 | if !conditionsVal.CanSet() { 47 | return fmt.Errorf("cannot set status.conditions field") 48 | } 49 | if !newConditionsVal.CanConvert(conditionsVal.Type()) { 50 | return fmt.Errorf("cannot set type %v to status.conditions type %v", newConditionsVal.Type(), conditionsVal.Type()) 51 | } 52 | conditionsVal.Set(newConditionsVal) 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://github.com/kubernetes/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: 4 | 5 | _As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ 6 | 7 | ## Getting Started 8 | 9 | We have full documentation on how to get started contributing here: 10 | 11 | 14 | 15 | - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests 16 | - [Kubernetes Contributor Guide](http://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](http://git.k8s.io/community/contributors/guide#contributing) 17 | - [Contributor Cheat Sheet](https://github.com/kubernetes/community/blob/master/contributors/guide/contributor-cheatsheet/README.md) - Common resources for existing developers 18 | 19 | ## Mentorship 20 | 21 | - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! 22 | 23 | 32 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/status/basic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 status 18 | 19 | import ( 20 | "sigs.k8s.io/controller-runtime/pkg/client" 21 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" 22 | ) 23 | 24 | // NewBasic provides an implementation of declarative.Status that 25 | // performs no preflight checks. 26 | // 27 | // Deprecated: This function exists for backward compatibility, please use NewKstatusCheck 28 | func NewBasic(client client.Client) declarative.Status { 29 | return &declarative.StatusBuilder{ 30 | BuildStatusImpl: NewAggregator(client), 31 | // no preflight checks 32 | } 33 | } 34 | 35 | // NewBasicVersionCheck provides an implementation of declarative.Status that 36 | // performs version checks for the version of the operator that the manifest requires. 37 | func NewBasicVersionChecks(client client.Client, version string) (declarative.Status, error) { 38 | v, err := NewVersionCheck(client, version) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return &declarative.StatusBuilder{ 44 | BuildStatusImpl: NewAggregator(client), 45 | VersionCheckImpl: v, 46 | // no preflight checks 47 | }, nil 48 | } 49 | 50 | // TODO: Create a version that doesn't take (unusued) client & reconciler args 51 | func NewKstatusCheck(client client.Client, d *declarative.Reconciler) declarative.Status { 52 | return &declarative.StatusBuilder{ 53 | BuildStatusImpl: NewKstatusAgregator(client, d), 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /dev/codebots/update-golang-version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2022 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | # Helper script to update golang to latest version 19 | 20 | set -o errexit 21 | set -o nounset 22 | set -o pipefail 23 | 24 | REPO_ROOT=$(git rev-parse --show-toplevel) 25 | cd ${REPO_ROOT} 26 | 27 | # TODO: Should we update to the latest go, or the latest go in a particular minor? 28 | GO_TOOLCHAIN=$(curl https://go.dev/dl/?mode=json | jq -r '[.[] | select(.stable == true)] | .[0].version') 29 | echo "GO_TOOLCHAIN=$GO_TOOLCHAIN" 30 | 31 | git switch --force-create codebot-update-golang-version-${GO_TOOLCHAIN} 32 | 33 | # GO_VERSION is the GO_TOOLCHAIN without the go prefix 34 | GO_VERSION=${GO_TOOLCHAIN#go} 35 | echo "GO_VERSION=$GO_VERSION" 36 | 37 | # Update go.mod files 38 | # Tidy each individual go.mod 39 | for gomod_file in $(find "${REPO_ROOT}" -name "go.mod"); do 40 | dir=$(dirname ${gomod_file}) 41 | cd "${dir}" 42 | echo "Updating $gomod_file to toolchain $GO_TOOLCHAIN" 43 | go mod edit -toolchain=${GO_TOOLCHAIN} 44 | done 45 | 46 | # Update Docker images 47 | for dockerfile in $(find "${REPO_ROOT}" -name "Dockerfile*"); do 48 | echo "Updating Dockerfile $dockerfile to go image $GO_VERSION" 49 | sed -i -e "s/FROM golang:[0-9.]*/FROM golang:$GO_VERSION/g" $dockerfile 50 | done 51 | 52 | if $(git diff --quiet); then 53 | echo "No changes" 54 | else 55 | cd ${REPO_ROOT} 56 | git add . 57 | git commit -m "codebot: update go to ${GO_VERSION}" 58 | fi 59 | -------------------------------------------------------------------------------- /mockkubeapiserver/request.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 mockkubeapiserver 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "net/http" 24 | "strconv" 25 | 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | type Request interface { 30 | Run(ctx context.Context, s *MockKubeAPIServer) error 31 | Init(w http.ResponseWriter, r *http.Request) 32 | } 33 | 34 | // baseRequest is the base for our higher-level http requests 35 | type baseRequest struct { 36 | w http.ResponseWriter 37 | r *http.Request 38 | } 39 | 40 | func (b *baseRequest) Init(w http.ResponseWriter, r *http.Request) { 41 | b.w = w 42 | b.r = r 43 | } 44 | 45 | func (r *baseRequest) writeResponse(obj interface{}) error { 46 | b, err := json.Marshal(obj) 47 | if err != nil { 48 | return fmt.Errorf("error from json.Marshal on %T: %w", obj, err) 49 | } 50 | r.w.Header().Add("Content-Type", "application/json") 51 | r.w.Header().Add("Cache-Control", "no-cache, private") 52 | r.w.Header().Add("Content-Length", strconv.Itoa(len(b))) 53 | 54 | r.w.WriteHeader(http.StatusOK) 55 | 56 | if _, err := r.w.Write(b); err != nil { 57 | // Too late to send error response 58 | klog.Warningf("error writing http response: %v", err) 59 | return nil 60 | } 61 | return nil 62 | } 63 | 64 | func (r *baseRequest) writeErrorResponse(statusCode int) error { 65 | klog.Warningf("%d for %s %s", statusCode, r.r.Method, r.r.URL) 66 | http.Error(r.w, http.StatusText(statusCode), statusCode) 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /applylib/forked/k8s.io/apimachinery/pkg/util/sets/ordered.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 sets 18 | 19 | // ordered is a constraint that permits any ordered type: any type 20 | // that supports the operators < <= >= >. 21 | // If future releases of Go add new ordered types, 22 | // this constraint will be modified to include them. 23 | type ordered interface { 24 | integer | float | ~string 25 | } 26 | 27 | // integer is a constraint that permits any integer type. 28 | // If future releases of Go add new predeclared integer types, 29 | // this constraint will be modified to include them. 30 | type integer interface { 31 | signed | unsigned 32 | } 33 | 34 | // float is a constraint that permits any floating-point type. 35 | // If future releases of Go add new predeclared floating-point types, 36 | // this constraint will be modified to include them. 37 | type float interface { 38 | ~float32 | ~float64 39 | } 40 | 41 | // signed is a constraint that permits any signed integer type. 42 | // If future releases of Go add new predeclared signed integer types, 43 | // this constraint will be modified to include them. 44 | type signed interface { 45 | ~int | ~int8 | ~int16 | ~int32 | ~int64 46 | } 47 | 48 | // unsigned is a constraint that permits any unsigned integer type. 49 | // If future releases of Go add new predeclared unsigned integer types, 50 | // this constraint will be modified to include them. 51 | type unsigned interface { 52 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr 53 | } 54 | -------------------------------------------------------------------------------- /mockkubeapiserver/apiresourcelist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 mockkubeapiserver 18 | 19 | import ( 20 | "context" 21 | "sort" 22 | 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/runtime/schema" 25 | ) 26 | 27 | // apiResourceList is a request for api discovery, such as GET /apis/resourcemanager.cnrm.cloud.google.com/v1beta1 28 | type apiResourceList struct { 29 | baseRequest 30 | 31 | Group string 32 | Version string 33 | } 34 | 35 | // Run serves the http request 36 | func (req *apiResourceList) Run(ctx context.Context, s *MockKubeAPIServer) error { 37 | gv := schema.GroupVersion{ 38 | Group: req.Group, 39 | Version: req.Version, 40 | } 41 | response := &metav1.APIResourceList{} 42 | response.Kind = "APIResourceList" 43 | response.APIVersion = "v1" 44 | response.GroupVersion = gv.String() 45 | for _, resource := range s.storage.AllResources() { 46 | if resource.Group != req.Group || resource.Version != req.Version { 47 | continue 48 | } 49 | response.APIResources = append(response.APIResources, resource) 50 | } 51 | 52 | // Return in a stable order, for more test predictability 53 | sort.Slice(response.APIResources, func(i, j int) bool { 54 | l := response.APIResources[i] 55 | r := response.APIResources[j] 56 | 57 | if l.Group != r.Group { 58 | return l.Group < r.Group 59 | } 60 | if l.Kind != r.Kind { 61 | return l.Kind < r.Kind 62 | } 63 | return false 64 | }) 65 | return req.writeResponse(response) 66 | } 67 | -------------------------------------------------------------------------------- /mockkubeapiserver/schemas/kubernetes_builtin_schema.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "sigs.k8s.io/structured-merge-diff/v4/typed" 8 | "sigs.k8s.io/yaml" 9 | 10 | _ "embed" 11 | ) 12 | 13 | //go:embed kubernetes_builtin_schema.yaml 14 | var kubernetesBuiltinSchemaYAML string 15 | 16 | //go:embed kubernetes_builtin_schema.meta.yaml 17 | var kubernetesBuiltinSchemaMetaYAML string 18 | 19 | const BuiltinKey = "kubernetes_builtin_schema" 20 | 21 | type Schema struct { 22 | Parser *typed.Parser 23 | Meta *SchemaMeta 24 | } 25 | 26 | type SchemaMeta struct { 27 | Resources []SchemaMetaResource `json:"resources"` 28 | } 29 | 30 | type SchemaMetaResource struct { 31 | Key string `json:"key"` 32 | Group string `json:"group"` 33 | Version string `json:"version"` 34 | Kind string `json:"kind"` 35 | Resource string `json:"resource"` 36 | Scope string `json:"scope"` 37 | } 38 | 39 | type schemaCache struct { 40 | mutex sync.Mutex 41 | schemas map[string]*Schema 42 | } 43 | 44 | var globalSchemaCache schemaCache 45 | 46 | func KubernetesBuiltInSchema() (*Schema, error) { 47 | return globalSchemaCache.Get(BuiltinKey) 48 | } 49 | 50 | func (c *schemaCache) Get(key string) (*Schema, error) { 51 | c.mutex.Lock() 52 | defer c.mutex.Unlock() 53 | 54 | if schema := c.schemas[key]; schema != nil { 55 | return schema, nil 56 | } 57 | 58 | if key != BuiltinKey { 59 | return nil, fmt.Errorf("schema %q not known", key) 60 | } 61 | 62 | schemaYAML := kubernetesBuiltinSchemaYAML 63 | parser, err := typed.NewParser(typed.YAMLObject(schemaYAML)) 64 | if err != nil { 65 | return nil, fmt.Errorf("error parsing schema: %w", err) 66 | } 67 | 68 | metaYAML := kubernetesBuiltinSchemaMetaYAML 69 | meta := &SchemaMeta{} 70 | if err := yaml.Unmarshal([]byte(metaYAML), meta); err != nil { 71 | return nil, fmt.Errorf("error parsing schema metadata: %w", err) 72 | } 73 | 74 | schema := &Schema{ 75 | Parser: parser, 76 | Meta: meta, 77 | } 78 | if c.schemas == nil { 79 | c.schemas = make(map[string]*Schema) 80 | } 81 | c.schemas[key] = schema 82 | return schema, nil 83 | } 84 | -------------------------------------------------------------------------------- /examples/guestbook-operator/controllers/guestbook_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes 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 controllers 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | "k8s.io/klog/v2/klogr" 24 | "sigs.k8s.io/controller-runtime/pkg/envtest" 25 | "sigs.k8s.io/controller-runtime/pkg/log" 26 | "sigs.k8s.io/controller-runtime/pkg/manager" 27 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 28 | 29 | api "sigs.k8s.io/kubebuilder-declarative-pattern/examples/guestbook-operator/api/v1alpha1" 30 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" 31 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/test/golden" 32 | ) 33 | 34 | func TestGuestbook(t *testing.T) { 35 | log.SetLogger(klogr.New()) 36 | 37 | env := &envtest.Environment{ 38 | CRDInstallOptions: envtest.CRDInstallOptions{ 39 | ErrorIfPathMissing: true, 40 | Paths: []string{ 41 | filepath.Join("..", "config", "crd", "bases"), 42 | }, 43 | }, 44 | } 45 | opt := golden.ValidatorOptions{ 46 | EnvtestEnvironment: env, 47 | ManagerOptions: manager.Options{ 48 | Metrics: metricsserver.Options{ 49 | BindAddress: "0", 50 | }, 51 | }, 52 | } 53 | opt.WithSchema(api.AddToScheme) 54 | 55 | v := golden.NewValidator(t, opt) 56 | 57 | v.Validate(func(mgr manager.Manager) (*declarative.Reconciler, error) { 58 | gr := &GuestbookReconciler{ 59 | Client: mgr.GetClient(), 60 | } 61 | err := gr.setupReconciler(mgr) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return &gr.Reconciler, nil 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /ktest/httprecorder/http_recorder.go: -------------------------------------------------------------------------------- 1 | package httprecorder 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | type HTTPRecorder struct { 12 | inner http.RoundTripper 13 | log *RequestLog 14 | } 15 | 16 | func NewRecorder(inner http.RoundTripper, log *RequestLog) *HTTPRecorder { 17 | rt := &HTTPRecorder{inner: inner, log: log} 18 | return rt 19 | } 20 | 21 | func (m *HTTPRecorder) RoundTrip(request *http.Request) (*http.Response, error) { 22 | entry := &LogEntry{} 23 | entry.Request = Request{ 24 | Method: request.Method, 25 | URL: request.URL.String(), 26 | Header: request.Header, 27 | } 28 | 29 | if request.Body != nil { 30 | requestBody, err := io.ReadAll(request.Body) 31 | if err != nil { 32 | return nil, fmt.Errorf("HTTPRecorder failed to read request body") 33 | } 34 | entry.Request.Body = string(requestBody) 35 | request.Body = io.NopCloser(bytes.NewReader(requestBody)) 36 | } 37 | 38 | streaming := false 39 | if request.URL.Query().Get("watch") == "true" { 40 | streaming = true 41 | } 42 | 43 | // We log the request here, because otherwise we miss long-running requests (watches) 44 | m.log.AddEntry(entry) 45 | 46 | response, err := m.inner.RoundTrip(request) 47 | 48 | if response != nil { 49 | entry.Response.Status = response.Status 50 | entry.Response.StatusCode = response.StatusCode 51 | 52 | entry.Response.Header = make(http.Header) 53 | for k, values := range response.Header { 54 | switch strings.ToLower(k) { 55 | case "authorization": 56 | entry.Response.Header[k] = []string{"(redacted)"} 57 | default: 58 | entry.Response.Header[k] = values 59 | } 60 | } 61 | 62 | if streaming { 63 | entry.Response.Body = "" 64 | } else if response.Body != nil { 65 | responseBody, err := io.ReadAll(response.Body) 66 | if err != nil { 67 | entry.Response.Body = fmt.Sprintf("", err) 68 | } else { 69 | entry.Response.Body = string(responseBody) 70 | response.Body = io.NopCloser(bytes.NewReader(responseBody)) 71 | } 72 | } 73 | } 74 | 75 | return response, err 76 | } 77 | -------------------------------------------------------------------------------- /mockkubeapiserver/hooks/namespace.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 hooks 18 | 19 | import ( 20 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver/storage" 23 | ) 24 | 25 | type NamespaceHook struct { 26 | } 27 | 28 | func (s *NamespaceHook) OnWatchEvent(ev *storage.WatchEvent) { 29 | switch ev.GroupKind() { 30 | case schema.GroupKind{Kind: "Namespace"}: 31 | s.namespaceChanged(ev) 32 | } 33 | } 34 | 35 | // Fake the required transitions for Namespace objects so they become ready 36 | func (s *NamespaceHook) namespaceChanged(ev *storage.WatchEvent) { 37 | u := ev.Unstructured() 38 | 39 | // These changes seem to be done synchronously (similar to a mutating webhook) 40 | labels := u.GetLabels() 41 | name := u.GetName() 42 | if labels["kubernetes.io/metadata.name"] != name { 43 | if labels == nil { 44 | labels = make(map[string]string) 45 | } 46 | labels["kubernetes.io/metadata.name"] = name 47 | u.SetLabels(labels) 48 | } 49 | phase, _, _ := unstructured.NestedFieldNoCopy(u.Object, "status", "phase") 50 | if phase != "Active" { 51 | unstructured.SetNestedField(u.Object, "Active", "status", "phase") 52 | } 53 | found := false 54 | finalizers, _, _ := unstructured.NestedSlice(u.Object, "spec", "finalizers") 55 | for _, finalizer := range finalizers { 56 | if finalizer == "kubernetes" { 57 | found = true 58 | } 59 | } 60 | if !found { 61 | finalizers = append(finalizers, "kubernetes") 62 | unstructured.SetNestedSlice(u.Object, finalizers, "spec", "finalizers") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/patterns/addon/init.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 addon 18 | 19 | import ( 20 | "context" 21 | "flag" 22 | "sync" 23 | 24 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/loaders" 25 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" 26 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 27 | ) 28 | 29 | var ( 30 | privateRegistry = flag.String("private-registry", "", "private image registry, if set overwrites the image repo on all pods") 31 | imagePullSecret = flag.String("image-pull-secret", "", "secret used accessing private image registry, if set imagePullSecret annotation is added to all pods") 32 | ) 33 | 34 | var initOnce sync.Once 35 | 36 | // Init should be called at the beginning of the main function for all addon operator controllers 37 | // 38 | // This function configures the environment and declarative library 39 | // with defaults specific to addons. 40 | func Init() { 41 | initOnce.Do(func() { 42 | if declarative.DefaultManifestLoader == nil { 43 | declarative.DefaultManifestLoader = func() (declarative.ManifestController, error) { 44 | return loaders.NewManifestLoader(loaders.FlagChannel) 45 | } 46 | } 47 | 48 | declarative.Options.Begin = append(declarative.Options.Begin, declarative.WithObjectTransform(func(ctx context.Context, obj declarative.DeclarativeObject, m *manifest.Objects) error { 49 | if *privateRegistry != "" || *imagePullSecret != "" { 50 | return declarative.ImageRegistryTransform(*privateRegistry, *imagePullSecret)(ctx, obj, m) 51 | } 52 | return nil 53 | })) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /mockkubeapiserver/go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/google/go-cmp v0.6.0 9 | k8s.io/apimachinery v0.32.1 10 | k8s.io/client-go v0.32.1 11 | k8s.io/klog/v2 v2.130.1 12 | sigs.k8s.io/controller-runtime v0.20.4 13 | sigs.k8s.io/kubebuilder-declarative-pattern/ktest v0.0.0-20240926141202-cf8082c623b8 14 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 15 | sigs.k8s.io/yaml v1.4.0 16 | ) 17 | 18 | require ( 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 21 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 22 | github.com/go-logr/logr v1.4.2 // indirect 23 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 24 | github.com/go-openapi/jsonreference v0.21.0 // indirect 25 | github.com/go-openapi/swag v0.23.0 // indirect 26 | github.com/gogo/protobuf v1.3.2 // indirect 27 | github.com/golang/protobuf v1.5.4 // indirect 28 | github.com/google/gnostic-models v0.6.8 // indirect 29 | github.com/google/gofuzz v1.2.0 // indirect 30 | github.com/google/uuid v1.6.0 // indirect 31 | github.com/josharian/intern v1.0.0 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/mailru/easyjson v0.7.7 // indirect 34 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 35 | github.com/modern-go/reflect2 v1.0.2 // indirect 36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 37 | github.com/x448/float16 v0.8.4 // indirect 38 | golang.org/x/net v0.30.0 // indirect 39 | golang.org/x/oauth2 v0.23.0 // indirect 40 | golang.org/x/sys v0.26.0 // indirect 41 | golang.org/x/term v0.25.0 // indirect 42 | golang.org/x/text v0.19.0 // indirect 43 | golang.org/x/time v0.7.0 // indirect 44 | google.golang.org/protobuf v1.35.1 // indirect 45 | gopkg.in/inf.v0 v0.9.1 // indirect 46 | gopkg.in/yaml.v3 v3.0.1 // indirect 47 | k8s.io/api v0.32.1 // indirect 48 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 49 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 50 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /applylib/applyset/health.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 applyset 18 | 19 | import ( 20 | "strings" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | "k8s.io/klog/v2" 24 | "sigs.k8s.io/kustomize/kstatus/status" 25 | ) 26 | 27 | // IsHealthy reports whether the object should be considered "healthy" 28 | func IsHealthy(u *unstructured.Unstructured) (bool, string, error) { 29 | result, err := status.Compute(u) 30 | if err != nil { 31 | klog.Infof("unable to compute condition for %s", humanName(u)) 32 | return false, result.Message, err 33 | } 34 | switch result.Status { 35 | case status.InProgressStatus: 36 | return false, result.Message, nil 37 | case status.FailedStatus: 38 | return false, result.Message, nil 39 | case status.TerminatingStatus: 40 | return false, result.Message, nil 41 | case status.UnknownStatus: 42 | klog.Warningf("unknown status for %s", humanName(u)) 43 | return false, result.Message, nil 44 | case status.CurrentStatus: 45 | return true, result.Message, nil 46 | default: 47 | klog.Warningf("unknown status value %s", result.Status) 48 | return false, result.Message, nil 49 | } 50 | } 51 | 52 | // humanName returns an identifier for the object suitable for printing in log messages 53 | func humanName(u *unstructured.Unstructured) string { 54 | gvk := u.GroupVersionKind() 55 | var s strings.Builder 56 | s.WriteString(gvk.Kind) 57 | if gvk.Group != "" { 58 | s.WriteString(".") 59 | s.WriteString(gvk.Group) 60 | } 61 | s.WriteString(":") 62 | namespace := u.GetNamespace() 63 | name := u.GetName() 64 | if namespace != "" { 65 | s.WriteString(namespace) 66 | s.WriteString("/") 67 | s.WriteString(name) 68 | } else { 69 | s.WriteString(name) 70 | } 71 | return s.String() 72 | } 73 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/apis/v1alpha1/common_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 | "k8s.io/apimachinery/pkg/runtime" 22 | ) 23 | 24 | // CommonObject is an interface that must be implemented by 25 | // all Addon objects in order to use the Addon pattern 26 | type CommonObject interface { 27 | runtime.Object 28 | metav1.Object 29 | ComponentName() string 30 | CommonSpec() CommonSpec 31 | GetCommonStatus() CommonStatus 32 | SetCommonStatus(CommonStatus) 33 | } 34 | 35 | // CommonSpec defines the set of configuration attributes that must be exposed on all addons. 36 | type CommonSpec struct { 37 | // Version specifies the exact addon version to be deployed, eg 1.2.3 38 | // It should not be specified if Channel is specified 39 | Version string `json:"version,omitempty"` 40 | // Channel specifies a channel that can be used to resolve a specific addon, eg: stable 41 | // It will be ignored if Version is specified 42 | Channel string `json:"channel,omitempty"` 43 | } 44 | 45 | // CommonStatus is a set of status attributes that must be exposed on all addons. 46 | type CommonStatus struct { 47 | Healthy bool `json:"healthy"` 48 | Errors []string `json:"errors,omitempty"` 49 | Phase string `json:"phase,omitempty"` 50 | // +kubebuilder:default:=0 51 | ObservedGeneration int64 `json:"observedGeneration"` 52 | } 53 | 54 | // Patchable is a trait for addon CRDs that expose a raw set of Patches to be 55 | // applied to the declarative manifest. 56 | type Patchable interface { 57 | PatchSpec() PatchSpec 58 | } 59 | 60 | // +k8s:deepcopy-gen=true 61 | type PatchSpec struct { 62 | // +kubebuilder:pruning:PreserveUnknownFields 63 | Patches []*runtime.RawExtension `json:"patches,omitempty"` 64 | } 65 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/labels.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 declarative 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "strings" 23 | 24 | "k8s.io/apimachinery/pkg/runtime" 25 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 26 | "sigs.k8s.io/controller-runtime/pkg/log" 27 | 28 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 29 | ) 30 | 31 | // AddLabels returns an ObjectTransform that adds labels to all the objects 32 | func AddLabels(labels map[string]string) ObjectTransform { 33 | return func(ctx context.Context, o DeclarativeObject, manifest *manifest.Objects) error { 34 | log := log.FromContext(ctx) 35 | // TODO: Add to selectors and labels in templates? 36 | for _, o := range manifest.Items { 37 | log.WithValues("object", o).WithValues("labels", labels).V(1).Info("add labels to object") 38 | o.AddLabels(labels) 39 | } 40 | 41 | return nil 42 | } 43 | } 44 | 45 | // SourceLabel returns a fixed label based on the type and name of the DeclarativeObject 46 | func SourceLabel(scheme *runtime.Scheme) LabelMaker { 47 | return func(ctx context.Context, o DeclarativeObject) map[string]string { 48 | log := log.FromContext(ctx) 49 | 50 | gvk := o.GetObjectKind().GroupVersionKind() 51 | gvk, err := apiutil.GVKForObject(o, scheme) 52 | 53 | if err != nil { 54 | log.WithValues("object", o).WithValues("GroupVersionKind", gvk).Error(err, "can't map GroupVersionKind") 55 | return map[string]string{} 56 | } 57 | 58 | if gvk.Group == "" || gvk.Kind == "" { 59 | log.WithValues("object", o).WithValues("GroupVersionKind", gvk).Info("GroupVersionKind is invalid") 60 | return map[string]string{} 61 | } 62 | 63 | return map[string]string{ 64 | fmt.Sprintf("%s/%s", gvk.Group, strings.ToLower(gvk.Kind)): o.GetName(), 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/loaders/versions_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 loaders 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | ) 23 | 24 | func TestCompareVersions(t *testing.T) { 25 | ctx := context.TODO() 26 | 27 | grid := []struct { 28 | Bigger Version 29 | Smaller Version 30 | }{ 31 | // Basic ordering applies 32 | { 33 | Bigger: Version{Package: "", Version: "2.0.0"}, 34 | Smaller: Version{Package: "", Version: "1.0.0"}, 35 | }, 36 | // Parsing is tolerant 37 | { 38 | Bigger: Version{Package: "", Version: "1"}, 39 | Smaller: Version{Package: "", Version: "0.1"}, 40 | }, 41 | // Named packages have higher priority 42 | { 43 | Bigger: Version{Package: "foo", Version: "v1"}, 44 | Smaller: Version{Package: "", Version: "v1"}, 45 | }, 46 | // Valid versions are higher priority than invalid versions 47 | { 48 | Bigger: Version{Package: "", Version: "v1"}, 49 | Smaller: Version{Package: "", Version: "corrupt2"}, 50 | }, 51 | } 52 | 53 | for _, g := range grid { 54 | { 55 | l := &g.Bigger 56 | r := &g.Smaller 57 | v := l.Compare(ctx, r) 58 | 59 | if v <= 0 { 60 | t.Errorf("comparison failed: %+v vs %+v was %d", l, r, v) 61 | } 62 | } 63 | 64 | { 65 | l := &g.Smaller 66 | r := &g.Bigger 67 | v := l.Compare(ctx, r) 68 | 69 | if v >= 0 { 70 | t.Errorf("comparison failed: %+v vs %+v was %d", l, r, v) 71 | } 72 | } 73 | 74 | { 75 | l := &g.Bigger 76 | r := &g.Bigger 77 | v := l.Compare(ctx, r) 78 | 79 | if v != 0 { 80 | t.Errorf("comparison failed: %+v vs %+v was %d", l, r, v) 81 | } 82 | } 83 | 84 | { 85 | l := &g.Smaller 86 | r := &g.Smaller 87 | v := l.Compare(ctx, r) 88 | 89 | if v != 0 { 90 | t.Errorf("comparison failed: %+v vs %+v was %d", l, r, v) 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/hooks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 declarative 18 | 19 | import ( 20 | "context" 21 | 22 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/applier" 23 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 24 | ) 25 | 26 | // Hook is the base interface implemented by a hook 27 | type Hook interface { 28 | } 29 | 30 | // ApplyOperation contains the details of an Apply operation 31 | type ApplyOperation struct { 32 | // Subject is the object we are reconciling 33 | Subject DeclarativeObject 34 | 35 | // Objects is the set of objects we are applying 36 | Objects *manifest.Objects 37 | 38 | // ApplierOptions is the set of options passed to the applier 39 | ApplierOptions *applier.ApplierOptions 40 | } 41 | 42 | // AfterApply is implemented by hooks that want to be called after every apply operation 43 | type AfterApply interface { 44 | AfterApply(ctx context.Context, op *ApplyOperation) error 45 | } 46 | 47 | // BeforeApply is implemented by hooks that want to be called before every apply operation 48 | type BeforeApply interface { 49 | BeforeApply(ctx context.Context, op *ApplyOperation) error 50 | } 51 | 52 | // UpdateStatusOperation contains the details of an Apply operation 53 | type UpdateStatusOperation struct { 54 | // Subject is the object we are reconciling 55 | Subject DeclarativeObject 56 | } 57 | 58 | // AfterUpdateStatus is implemented by hooks that want to be called after the update-status phase 59 | type AfterUpdateStatus interface { 60 | AfterUpdateStatus(ctx context.Context, op *UpdateStatusOperation) error 61 | } 62 | 63 | // BeforeUpdateStatus is implemented by hooks that want to be called before the update-status phase 64 | type BeforeUpdateStatus interface { 65 | BeforeUpdateStatus(ctx context.Context, op *UpdateStatusOperation) error 66 | } 67 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/sort.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 declarative 18 | 19 | import ( 20 | "context" 21 | 22 | "sigs.k8s.io/controller-runtime/pkg/log" 23 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 24 | ) 25 | 26 | // This will likely become a topological sort in the future, for owner refs. 27 | // For now, we implement a simple kind-based heuristic for the sort. 28 | 29 | func DefaultObjectOrder(ctx context.Context) func(o *manifest.Object) int { 30 | log := log.FromContext(ctx) 31 | 32 | return func(o *manifest.Object) int { 33 | gk := o.Group + "/" + o.Kind 34 | switch gk { 35 | // Create CRDs asap - both because they are slow and because we will likely create instances of them soon 36 | case "apiextensions.k8s.io/CustomResourceDefinition": 37 | return -1000 38 | 39 | // Namespaces need to be created before any other resources 40 | case "/Namespace": 41 | return 0 42 | // We need to create ServiceAccounts, Roles before we bind them with a RoleBinding 43 | case "/ServiceAccount", "rbac.authorization.k8s.io/ClusterRole": 44 | return 1 45 | case "rbac.authorization.k8s.io/ClusterRoleBinding": 46 | return 2 47 | 48 | // Pods might need configmap or secrets - avoid backoff by creating them first 49 | case "/ConfigMap", "/Secret": 50 | return 100 51 | 52 | // Create the pods after we've created other things they might be waiting for 53 | case "extensions/Deployment", "apps/Deployment": 54 | return 1000 55 | 56 | // Autoscalers typically act on a deployment 57 | case "autoscaling/HorizontalPodAutoscaler": 58 | return 1001 59 | 60 | // Create services late - after pods have been started 61 | case "/Service": 62 | return 10000 63 | 64 | default: 65 | log.WithValues("group", o.Group).WithValues("kind", o.Kind).V(2).Info("unknown group / kind") 66 | return 1000 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/patterns/addon/application.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 addon 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | 23 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/utils" 24 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" 25 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 26 | ) 27 | 28 | // Application Constants 29 | const ( 30 | // Used to indicate that not all of application's components 31 | // have been deployed yet. 32 | Pending = "Pending" 33 | // Used to indicate that all of application's components 34 | // have already been deployed. 35 | Succeeded = "Succeeded" 36 | // Used to indicate that deployment of application's components 37 | // failed. Some components might be present, but deployment of 38 | // the remaining ones will not be re-attempted. 39 | Failed = "Failed" 40 | ) 41 | 42 | // TransformApplicationFromStatus modifies the Application in the deployment based off the CommonStatus 43 | func TransformApplicationFromStatus(ctx context.Context, instance declarative.DeclarativeObject, objects *manifest.Objects) error { 44 | status, err := utils.GetCommonStatus(instance) 45 | if err != nil { 46 | return err 47 | } 48 | healthy := status.Healthy 49 | 50 | spec, err := utils.GetCommonSpec(instance) 51 | if err != nil { 52 | return err 53 | } 54 | version := spec.Version 55 | 56 | app, err := declarative.ExtractApplication(objects) 57 | if err != nil { 58 | return err 59 | } 60 | if app == nil { 61 | return errors.New("cannot TransformApplicationFromStatus without an app.k8s.io/Application in the manifest") 62 | } 63 | 64 | assemblyPhase := Pending 65 | if healthy { 66 | assemblyPhase = Succeeded 67 | } 68 | 69 | // TODO: Version should be on CommonStatus as well 70 | app.SetNestedField(version, "spec", "descriptor", "version") 71 | app.SetNestedField(assemblyPhase, "spec", "assemblyPhase") 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /mockkubeapiserver/apigrouplist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 mockkubeapiserver 18 | 19 | import ( 20 | "context" 21 | "sort" 22 | 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | ) 25 | 26 | // apiGroupList is a request for api discovery, such as GET /apis 27 | type apiGroupList struct { 28 | baseRequest 29 | } 30 | 31 | // Run serves the GET /apis endpoint 32 | func (r *apiGroupList) Run(ctx context.Context, s *MockKubeAPIServer) error { 33 | groupMap := make(map[string]*metav1.APIGroup) 34 | for _, resource := range s.storage.AllResources() { 35 | if resource.Group == "" { 36 | // core API does not appear in this list 37 | continue 38 | } 39 | group := groupMap[resource.Group] 40 | if group == nil { 41 | group = &metav1.APIGroup{Name: resource.Group} 42 | groupMap[resource.Group] = group 43 | } 44 | 45 | foundVersion := false 46 | for _, version := range group.Versions { 47 | if version.Version == resource.Version { 48 | foundVersion = true 49 | } 50 | } 51 | if !foundVersion { 52 | group.Versions = append(group.Versions, metav1.GroupVersionForDiscovery{ 53 | GroupVersion: resource.Group + "/" + resource.Version, 54 | Version: resource.Version, 55 | }) 56 | } 57 | } 58 | 59 | for _, group := range groupMap { 60 | sort.Slice(group.Versions, func(i, j int) bool { 61 | return group.Versions[i].Version < group.Versions[j].Version 62 | }) 63 | } 64 | 65 | var groupKeys []string 66 | for key := range groupMap { 67 | groupKeys = append(groupKeys, key) 68 | } 69 | sort.Strings(groupKeys) 70 | 71 | response := &metav1.APIGroupList{} 72 | response.Kind = "APIGroupList" 73 | response.APIVersion = "v1" 74 | for _, groupKey := range groupKeys { 75 | group := groupMap[groupKey] 76 | // Assume preferred version is newest 77 | group.PreferredVersion = group.Versions[len(group.Versions)-1] 78 | response.Groups = append(response.Groups, *group) 79 | } 80 | return r.writeResponse(response) 81 | } 82 | -------------------------------------------------------------------------------- /applylib/applyset/parentref.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes 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 | // This package provides Parent object related methods. 17 | package applyset 18 | 19 | import ( 20 | "k8s.io/apimachinery/pkg/api/meta" 21 | "k8s.io/apimachinery/pkg/runtime" 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | ) 24 | 25 | // Parent is aimed for adaption. We want users to us Parent rather than the third-party/forked ParentRef directly. 26 | // This gives us more flexibility to change the third-party/forked without introducing breaking changes to users. 27 | type Parent interface { 28 | GroupVersionKind() schema.GroupVersionKind 29 | Name() string 30 | Namespace() string 31 | RESTMapping() *meta.RESTMapping 32 | GetSubject() runtime.Object 33 | } 34 | 35 | // NewParentRef initialize a ParentRef object. 36 | func NewParentRef(object runtime.Object, name, namespace string, rest *meta.RESTMapping) *ParentRef { 37 | return &ParentRef{ 38 | object: object, 39 | name: name, 40 | namespace: namespace, 41 | restMapping: rest, 42 | } 43 | } 44 | 45 | var _ Parent = &ParentRef{} 46 | 47 | // ParentRef defines the Parent object information 48 | type ParentRef struct { 49 | namespace string 50 | name string 51 | restMapping *meta.RESTMapping 52 | object runtime.Object 53 | } 54 | 55 | // GroupVersionKind returns the parent GroupVersionKind 56 | func (p *ParentRef) GroupVersionKind() schema.GroupVersionKind { 57 | return p.object.GetObjectKind().GroupVersionKind() 58 | } 59 | 60 | // Name returns the parent Name 61 | func (p *ParentRef) Name() string { 62 | return p.name 63 | } 64 | 65 | // Namespace returns the parent Namespace 66 | func (p *ParentRef) Namespace() string { 67 | return p.namespace 68 | } 69 | 70 | // RESTMapping returns the parent RESTMapping 71 | func (p *ParentRef) RESTMapping() *meta.RESTMapping { 72 | return p.restMapping 73 | } 74 | 75 | // GetSubject returns the parent runtime.Object 76 | func (p *ParentRef) GetSubject() runtime.Object { 77 | return p.object 78 | } 79 | -------------------------------------------------------------------------------- /ktest/go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/kubebuilder-declarative-pattern/ktest 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/google/go-cmp v0.6.0 9 | k8s.io/apimachinery v0.32.1 10 | k8s.io/client-go v0.32.1 11 | k8s.io/klog/v2 v2.130.1 12 | sigs.k8s.io/controller-runtime v0.20.4 13 | sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver v0.0.0-20221021151406-9bd3fb842119 14 | sigs.k8s.io/yaml v1.4.0 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 19 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 20 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 21 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 22 | github.com/go-logr/logr v1.4.2 // indirect 23 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 24 | github.com/go-openapi/jsonreference v0.21.0 // indirect 25 | github.com/go-openapi/swag v0.23.0 // indirect 26 | github.com/gogo/protobuf v1.3.2 // indirect 27 | github.com/golang/protobuf v1.5.4 // indirect 28 | github.com/google/gnostic-models v0.6.8 // indirect 29 | github.com/google/gofuzz v1.2.0 // indirect 30 | github.com/google/uuid v1.6.0 // indirect 31 | github.com/josharian/intern v1.0.0 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/mailru/easyjson v0.7.7 // indirect 34 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 35 | github.com/modern-go/reflect2 v1.0.2 // indirect 36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 37 | github.com/pkg/errors v0.9.1 // indirect 38 | github.com/prometheus/client_golang v1.20.4 // indirect 39 | github.com/prometheus/common v0.59.1 // indirect 40 | github.com/spf13/pflag v1.0.5 // indirect 41 | github.com/x448/float16 v0.8.4 // indirect 42 | golang.org/x/net v0.30.0 // indirect 43 | golang.org/x/oauth2 v0.23.0 // indirect 44 | golang.org/x/sys v0.26.0 // indirect 45 | golang.org/x/term v0.25.0 // indirect 46 | golang.org/x/text v0.19.0 // indirect 47 | golang.org/x/time v0.7.0 // indirect 48 | google.golang.org/protobuf v1.35.1 // indirect 49 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 50 | gopkg.in/inf.v0 v0.9.1 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | k8s.io/api v0.32.1 // indirect 53 | k8s.io/apiextensions-apiserver v0.32.1 // indirect 54 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 55 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 56 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 57 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /pkg/test/testreconciler/simpletest/v1alpha1/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes 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 | addonv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" 22 | ) 23 | 24 | // SimpleTestSpec defines the desired state of Guestbook 25 | type SimpleTestSpec struct { 26 | addonv1alpha1.CommonSpec `json:",inline"` 27 | addonv1alpha1.PatchSpec `json:",inline"` 28 | } 29 | 30 | // SimpleTestStatus defines the observed state of Guestbook 31 | type SimpleTestStatus struct { 32 | addonv1alpha1.CommonStatus `json:",inline"` 33 | addonv1alpha1.StatusConditions `json:",inline"` 34 | } 35 | 36 | //+kubebuilder:object:root=true 37 | //+kubebuilder:subresource:status 38 | 39 | // SimpleTest is the Schema for the simpletest API 40 | type SimpleTest struct { 41 | metav1.TypeMeta `json:",inline"` 42 | metav1.ObjectMeta `json:"metadata,omitempty"` 43 | 44 | Spec SimpleTestSpec `json:"spec,omitempty"` 45 | Status SimpleTestStatus `json:"status,omitempty"` 46 | } 47 | 48 | var _ addonv1alpha1.CommonObject = &SimpleTest{} 49 | var _ addonv1alpha1.Patchable = &SimpleTest{} 50 | 51 | func (o *SimpleTest) ComponentName() string { 52 | return "simpletest" 53 | } 54 | 55 | func (o *SimpleTest) CommonSpec() addonv1alpha1.CommonSpec { 56 | return o.Spec.CommonSpec 57 | } 58 | 59 | func (o *SimpleTest) PatchSpec() addonv1alpha1.PatchSpec { 60 | return o.Spec.PatchSpec 61 | } 62 | 63 | func (o *SimpleTest) GetCommonStatus() addonv1alpha1.CommonStatus { 64 | return o.Status.CommonStatus 65 | } 66 | 67 | func (o *SimpleTest) SetCommonStatus(s addonv1alpha1.CommonStatus) { 68 | o.Status.CommonStatus = s 69 | } 70 | 71 | //+kubebuilder:object:root=true 72 | 73 | // SimpleTestList contains a list of Guestbook 74 | type SimpleTestList struct { 75 | metav1.TypeMeta `json:",inline"` 76 | metav1.ListMeta `json:"metadata,omitempty"` 77 | Items []SimpleTest `json:"items"` 78 | } 79 | 80 | func init() { 81 | SchemeBuilder.Register(&SimpleTest{}, &SimpleTestList{}) 82 | } 83 | -------------------------------------------------------------------------------- /mockkubeapiserver/storage/memorystorage/schema_test.go: -------------------------------------------------------------------------------- 1 | package memorystorage 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver/storage" 13 | "sigs.k8s.io/yaml" 14 | ) 15 | 16 | func TestApply(t *testing.T) { 17 | ctx := context.TODO() 18 | 19 | gvk := schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"} 20 | memoryStorage, err := NewMemoryStorage(storage.NewTestClock(), storage.NewTestUIDGenerator()) 21 | if err != nil { 22 | t.Fatalf("NewMemoryStorage failed: %v", err) 23 | } 24 | 25 | resource := memoryStorage.findResourceByGVK(gvk) 26 | if resource == nil { 27 | t.Fatalf("findResourceByGVK(%v) unexpectedly returned nil", gvk) 28 | } 29 | 30 | liveYAML := ` 31 | apiVersion: v1 32 | kind: ConfigMap 33 | metadata: 34 | name: cm1 35 | namespace: default 36 | data: 37 | foo1: bar1 38 | ` 39 | 40 | liveObj := &unstructured.Unstructured{} 41 | if err := yaml.Unmarshal([]byte(liveYAML), liveObj); err != nil { 42 | t.Fatalf("error parsing yaml: %v", err) 43 | } 44 | 45 | patchYAML := ` 46 | apiVersion: v1 47 | kind: ConfigMap 48 | metadata: 49 | name: cm1 50 | namespace: default 51 | data: 52 | foo2: bar2 53 | ` 54 | 55 | applyOptions := metav1.PatchOptions{ 56 | FieldManager: "foo", 57 | } 58 | 59 | mergedObject, changed, err := storage.DoServerSideApply(ctx, resource, liveObj, []byte(patchYAML), applyOptions) 60 | if err != nil { 61 | t.Fatalf("DoServerSideApply gave error: %v", err) 62 | } 63 | if !changed { 64 | t.Fatalf("DoServerSideApply indicated object was not changed; expected change") 65 | } 66 | 67 | gotYAML, err := yaml.Marshal(mergedObject) 68 | if err != nil { 69 | t.Fatalf("error marshaling yaml: %v", err) 70 | } 71 | 72 | want := ` 73 | apiVersion: v1 74 | data: 75 | foo1: bar1 76 | foo2: bar2 77 | kind: ConfigMap 78 | metadata: 79 | managedFields: 80 | - apiVersion: v1 81 | fieldsType: FieldsV1 82 | fieldsV1: 83 | f:apiVersion: {} 84 | f:data: 85 | f:foo2: {} 86 | f:kind: {} 87 | f:metadata: 88 | f:name: {} 89 | f:namespace: {} 90 | manager: foo 91 | operation: Apply 92 | name: cm1 93 | namespace: default 94 | ` 95 | 96 | got := strings.TrimSpace(string(gotYAML)) 97 | want = strings.TrimSpace(want) 98 | if diff := cmp.Diff(want, got); diff != "" { 99 | t.Errorf("unexpected diff in result: %v", diff) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/patterns/addon/patch.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 addon 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "fmt" 23 | 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | "k8s.io/apimachinery/pkg/util/yaml" 26 | "sigs.k8s.io/controller-runtime/pkg/log" 27 | 28 | addonsv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" 29 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" 30 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 31 | ) 32 | 33 | // ApplyPatches is an ObjectTransform to apply Patches specified on the Addon object to the manifest 34 | // This transform requires the DeclarativeObject to implement addonsv1alpha1.Patchable 35 | func ApplyPatches(ctx context.Context, object declarative.DeclarativeObject, objects *manifest.Objects) error { 36 | log := log.FromContext(ctx) 37 | 38 | var patches []*unstructured.Unstructured 39 | 40 | unstruct, ok := object.(*unstructured.Unstructured) 41 | if ok { 42 | patch, _, err := unstructured.NestedSlice(unstruct.Object, "spec", "patches") 43 | if err != nil { 44 | return fmt.Errorf("unable to get patches from unstructured: %v", err) 45 | } 46 | 47 | for _, p := range patch { 48 | m := p.(map[string]interface{}) 49 | patches = append(patches, &unstructured.Unstructured{ 50 | Object: m, 51 | }) 52 | } 53 | } else if p, ok := object.(addonsv1alpha1.Patchable); ok { 54 | for _, p := range p.PatchSpec().Patches { 55 | // Object is nil, Raw is populated (with json, even when input was yaml) 56 | r := bytes.NewReader(p.Raw) 57 | decoder := yaml.NewYAMLOrJSONDecoder(r, 1024) 58 | patch := &unstructured.Unstructured{} 59 | 60 | if err := decoder.Decode(patch); err != nil { 61 | log.Info("invalid patch", "patch", string(p.Raw)) 62 | return fmt.Errorf("error parsing patch json into unstructured object: %v", err) 63 | } 64 | log.WithValues("patch", patch).V(1).Info("parsed patch") 65 | 66 | patches = append(patches, patch) 67 | } 68 | } else { 69 | return fmt.Errorf("provided object (%T) does not implement Patchable type", object) 70 | } 71 | 72 | return objects.Patch(ctx, patches) 73 | } 74 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | apierrors "k8s.io/apimachinery/pkg/api/errors" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/klog/v2" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | const ( 17 | dnsDomain = "cluster.local" 18 | dnsIP = "10.96.0.10" 19 | ) 20 | 21 | // getKubernetesService fetches the Kubernetes Service 22 | func getKubernetesService(ctx context.Context, c client.Client) (*corev1.Service, error) { 23 | kubernetesService := &corev1.Service{} 24 | id := client.ObjectKey{Namespace: metav1.NamespaceDefault, Name: "kubernetes"} 25 | 26 | // Get the Kubernetes Service 27 | err := c.Get(ctx, id, kubernetesService) 28 | 29 | return kubernetesService, err 30 | } 31 | 32 | // FindDNSClusterIP tries to find the Cluster IP to be used by the DNS service 33 | // It is usually the 10th address to the Kubernetes Service Cluster IP 34 | // If the Kubernetes Service Cluster IP is not found, we default it to be "10.96.0.10" 35 | func FindDNSClusterIP(ctx context.Context, c client.Client) (string, error) { 36 | kubernetesService, err := getKubernetesService(ctx, c) 37 | if err != nil && !apierrors.IsNotFound(err) { 38 | return "", err 39 | } 40 | 41 | if apierrors.IsNotFound(err) { 42 | // If it cannot determine the Cluster IP, we default it to "10.96.0.10" 43 | return dnsIP, nil 44 | } 45 | 46 | ip := net.ParseIP(kubernetesService.Spec.ClusterIP) 47 | if ip == nil { 48 | return "", fmt.Errorf("cannot parse kubernetes ClusterIP %q", kubernetesService.Spec.ClusterIP) 49 | } 50 | 51 | // The kubernetes Service ClusterIP is the 1st IP in the Service Subnet. 52 | // Increment the right-most byte by 9 to get to the 10th address, canonically used for DNS. 53 | // This works for both IPV4, IPV6, and 16-byte IPV4 addresses. 54 | ip[len(ip)-1] += 9 55 | 56 | result := ip.String() 57 | klog.Infof("determined ClusterIP for cluster should be %q", result) 58 | return result, nil 59 | } 60 | 61 | // GetDNSDomain returns Kubernetes DNS cluster domain 62 | // If it cannot determine the domain, we default it to "cluster.local" 63 | // TODO (rajansandeep): find a better way to implement this? 64 | func GetDNSDomain() string { 65 | svc := "kubernetes.default.svc" 66 | 67 | cname, err := net.LookupCNAME(svc) 68 | if err != nil { 69 | // If it cannot determine the domain, we default it to "cluster.local" 70 | klog.Infof("could not determine the domain, the DNS Domain for the cluster will default to %q", dnsDomain) 71 | return dnsDomain 72 | } 73 | 74 | domain := strings.TrimPrefix(cname, svc) 75 | domain = strings.TrimPrefix(domain, ".") 76 | domain = strings.TrimSuffix(domain, ".") 77 | 78 | klog.Infof("determined DNS Domain for DNS should be %q", domain) 79 | 80 | return domain 81 | } 82 | -------------------------------------------------------------------------------- /commonclient/factory_cr11.go: -------------------------------------------------------------------------------- 1 | //go:build controllerruntime_11 || controllerruntime_12 || controllerruntime_13 || controllerruntime_14 2 | 3 | package commonclient 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | 9 | "k8s.io/client-go/rest" 10 | "k8s.io/client-go/util/workqueue" 11 | "sigs.k8s.io/controller-runtime/pkg/cache" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | "sigs.k8s.io/controller-runtime/pkg/cluster" 14 | "sigs.k8s.io/controller-runtime/pkg/event" 15 | "sigs.k8s.io/controller-runtime/pkg/handler" 16 | "sigs.k8s.io/controller-runtime/pkg/source" 17 | ) 18 | 19 | // SourceKind is a version-indendenent abstraction over calling source.Kind 20 | func SourceKind(cache cache.Cache, obj client.Object) source.SyncingSource { 21 | return source.NewKindWithCache(obj, cache) 22 | } 23 | 24 | // WrapEventHandler is a version-indendenent abstraction over handler.EventHandler 25 | func WrapEventHandler(h EventHandler) handler.EventHandler { 26 | return &eventHandlerWithoutContext{h: h} 27 | } 28 | 29 | // GetHTTPClient returns the http.Client associated with the Cluster 30 | func GetHTTPClient(c cluster.Cluster) (*http.Client, error) { 31 | // This is a "polyfill", later versions of controller-runtime do it better. 32 | return rest.HTTPClientFor(c.GetConfig()) 33 | } 34 | 35 | type eventHandlerWithoutContext struct { 36 | h EventHandler 37 | } 38 | 39 | func (h *eventHandlerWithoutContext) Create(ev event.CreateEvent, q workqueue.RateLimitingInterface) { 40 | h.h.Create(context.TODO(), ev, q) 41 | } 42 | func (h *eventHandlerWithoutContext) Update(ev event.UpdateEvent, q workqueue.RateLimitingInterface) { 43 | h.h.Update(context.TODO(), ev, q) 44 | } 45 | func (h *eventHandlerWithoutContext) Delete(ev event.DeleteEvent, q workqueue.RateLimitingInterface) { 46 | h.h.Delete(context.TODO(), ev, q) 47 | } 48 | func (h *eventHandlerWithoutContext) Generic(ev event.GenericEvent, q workqueue.RateLimitingInterface) { 49 | h.h.Generic(context.TODO(), ev, q) 50 | } 51 | 52 | // EventHandler is the controller-runtime 0.15 version of EventHandler (with a context argument) 53 | type EventHandler interface { 54 | // Create is called in response to an create event - e.g. Pod Creation. 55 | Create(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) 56 | 57 | // Update is called in response to an update event - e.g. Pod Updated. 58 | Update(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface) 59 | 60 | // Delete is called in response to a delete event - e.g. Pod Deleted. 61 | Delete(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface) 62 | 63 | // Generic is called in response to an event of an unknown type or a synthetic event triggered as a cron or 64 | // external trigger request - e.g. reconcile Autoscaling, or a Webhook. 65 | Generic(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) 66 | } 67 | -------------------------------------------------------------------------------- /applylib/go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/kubebuilder-declarative-pattern/applylib 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | // Sometimes handy for development, but breaks usage as a library 8 | // Instead, please break apart commits to this module 9 | // replace sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver => ../mockkubeapiserver 10 | 11 | require ( 12 | github.com/google/go-cmp v0.6.0 13 | k8s.io/api v0.32.1 14 | k8s.io/apimachinery v0.32.1 15 | k8s.io/client-go v0.32.1 16 | k8s.io/klog/v2 v2.130.1 17 | sigs.k8s.io/controller-runtime v0.20.4 18 | sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver v0.0.0-20221021151406-9bd3fb842119 19 | sigs.k8s.io/kustomize/kstatus v0.0.2-0.20200509233124-065f70705d4d 20 | sigs.k8s.io/yaml v1.4.0 21 | ) 22 | 23 | require ( 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 25 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 26 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 27 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 28 | github.com/go-logr/logr v1.4.2 // indirect 29 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 30 | github.com/go-openapi/jsonreference v0.21.0 // indirect 31 | github.com/go-openapi/swag v0.23.0 // indirect 32 | github.com/gogo/protobuf v1.3.2 // indirect 33 | github.com/golang/protobuf v1.5.4 // indirect 34 | github.com/google/btree v1.1.3 // indirect 35 | github.com/google/gnostic-models v0.6.8 // indirect 36 | github.com/google/gofuzz v1.2.0 // indirect 37 | github.com/google/uuid v1.6.0 // indirect 38 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 39 | github.com/josharian/intern v1.0.0 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/mailru/easyjson v0.7.7 // indirect 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 43 | github.com/modern-go/reflect2 v1.0.2 // indirect 44 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 45 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 46 | github.com/pkg/errors v0.9.1 // indirect 47 | github.com/spf13/pflag v1.0.5 // indirect 48 | github.com/x448/float16 v0.8.4 // indirect 49 | golang.org/x/net v0.30.0 // indirect 50 | golang.org/x/oauth2 v0.23.0 // indirect 51 | golang.org/x/sys v0.26.0 // indirect 52 | golang.org/x/term v0.25.0 // indirect 53 | golang.org/x/text v0.19.0 // indirect 54 | golang.org/x/time v0.7.0 // indirect 55 | google.golang.org/protobuf v1.35.1 // indirect 56 | gopkg.in/inf.v0 v0.9.1 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 59 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 60 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 61 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /examples/guestbook-operator/config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: guestbook-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: guestbook- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | - manager_resource_patch.yaml 29 | # Protect the /metrics endpoint by putting it behind auth. 30 | # If you want your controller-manager to expose the /metrics 31 | # endpoint w/o any authn/z, please comment the following line. 32 | - manager_auth_proxy_patch.yaml 33 | 34 | # Mount the controller config file for loading manager configurations 35 | # through a ComponentConfig type 36 | #- manager_config_patch.yaml 37 | 38 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 39 | # crd/kustomization.yaml 40 | #- manager_webhook_patch.yaml 41 | 42 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 43 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 44 | # 'CERTMANAGER' needs to be enabled to use ca injection 45 | #- webhookcainjection_patch.yaml 46 | 47 | # the following config is for teaching kustomize how to do var substitution 48 | vars: 49 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 50 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 51 | # objref: 52 | # kind: Certificate 53 | # group: cert-manager.io 54 | # version: v1 55 | # name: serving-cert # this name should match the one in certificate.yaml 56 | # fieldref: 57 | # fieldpath: metadata.namespace 58 | #- name: CERTIFICATE_NAME 59 | # objref: 60 | # kind: Certificate 61 | # group: cert-manager.io 62 | # version: v1 63 | # name: serving-cert # this name should match the one in certificate.yaml 64 | #- name: SERVICE_NAMESPACE # namespace of the service 65 | # objref: 66 | # kind: Service 67 | # version: v1 68 | # name: webhook-service 69 | # fieldref: 70 | # fieldpath: metadata.namespace 71 | #- name: SERVICE_NAME 72 | # objref: 73 | # kind: Service 74 | # version: v1 75 | # name: webhook-service 76 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/status/version.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | 8 | semver "github.com/blang/semver/v4" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | "sigs.k8s.io/controller-runtime/pkg/log" 11 | 12 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/utils" 13 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" 14 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 15 | ) 16 | 17 | // NewVersionCheck provides an implementation of declarative.Reconciled that 18 | // checks the version of the operator if it is up to the version required by the manifest 19 | func NewVersionCheck(client client.Client, operatorVersionString string) (*versionCheck, error) { 20 | operatorVersion, err := semver.Parse(operatorVersionString) 21 | if err != nil { 22 | return nil, fmt.Errorf("unable to parse operator version %q: %v", operatorVersionString, err) 23 | } 24 | return &versionCheck{client: client, operatorVersion: operatorVersion}, nil 25 | } 26 | 27 | type versionCheck struct { 28 | client client.Client 29 | operatorVersion semver.Version 30 | } 31 | 32 | func (p *versionCheck) VersionCheck( 33 | ctx context.Context, 34 | src declarative.DeclarativeObject, 35 | objs *manifest.Objects, 36 | ) (bool, error) { 37 | log := log.FromContext(ctx) 38 | var minOperatorVersion semver.Version 39 | 40 | // Look for annotation from any resource with the max version 41 | for _, obj := range objs.Items { 42 | annotations := obj.UnstructuredObject().GetAnnotations() 43 | if versionNeededStr, ok := annotations["addons.k8s.io/min-operator-version"]; ok { 44 | log.WithValues("min-operator-version", versionNeededStr).Info("Got version requirement addons.k8s.io/operator-version") 45 | 46 | versionNeeded, err := semver.Parse(versionNeededStr) 47 | if err != nil { 48 | log.WithValues("version", versionNeededStr).Error(err, "Unable to parse version restriction") 49 | return false, err 50 | } 51 | 52 | if versionNeeded.GT(minOperatorVersion) { 53 | minOperatorVersion = versionNeeded 54 | } 55 | } 56 | } 57 | 58 | if p.operatorVersion.GTE(minOperatorVersion) { 59 | return true, nil 60 | } 61 | 62 | errors := []string{ 63 | fmt.Sprintf("manifest needs operator version >= %v, this operator is version %v", minOperatorVersion.String(), 64 | p.operatorVersion.String()), 65 | } 66 | 67 | currentStatus, err := utils.GetCommonStatus(src) 68 | if err != nil { 69 | log.Error(err, "getting status") 70 | return false, err 71 | } 72 | 73 | status := currentStatus 74 | status.Healthy = false 75 | status.Errors = errors 76 | 77 | if !reflect.DeepEqual(status, currentStatus) { 78 | err := utils.SetCommonStatus(src, status) 79 | if err != nil { 80 | log.Error(err, "unable to update status") 81 | } 82 | } 83 | 84 | return false, fmt.Errorf("operator not qualified, manifest needs operator version >= %v", minOperatorVersion.String()) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/utils/helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | addonsv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" 10 | ) 11 | 12 | func genError(v runtime.Object) error { 13 | return fmt.Errorf("instance %T is not addonsv1alpha1.CommonObject or unstructured", v) 14 | } 15 | 16 | func GetCommonStatus(instance runtime.Object) (addonsv1alpha1.CommonStatus, error) { 17 | switch v := instance.(type) { 18 | case addonsv1alpha1.CommonObject: 19 | return v.GetCommonStatus(), nil 20 | case *unstructured.Unstructured: 21 | unstructStatus, _, err := unstructured.NestedMap(v.Object, "status") 22 | if err != nil { 23 | return addonsv1alpha1.CommonStatus{}, fmt.Errorf("unable to get status from unstuctured: %v", err) 24 | } 25 | var addonStatus addonsv1alpha1.CommonStatus 26 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructStatus, &addonStatus) 27 | if err != nil { 28 | return addonStatus, err 29 | } 30 | 31 | return addonStatus, nil 32 | default: 33 | return addonsv1alpha1.CommonStatus{}, genError(v) 34 | } 35 | } 36 | 37 | func SetCommonStatus(instance runtime.Object, status addonsv1alpha1.CommonStatus) error { 38 | switch v := instance.(type) { 39 | case addonsv1alpha1.CommonObject: 40 | v.SetCommonStatus(status) 41 | case *unstructured.Unstructured: 42 | unstructStatus, err := runtime.DefaultUnstructuredConverter.ToUnstructured(status) 43 | if err != nil { 44 | return fmt.Errorf("unable to convert unstructured to addonStatus: %v", err) 45 | } 46 | 47 | err = unstructured.SetNestedMap(v.Object, unstructStatus, "status") 48 | if err != nil { 49 | return fmt.Errorf("unable to set status in unstructured: %v", err) 50 | } 51 | 52 | return nil 53 | default: 54 | return genError(v) 55 | } 56 | return nil 57 | } 58 | 59 | func GetCommonSpec(instance runtime.Object) (addonsv1alpha1.CommonSpec, error) { 60 | switch v := instance.(type) { 61 | case addonsv1alpha1.CommonObject: 62 | return v.CommonSpec(), nil 63 | case *unstructured.Unstructured: 64 | unstructSpec, _, err := unstructured.NestedMap(v.Object, "spec") 65 | if err != nil { 66 | return addonsv1alpha1.CommonSpec{}, fmt.Errorf("unable to get spec from unstuctured: %v", err) 67 | } 68 | var addonSpec addonsv1alpha1.CommonSpec 69 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructSpec, &addonSpec) 70 | if err != nil { 71 | return addonSpec, err 72 | } 73 | 74 | return addonSpec, nil 75 | default: 76 | return addonsv1alpha1.CommonSpec{}, genError(v) 77 | } 78 | } 79 | 80 | func GetCommonName(instance runtime.Object) (string, error) { 81 | switch v := instance.(type) { 82 | case addonsv1alpha1.CommonObject: 83 | return v.ComponentName(), nil 84 | case *unstructured.Unstructured: 85 | return strings.ToLower(v.GetKind()), nil 86 | default: 87 | return "", genError(v) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/guestbook-operator/api/v1alpha1/guestbook_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes 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 | addonv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // GuestbookSpec defines the desired state of Guestbook 28 | type GuestbookSpec struct { 29 | addonv1alpha1.CommonSpec `json:",inline"` 30 | addonv1alpha1.PatchSpec `json:",inline"` 31 | 32 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 33 | // Important: Run "make" to regenerate code after modifying this file 34 | } 35 | 36 | // GuestbookStatus defines the observed state of Guestbook 37 | type GuestbookStatus struct { 38 | addonv1alpha1.CommonStatus `json:",inline"` 39 | 40 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 41 | // Important: Run "make" to regenerate code after modifying this file 42 | } 43 | 44 | //+kubebuilder:object:root=true 45 | //+kubebuilder:subresource:status 46 | 47 | // Guestbook is the Schema for the guestbooks API 48 | type Guestbook struct { 49 | metav1.TypeMeta `json:",inline"` 50 | metav1.ObjectMeta `json:"metadata,omitempty"` 51 | 52 | Spec GuestbookSpec `json:"spec,omitempty"` 53 | Status GuestbookStatus `json:"status,omitempty"` 54 | } 55 | 56 | var _ addonv1alpha1.CommonObject = &Guestbook{} 57 | var _ addonv1alpha1.Patchable = &Guestbook{} 58 | 59 | func (o *Guestbook) ComponentName() string { 60 | return "guestbook" 61 | } 62 | 63 | func (o *Guestbook) CommonSpec() addonv1alpha1.CommonSpec { 64 | return o.Spec.CommonSpec 65 | } 66 | 67 | func (o *Guestbook) PatchSpec() addonv1alpha1.PatchSpec { 68 | return o.Spec.PatchSpec 69 | } 70 | 71 | func (o *Guestbook) GetCommonStatus() addonv1alpha1.CommonStatus { 72 | return o.Status.CommonStatus 73 | } 74 | 75 | func (o *Guestbook) SetCommonStatus(s addonv1alpha1.CommonStatus) { 76 | o.Status.CommonStatus = s 77 | } 78 | 79 | //+kubebuilder:object:root=true 80 | 81 | // GuestbookList contains a list of Guestbook 82 | type GuestbookList struct { 83 | metav1.TypeMeta `json:",inline"` 84 | metav1.ListMeta `json:"metadata,omitempty"` 85 | Items []Guestbook `json:"items"` 86 | } 87 | 88 | func init() { 89 | SchemeBuilder.Register(&Guestbook{}, &GuestbookList{}) 90 | } 91 | -------------------------------------------------------------------------------- /reconciler-options.md: -------------------------------------------------------------------------------- 1 | ## Reconciler Options in kubebuilder-declarative-pattern 2 | 3 | kubebuilder-declarative-pattern is structured in a way that makes it easy for you to turn functionality (provided in kubebuilder-declarative-pattern) on and off in the operator you have created. This also makes it easy to add new functionality to your operator. This README serves as a reference for these functionalities and indicates which ones are enabled by default. 4 | 5 | The options are: 6 | ## WithRawManifestOperation 7 | WithRawManifestOperation takes in a set of functions that transforms raw string manifests before applying it. 8 | The functions should be of the form: 9 | ``` 10 | type ManifestOperation = func(context.Context, DeclarativeObject, string) (string, error) 11 | ``` 12 | 13 | ## WithObjectTransform 14 | WithObjectTransform takes in a set of functions that transforms the manifest objects before applying it 15 | The functions should be of the form: 16 | ``` 17 | type ObjectTransform = func(context.Context, DeclarativeObject, *manifest.Objects) error 18 | ``` 19 | 20 | ## WithManifestController 21 | WithManifestController overrides the default source for loading manifests. 22 | 23 | ## WithApplyPrune 24 | WithApplyPrune turns on the --prune behavior of kubectl apply. This behavior deletes any objects that exist in the API server that are not deployed by the current version of the manifest which match a label specific to the addon instance. 25 | This option requires [WithLabels](#withlabels) to be used. 26 | 27 | ## WithOwner 28 | WithOwner sets an owner ref on each deployed object by the [OwnerSelector]. 29 | 30 | ## WithLabels 31 | WithLabels sets a fixed set of labels configured provided by a LabelMaker to all deployment objecs for a given DeclarativeObject 32 | 33 | ## WithStatus 34 | WithStatus provides a [Status] interface that will be used during Reconcile. 35 | 36 | ## WithPreserveNamespace 37 | WithPreserveNamespace preserves the namespaces defined in the deployment manifest 38 | instead of matching the namespace of the DeclarativeObject 39 | 40 | ## WithApplyKustomize 41 | WithApplyKustomize runs the kustomize build to create final manifest. This feature needs the go dependency `kustomize/api`. 42 | If you do not need kustomize or want to use a conflict version of `kustomize/api`, you can opt out the kustomize and 43 | the `kustomize/api` dependency via go build tag `without_kustomize`. e.g. `go run ./main.go -tags without_kustomize` 44 | 45 | ## WithManagedApplication 46 | WithManagedApplication is a transform that will modify the Application object in the deployment to match the configuration of the rest of the deployment. 47 | 48 | ## WithApplyValidation 49 | WithApplyValidation enables validation with kubectl apply 50 | 51 | ## WithReconcileMetrics 52 | WithReconcileMetrics enables metrics of declarative reconciler. 53 | 54 | 55 | [OwnerSelector]: https://github.com/kubernetes-sigs/kubebuilder-declarative-pattern/blob/master/pkg/patterns/declarative/options.go#L74 56 | [Status]: https://github.com/kubernetes-sigs/kubebuilder-declarative-pattern/blob/master/pkg/patterns/declarative/status.go#L26 57 | -------------------------------------------------------------------------------- /mockkubeapiserver/hooks/deployment.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes 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 hooks 18 | 19 | import ( 20 | "fmt" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "k8s.io/klog/v2" 25 | "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver/storage" 26 | ) 27 | 28 | type DeploymentHook struct { 29 | } 30 | 31 | func (s *DeploymentHook) OnWatchEvent(ev *storage.WatchEvent) { 32 | switch ev.GroupKind() { 33 | case schema.GroupKind{Group: "apps", Kind: "Deployment"}: 34 | if err := s.deploymentChanged(ev); err != nil { 35 | klog.Fatalf("could not update deployment status: %v", err) 36 | } 37 | } 38 | } 39 | 40 | // Fake the required transitions for Deployment objects so they become ready 41 | func (s *DeploymentHook) deploymentChanged(ev *storage.WatchEvent) error { 42 | u := ev.Unstructured() 43 | 44 | // So that deployments become ready, we immediately update the status. 45 | // We could do something better here, like e.g. a 1 second pause before changing the status 46 | statusObj := u.Object["status"] 47 | if statusObj == nil { 48 | statusObj = make(map[string]interface{}) 49 | u.Object["status"] = statusObj 50 | } 51 | status, ok := statusObj.(map[string]interface{}) 52 | if !ok { 53 | return fmt.Errorf("status was of unexpected type %T", statusObj) 54 | } 55 | 56 | replicasVal, _, err := unstructured.NestedFieldNoCopy(u.Object, "spec", "replicas") 57 | if err != nil { 58 | return fmt.Errorf("error getting spec.replicas: %w", err) 59 | } 60 | replicas := int64(0) 61 | switch replicasVal := replicasVal.(type) { 62 | case int64: 63 | replicas = replicasVal 64 | case float64: 65 | replicas = int64(replicasVal) 66 | default: 67 | return fmt.Errorf("unhandled type for spec.replicas %T", replicasVal) 68 | } 69 | 70 | var conditions []interface{} 71 | conditions = append(conditions, map[string]interface{}{ 72 | "type": "Available", 73 | "status": "True", 74 | "reason": "MinimumReplicasAvailable", 75 | }) 76 | conditions = append(conditions, map[string]interface{}{ 77 | "type": "Progressing", 78 | "status": "True", 79 | "reason": "NewReplicaSetAvailable", 80 | }) 81 | status["conditions"] = conditions 82 | 83 | status["availableReplicas"] = replicas 84 | status["readyReplicas"] = replicas 85 | status["replicas"] = replicas 86 | status["updatedReplicas"] = replicas 87 | 88 | observedGeneration := u.GetGeneration() 89 | status["observedGeneration"] = observedGeneration 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /pkg/patterns/declarative/application.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 | // application.go manages an Application[1] 18 | // 19 | // [1] https://github.com/kubernetes-sigs/application 20 | package declarative 21 | 22 | import ( 23 | "context" 24 | "errors" 25 | "sort" 26 | 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | 29 | "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" 30 | ) 31 | 32 | func transformApplication(ctx context.Context, instance DeclarativeObject, objects *manifest.Objects, labelMaker LabelMaker) error { 33 | app, err := ExtractApplication(objects) 34 | if err != nil { 35 | return err 36 | } 37 | if app == nil { 38 | return errors.New("cannot transformApplication without an app.k8s.io/Application in the manifest") 39 | } 40 | 41 | labels := labelMaker(ctx, instance) 42 | convertedLabels := map[string]interface{}{} 43 | for k, v := range labels { 44 | convertedLabels[k] = v 45 | } 46 | labelSelector := map[string]interface{}{"matchLabels": convertedLabels} 47 | app.SetNestedField(labelSelector, "spec", "selector") 48 | 49 | componentGroupKinds := []interface{}{} 50 | for _, gk := range uniqueGroupKind(objects) { 51 | componentGroupKinds = append(componentGroupKinds, map[string]interface{}{"group": gk.Group, "kind": gk.Kind}) 52 | } 53 | app.SetNestedSlice(componentGroupKinds, "spec", "componentGroupKinds") 54 | 55 | return nil 56 | } 57 | 58 | // uniqueGroupKind returns all unique GroupKind defined in objects 59 | func uniqueGroupKind(objects *manifest.Objects) []metav1.GroupKind { 60 | kinds := map[metav1.GroupKind]struct{}{} 61 | for _, o := range objects.Items { 62 | gk := o.GroupKind() 63 | kinds[metav1.GroupKind{Group: gk.Group, Kind: gk.Kind}] = struct{}{} 64 | } 65 | var unique []metav1.GroupKind 66 | for gk := range kinds { 67 | unique = append(unique, gk) 68 | } 69 | sort.Slice(unique, func(i, j int) bool { 70 | return unique[i].String() < unique[j].String() 71 | }) 72 | return unique 73 | } 74 | 75 | // ExtractApplication extracts a single app.k8s.io/Application from objects. 76 | // 77 | // - 0 Application: (nil, nil) 78 | // - 1 Application: (*app, nil) 79 | // - >1 Application: (nil, err) 80 | func ExtractApplication(objects *manifest.Objects) (*manifest.Object, error) { 81 | var app *manifest.Object 82 | for _, o := range objects.Items { 83 | if o.Group == "app.k8s.io" && o.Kind == "Application" { 84 | if app != nil { 85 | return nil, errors.New("multiple app.k8s.io/Application found in manifest") 86 | } 87 | app = o 88 | } 89 | } 90 | return app, nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/patterns/addon/pkg/apis/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by main. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 30 | func (in *CommonStatus) DeepCopyInto(out *CommonStatus) { 31 | *out = *in 32 | if in.Errors != nil { 33 | in, out := &in.Errors, &out.Errors 34 | *out = make([]string, len(*in)) 35 | copy(*out, *in) 36 | } 37 | 38 | return 39 | } 40 | 41 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonStatus. 42 | func (in *StatusConditions) DeepCopy() *StatusConditions { 43 | if in == nil { 44 | return nil 45 | } 46 | out := new(StatusConditions) 47 | in.DeepCopyInto(out) 48 | return out 49 | } 50 | 51 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 52 | func (in *StatusConditions) DeepCopyInto(out *StatusConditions) { 53 | *out = *in 54 | if in.Conditions != nil { 55 | in, out := &in.Conditions, &out.Conditions 56 | *out = make([]metav1.Condition, len(*in)) 57 | for i := range *in { 58 | (*in)[i].DeepCopyInto(&(*out)[i]) 59 | } 60 | } 61 | return 62 | } 63 | 64 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonStatus. 65 | func (in *CommonStatus) DeepCopy() *CommonStatus { 66 | if in == nil { 67 | return nil 68 | } 69 | out := new(CommonStatus) 70 | in.DeepCopyInto(out) 71 | return out 72 | } 73 | 74 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 75 | func (in *PatchSpec) DeepCopyInto(out *PatchSpec) { 76 | *out = *in 77 | if in.Patches != nil { 78 | in, out := &in.Patches, &out.Patches 79 | *out = make([]*runtime.RawExtension, len(*in)) 80 | for i := range *in { 81 | if (*in)[i] != nil { 82 | in, out := &(*in)[i], &(*out)[i] 83 | *out = new(runtime.RawExtension) 84 | (*in).DeepCopyInto(*out) 85 | } 86 | } 87 | } 88 | return 89 | } 90 | 91 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchSpec. 92 | func (in *PatchSpec) DeepCopy() *PatchSpec { 93 | if in == nil { 94 | return nil 95 | } 96 | out := new(PatchSpec) 97 | in.DeepCopyInto(out) 98 | return out 99 | } 100 | -------------------------------------------------------------------------------- /mockkubeapiserver/storage/memorystorage/crd.go: -------------------------------------------------------------------------------- 1 | package memorystorage 2 | 3 | import ( 4 | "fmt" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "k8s.io/apimachinery/pkg/types" 10 | "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver/storage" 11 | ) 12 | 13 | // UpdateCRD is called whenever a CRD is updated. 14 | // We register the types from the CRD. 15 | func (s *MemoryStorage) UpdateCRD(ev *storage.WatchEvent) error { 16 | // TODO: Deleted / changed CRDs 17 | 18 | u := ev.Unstructured() 19 | 20 | group, _, _ := unstructured.NestedString(u.Object, "spec", "group") 21 | if group == "" { 22 | return fmt.Errorf("spec.group not set") 23 | } 24 | 25 | kind, _, _ := unstructured.NestedString(u.Object, "spec", "names", "kind") 26 | if kind == "" { 27 | return fmt.Errorf("spec.names.kind not set") 28 | } 29 | 30 | resource, _, _ := unstructured.NestedString(u.Object, "spec", "names", "plural") 31 | if resource == "" { 32 | return fmt.Errorf("spec.names.plural not set") 33 | } 34 | 35 | scope, _, _ := unstructured.NestedString(u.Object, "spec", "scope") 36 | if scope == "" { 37 | return fmt.Errorf("spec.scope not set") 38 | } 39 | 40 | versionsObj, found, _ := unstructured.NestedFieldNoCopy(u.Object, "spec", "versions") 41 | if !found { 42 | return fmt.Errorf("spec.versions not set") 43 | } 44 | 45 | versions, ok := versionsObj.([]interface{}) 46 | if !ok { 47 | return fmt.Errorf("spec.versions not a slice") 48 | } 49 | 50 | for _, versionObj := range versions { 51 | version, ok := versionObj.(map[string]interface{}) 52 | if !ok { 53 | return fmt.Errorf("spec.versions element not an object") 54 | } 55 | 56 | versionName, _, _ := unstructured.NestedString(version, "name") 57 | if versionName == "" { 58 | return fmt.Errorf("version name not set") 59 | } 60 | gvk := schema.GroupVersionKind{Group: group, Version: versionName, Kind: kind} 61 | gvr := gvk.GroupVersion().WithResource(resource) 62 | gr := gvr.GroupResource() 63 | 64 | storage := &resourceStorage{ 65 | GroupResource: gr, 66 | objects: make(map[types.NamespacedName]*unstructured.Unstructured), 67 | parent: s, 68 | resourceVersionClock: &s.resourceVersionClock, 69 | } 70 | 71 | // TODO: share storage across different versions 72 | s.resourceStorages[gr] = storage 73 | 74 | r := &memoryResourceInfo{ 75 | api: metav1.APIResource{ 76 | Name: resource, 77 | Group: gvk.Group, 78 | Version: gvk.Version, 79 | Kind: gvk.Kind, 80 | }, 81 | gvk: gvk, 82 | gvr: gvr, 83 | parent: s, 84 | storage: storage, 85 | } 86 | r.listGVK = gvk.GroupVersion().WithKind(gvk.Kind + "List") 87 | 88 | // TODO: Set r.TypeInfo from schema 89 | 90 | switch scope { 91 | case "Namespaced": 92 | r.api.Namespaced = true 93 | case "Cluster": 94 | r.api.Namespaced = false 95 | default: 96 | return fmt.Errorf("scope %q is not recognized", scope) 97 | } 98 | 99 | s.schema.resources = append(s.schema.resources, r) 100 | } 101 | return nil 102 | } 103 | --------------------------------------------------------------------------------