From 41ce2a67f8ba380bf493d15cd2fa3af90ab981ec Mon Sep 17 00:00:00 2001 From: Camila Macedo Date: Sat, 26 Aug 2023 04:52:05 +0100 Subject: [PATCH] :bug: (deployimage/v1-beta1): fix scaffold with multigroup and add optional plugins to multigroup sample to validate changes --- .../deploy-image/v1alpha1/scaffolds/api.go | 31 +- test/testdata/generate.sh | 12 +- testdata/project-v4-multigroup/PROJECT | 40 + .../api/example.com/v1alpha1/busybox_types.go | 77 ++ .../example.com/v1alpha1/groupversion_info.go | 36 + .../example.com/v1alpha1/memcached_types.go | 80 ++ .../example.com/v1alpha1/memcached_webhook.go | 65 ++ .../v1alpha1/webhook_suite_test.go | 142 ++++ .../v1alpha1/zz_generated.deepcopy.go | 219 ++++++ testdata/project-v4-multigroup/cmd/main.go | 23 + ...example.com.testproject.org_busyboxes.yaml | 122 +++ ...xample.com.testproject.org_memcacheds.yaml | 127 ++++ .../config/crd/kustomization.yaml | 6 + .../cainjection_in_example.com_busyboxes.yaml | 7 + ...cainjection_in_example.com_memcacheds.yaml | 7 + .../webhook_in_example.com_busyboxes.yaml | 16 + .../webhook_in_example.com_memcacheds.yaml | 16 + .../config/manager/manager.yaml | 5 + .../rbac/example.com_busybox_editor_role.yaml | 31 + .../rbac/example.com_busybox_viewer_role.yaml | 27 + .../example.com_memcached_editor_role.yaml | 31 + .../example.com_memcached_viewer_role.yaml | 27 + .../config/rbac/role.yaml | 67 ++ .../samples/example.com_v1alpha1_busybox.yaml | 9 + .../example.com_v1alpha1_memcached.yaml | 10 + .../config/samples/kustomization.yaml | 2 + .../config/webhook/manifests.yaml | 20 + .../grafana/controller-resources-metrics.json | 306 ++++++++ .../grafana/controller-runtime-metrics.json | 710 ++++++++++++++++++ .../grafana/custom-metrics/config.yaml | 15 + .../example.com/busybox_controller.go | 434 +++++++++++ .../example.com/busybox_controller_test.go | 142 ++++ .../example.com/memcached_controller.go | 440 +++++++++++ .../example.com/memcached_controller_test.go | 143 ++++ .../controller/example.com/suite_test.go | 90 +++ 35 files changed, 3512 insertions(+), 23 deletions(-) create mode 100644 testdata/project-v4-multigroup/api/example.com/v1alpha1/busybox_types.go create mode 100644 testdata/project-v4-multigroup/api/example.com/v1alpha1/groupversion_info.go create mode 100644 testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_types.go create mode 100644 testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook.go create mode 100644 testdata/project-v4-multigroup/api/example.com/v1alpha1/webhook_suite_test.go create mode 100644 testdata/project-v4-multigroup/api/example.com/v1alpha1/zz_generated.deepcopy.go create mode 100644 testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_busyboxes.yaml create mode 100644 testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_memcacheds.yaml create mode 100644 testdata/project-v4-multigroup/config/crd/patches/cainjection_in_example.com_busyboxes.yaml create mode 100644 testdata/project-v4-multigroup/config/crd/patches/cainjection_in_example.com_memcacheds.yaml create mode 100644 testdata/project-v4-multigroup/config/crd/patches/webhook_in_example.com_busyboxes.yaml create mode 100644 testdata/project-v4-multigroup/config/crd/patches/webhook_in_example.com_memcacheds.yaml create mode 100644 testdata/project-v4-multigroup/config/rbac/example.com_busybox_editor_role.yaml create mode 100644 testdata/project-v4-multigroup/config/rbac/example.com_busybox_viewer_role.yaml create mode 100644 testdata/project-v4-multigroup/config/rbac/example.com_memcached_editor_role.yaml create mode 100644 testdata/project-v4-multigroup/config/rbac/example.com_memcached_viewer_role.yaml create mode 100644 testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_busybox.yaml create mode 100644 testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_memcached.yaml create mode 100644 testdata/project-v4-multigroup/grafana/controller-resources-metrics.json create mode 100644 testdata/project-v4-multigroup/grafana/controller-runtime-metrics.json create mode 100644 testdata/project-v4-multigroup/grafana/custom-metrics/config.yaml create mode 100644 testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller.go create mode 100644 testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller_test.go create mode 100644 testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller.go create mode 100644 testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller_test.go create mode 100644 testdata/project-v4-multigroup/internal/controller/example.com/suite_test.go diff --git a/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go index c02efe60524..6791edcb310 100644 --- a/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go +++ b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go @@ -134,7 +134,7 @@ func (s *apiScaffolder) Scaffold() error { if isGoV3 { defaultMainPath = "main.go" } - if err := s.updateMainByAddingEventRecorder(isGoV3, defaultMainPath); err != nil { + if err := s.updateMainByAddingEventRecorder(defaultMainPath); err != nil { return fmt.Errorf("error updating main.go: %v", err) } @@ -188,29 +188,16 @@ func (s *apiScaffolder) scaffoldCreateAPIFromPlugins(isLegacyLayout bool) error // TODO: replace this implementation by creating its own MainUpdater // which will have its own controller template which set the recorder so that we can use it // in the reconciliation to create an event inside for the finalizer -func (s *apiScaffolder) updateMainByAddingEventRecorder(isGoV3 bool, defaultMainPath string) error { - if isGoV3 { - if err := util.InsertCode( - defaultMainPath, - fmt.Sprintf( - `if err = (&controllers.%sReconciler{ +func (s *apiScaffolder) updateMainByAddingEventRecorder(defaultMainPath string) error { + if err := util.InsertCode( + defaultMainPath, + fmt.Sprintf( + `%sReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(),`, s.resource.Kind), - fmt.Sprintf(recorderTemplate, strings.ToLower(s.resource.Kind)), - ); err != nil { - return fmt.Errorf("error scaffolding event recorder in %s: %v", defaultMainPath, err) - } - } else { - if err := util.InsertCode( - defaultMainPath, - fmt.Sprintf( - `if err = (&controller.%sReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(),`, s.resource.Kind), - fmt.Sprintf(recorderTemplate, strings.ToLower(s.resource.Kind)), - ); err != nil { - return fmt.Errorf("error scaffolding event recorder in %s: %v", defaultMainPath, err) - } + fmt.Sprintf(recorderTemplate, strings.ToLower(s.resource.Kind)), + ); err != nil { + return fmt.Errorf("error scaffolding event recorder in %s: %v", defaultMainPath, err) } return nil diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index a145caece1a..1a4ba360003 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -102,6 +102,16 @@ function scaffold_test_project { $kb create api --version v1 --kind Lakers --controller=true --resource=true --make=false $kb create webhook --version v1 --kind Lakers --defaulting --programmatic-validation fi + + # Call ALL optional plugins within multigroup layout to ensure that they can work within + header_text 'Creating Memcached API with deploy-image plugin ...' + $kb create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:1.4.36-alpine --image-container-command="memcached,-m=64,-o,modern,-v" --image-container-port="11211" --run-as-user="1001" --plugins="deploy-image/v1-alpha" --make=false + $kb create api --group example.com --version v1alpha1 --kind Busybox --image=busybox:1.28 --plugins="deploy-image/v1-alpha" --make=false + header_text 'Creating Memcached webhook ...' + $kb create webhook --group example.com --version v1alpha1 --kind Memcached --programmatic-validation + + header_text 'Editing project with Grafana plugin ...' + $kb edit --plugins=grafana.kubebuilder.io/v1-alpha elif [[ $project =~ declarative ]]; then header_text 'Creating APIs ...' $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false @@ -114,7 +124,7 @@ function scaffold_test_project { header_text 'Creating Memcached webhook ...' $kb create webhook --group example.com --version v1alpha1 --kind Memcached --programmatic-validation elif [[ $project =~ "with-grafana" ]]; then - header_text 'Editing project with Grafana plugin ...' + header_text 'Editing project with Grafana plugin ...' $kb edit --plugins=grafana.kubebuilder.io/v1-alpha fi diff --git a/testdata/project-v4-multigroup/PROJECT b/testdata/project-v4-multigroup/PROJECT index 92f8bcfd7a5..12d9db3cd58 100644 --- a/testdata/project-v4-multigroup/PROJECT +++ b/testdata/project-v4-multigroup/PROJECT @@ -6,6 +6,25 @@ domain: testproject.org layout: - go.kubebuilder.io/v4 multigroup: true +plugins: + deploy-image.go.kubebuilder.io/v1-alpha: + resources: + - domain: testproject.org + group: example.com + kind: Memcached + options: + containerCommand: memcached,-m=64,-o,modern,-v + containerPort: "11211" + image: memcached:1.4.36-alpine + runAsUser: "1001" + version: v1alpha1 + - domain: testproject.org + group: example.com + kind: Busybox + options: + image: busybox:1.28 + version: v1alpha1 + grafana.kubebuilder.io/v1-alpha: {} projectName: project-v4-multigroup repo: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup resources: @@ -118,4 +137,25 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: testproject.org + group: example.com + kind: Memcached + path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1 + version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: testproject.org + group: example.com + kind: Busybox + path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1 + version: v1alpha1 version: "3" diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/busybox_types.go b/testdata/project-v4-multigroup/api/example.com/v1alpha1/busybox_types.go new file mode 100644 index 00000000000..440dd96baef --- /dev/null +++ b/testdata/project-v4-multigroup/api/example.com/v1alpha1/busybox_types.go @@ -0,0 +1,77 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// BusyboxSpec defines the desired state of Busybox +type BusyboxSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Size defines the number of Busybox instances + // The following markers will use OpenAPI v3 schema to validate the value + // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=3 + // +kubebuilder:validation:ExclusiveMaximum=false + Size int32 `json:"size,omitempty"` +} + +// BusyboxStatus defines the observed state of Busybox +type BusyboxStatus struct { + // Represents the observations of a Busybox's current state. + // Busybox.status.conditions.type are: "Available", "Progressing", and "Degraded" + // Busybox.status.conditions.status are one of True, False, Unknown. + // Busybox.status.conditions.reason the value should be a CamelCase string and producers of specific + // condition types may define expected values and meanings for this field, and whether the values + // are considered a guaranteed API. + // Busybox.status.conditions.Message is a human readable message indicating details about the transition. + // For further information see: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties + + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Busybox is the Schema for the busyboxes API +type Busybox struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BusyboxSpec `json:"spec,omitempty"` + Status BusyboxStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// BusyboxList contains a list of Busybox +type BusyboxList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Busybox `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Busybox{}, &BusyboxList{}) +} diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/groupversion_info.go b/testdata/project-v4-multigroup/api/example.com/v1alpha1/groupversion_info.go new file mode 100644 index 00000000000..9541616caff --- /dev/null +++ b/testdata/project-v4-multigroup/api/example.com/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the example.com v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=example.com.testproject.org +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "example.com.testproject.org", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_types.go b/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_types.go new file mode 100644 index 00000000000..aa82a833c00 --- /dev/null +++ b/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_types.go @@ -0,0 +1,80 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// MemcachedSpec defines the desired state of Memcached +type MemcachedSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Size defines the number of Memcached instances + // The following markers will use OpenAPI v3 schema to validate the value + // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=3 + // +kubebuilder:validation:ExclusiveMaximum=false + Size int32 `json:"size,omitempty"` + + // Port defines the port that will be used to init the container with the image + ContainerPort int32 `json:"containerPort,omitempty"` +} + +// MemcachedStatus defines the observed state of Memcached +type MemcachedStatus struct { + // Represents the observations of a Memcached's current state. + // Memcached.status.conditions.type are: "Available", "Progressing", and "Degraded" + // Memcached.status.conditions.status are one of True, False, Unknown. + // Memcached.status.conditions.reason the value should be a CamelCase string and producers of specific + // condition types may define expected values and meanings for this field, and whether the values + // are considered a guaranteed API. + // Memcached.status.conditions.Message is a human readable message indicating details about the transition. + // For further information see: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties + + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Memcached is the Schema for the memcacheds API +type Memcached struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MemcachedSpec `json:"spec,omitempty"` + Status MemcachedStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// MemcachedList contains a list of Memcached +type MemcachedList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Memcached `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Memcached{}, &MemcachedList{}) +} diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook.go b/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook.go new file mode 100644 index 00000000000..9dde8721abe --- /dev/null +++ b/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook.go @@ -0,0 +1,65 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var memcachedlog = logf.Log.WithName("memcached-resource") + +func (r *Memcached) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +//+kubebuilder:webhook:path=/validate-example-com-testproject-org-v1alpha1-memcached,mutating=false,failurePolicy=fail,sideEffects=None,groups=example.com.testproject.org,resources=memcacheds,verbs=create;update,versions=v1alpha1,name=vmemcached.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &Memcached{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Memcached) ValidateCreate() (admission.Warnings, error) { + memcachedlog.Info("validate create", "name", r.Name) + + // TODO(user): fill in your validation logic upon object creation. + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Memcached) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + memcachedlog.Info("validate update", "name", r.Name) + + // TODO(user): fill in your validation logic upon object update. + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Memcached) ValidateDelete() (admission.Warnings, error) { + memcachedlog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil, nil +} diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/webhook_suite_test.go b/testdata/project-v4-multigroup/api/example.com/v1alpha1/webhook_suite_test.go new file mode 100644 index 00000000000..48b751c4264 --- /dev/null +++ b/testdata/project-v4-multigroup/api/example.com/v1alpha1/webhook_suite_test.go @@ -0,0 +1,142 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "runtime" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + //+kubebuilder:scaffold:imports + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", + fmt.Sprintf("1.27.1-%s-%s", runtime.GOOS, runtime.GOARCH)), + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + }, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := apimachineryruntime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + LeaderElection: false, + MetricsBindAddress: "0", + }) + Expect(err).NotTo(HaveOccurred()) + + err = (&Memcached{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) + +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/zz_generated.deepcopy.go b/testdata/project-v4-multigroup/api/example.com/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..a1b201c4512 --- /dev/null +++ b/testdata/project-v4-multigroup/api/example.com/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,219 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Busybox) DeepCopyInto(out *Busybox) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Busybox. +func (in *Busybox) DeepCopy() *Busybox { + if in == nil { + return nil + } + out := new(Busybox) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Busybox) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BusyboxList) DeepCopyInto(out *BusyboxList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Busybox, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BusyboxList. +func (in *BusyboxList) DeepCopy() *BusyboxList { + if in == nil { + return nil + } + out := new(BusyboxList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BusyboxList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BusyboxSpec) DeepCopyInto(out *BusyboxSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BusyboxSpec. +func (in *BusyboxSpec) DeepCopy() *BusyboxSpec { + if in == nil { + return nil + } + out := new(BusyboxSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BusyboxStatus) DeepCopyInto(out *BusyboxStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BusyboxStatus. +func (in *BusyboxStatus) DeepCopy() *BusyboxStatus { + if in == nil { + return nil + } + out := new(BusyboxStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Memcached) DeepCopyInto(out *Memcached) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Memcached. +func (in *Memcached) DeepCopy() *Memcached { + if in == nil { + return nil + } + out := new(Memcached) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Memcached) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MemcachedList) DeepCopyInto(out *MemcachedList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Memcached, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedList. +func (in *MemcachedList) DeepCopy() *MemcachedList { + if in == nil { + return nil + } + out := new(MemcachedList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MemcachedList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MemcachedSpec) DeepCopyInto(out *MemcachedSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedSpec. +func (in *MemcachedSpec) DeepCopy() *MemcachedSpec { + if in == nil { + return nil + } + out := new(MemcachedSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MemcachedStatus) DeepCopyInto(out *MemcachedStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedStatus. +func (in *MemcachedStatus) DeepCopy() *MemcachedStatus { + if in == nil { + return nil + } + out := new(MemcachedStatus) + in.DeepCopyInto(out) + return out +} diff --git a/testdata/project-v4-multigroup/cmd/main.go b/testdata/project-v4-multigroup/cmd/main.go index c773770f22e..e891c0955d8 100644 --- a/testdata/project-v4-multigroup/cmd/main.go +++ b/testdata/project-v4-multigroup/cmd/main.go @@ -32,6 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1" + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/fiz/v1" foopolicyv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo.policy/v1" foov1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo/v1" @@ -44,6 +45,7 @@ import ( "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller" appscontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/apps" crewcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/crew" + examplecomcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/example.com" fizcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/fiz" foocontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/foo" foopolicycontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/foo.policy" @@ -70,6 +72,7 @@ func init() { utilruntime.Must(foov1.AddToScheme(scheme)) utilruntime.Must(fizv1.AddToScheme(scheme)) utilruntime.Must(testprojectorgv1.AddToScheme(scheme)) + utilruntime.Must(examplecomv1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -211,6 +214,26 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "Lakers") os.Exit(1) } + if err = (&examplecomcontroller.MemcachedReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("memcached-controller"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Memcached") + os.Exit(1) + } + if err = (&examplecomcontroller.BusyboxReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("busybox-controller"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Busybox") + os.Exit(1) + } + if err = (&examplecomv1alpha1.Memcached{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Memcached") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_busyboxes.yaml b/testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_busyboxes.yaml new file mode 100644 index 00000000000..a0e9a2c4d37 --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_busyboxes.yaml @@ -0,0 +1,122 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: busyboxes.example.com.testproject.org +spec: + group: example.com.testproject.org + names: + kind: Busybox + listKind: BusyboxList + plural: busyboxes + singular: busybox + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Busybox is the Schema for the busyboxes API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BusyboxSpec defines the desired state of Busybox + properties: + size: + description: 'Size defines the number of Busybox instances The following + markers will use OpenAPI v3 schema to validate the value More info: + https://book.kubebuilder.io/reference/markers/crd-validation.html' + format: int32 + maximum: 3 + minimum: 1 + type: integer + type: object + status: + description: BusyboxStatus defines the observed state of Busybox + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_memcacheds.yaml b/testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_memcacheds.yaml new file mode 100644 index 00000000000..d779aa1197b --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_memcacheds.yaml @@ -0,0 +1,127 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: memcacheds.example.com.testproject.org +spec: + group: example.com.testproject.org + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Memcached is the Schema for the memcacheds API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MemcachedSpec defines the desired state of Memcached + properties: + containerPort: + description: Port defines the port that will be used to init the container + with the image + format: int32 + type: integer + size: + description: 'Size defines the number of Memcached instances The following + markers will use OpenAPI v3 schema to validate the value More info: + https://book.kubebuilder.io/reference/markers/crd-validation.html' + format: int32 + maximum: 3 + minimum: 1 + type: integer + type: object + status: + description: MemcachedStatus defines the observed state of Memcached + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/testdata/project-v4-multigroup/config/crd/kustomization.yaml b/testdata/project-v4-multigroup/config/crd/kustomization.yaml index 633e725aa1a..c77c95dc29e 100644 --- a/testdata/project-v4-multigroup/config/crd/kustomization.yaml +++ b/testdata/project-v4-multigroup/config/crd/kustomization.yaml @@ -12,6 +12,8 @@ resources: - bases/foo.testproject.org_bars.yaml - bases/fiz.testproject.org_bars.yaml - bases/testproject.org_lakers.yaml +- bases/example.com.testproject.org_memcacheds.yaml +- bases/example.com.testproject.org_busyboxes.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: @@ -26,6 +28,8 @@ patches: #- path: patches/webhook_in_healthcheckpolicies.yaml #- path: patches/webhook_in_bars.yaml #- path: patches/webhook_in_lakers.yaml +#- path: patches/webhook_in_memcacheds.yaml +#- path: patches/webhook_in_busyboxes.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -39,6 +43,8 @@ patches: #- path: patches/cainjection_in_healthcheckpolicies.yaml #- path: patches/cainjection_in_bars.yaml #- path: patches/cainjection_in_lakers.yaml +#- path: patches/cainjection_in_memcacheds.yaml +#- path: patches/cainjection_in_busyboxes.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_example.com_busyboxes.yaml b/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_example.com_busyboxes.yaml new file mode 100644 index 00000000000..5f6b0384f48 --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_example.com_busyboxes.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: busyboxes.example.com.testproject.org diff --git a/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_example.com_memcacheds.yaml b/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_example.com_memcacheds.yaml new file mode 100644 index 00000000000..5b9e839364d --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_example.com_memcacheds.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: memcacheds.example.com.testproject.org diff --git a/testdata/project-v4-multigroup/config/crd/patches/webhook_in_example.com_busyboxes.yaml b/testdata/project-v4-multigroup/config/crd/patches/webhook_in_example.com_busyboxes.yaml new file mode 100644 index 00000000000..5dbd9da7176 --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/patches/webhook_in_example.com_busyboxes.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: busyboxes.example.com.testproject.org +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/testdata/project-v4-multigroup/config/crd/patches/webhook_in_example.com_memcacheds.yaml b/testdata/project-v4-multigroup/config/crd/patches/webhook_in_example.com_memcacheds.yaml new file mode 100644 index 00000000000..4a56b0f4c69 --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/patches/webhook_in_example.com_memcacheds.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: memcacheds.example.com.testproject.org +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/testdata/project-v4-multigroup/config/manager/manager.yaml b/testdata/project-v4-multigroup/config/manager/manager.yaml index b9d340945db..e36823baac8 100644 --- a/testdata/project-v4-multigroup/config/manager/manager.yaml +++ b/testdata/project-v4-multigroup/config/manager/manager.yaml @@ -72,6 +72,11 @@ spec: - --leader-elect image: controller:latest name: manager + env: + - name: BUSYBOX_IMAGE + value: busybox:1.28 + - name: MEMCACHED_IMAGE + value: memcached:1.4.36-alpine securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/testdata/project-v4-multigroup/config/rbac/example.com_busybox_editor_role.yaml b/testdata/project-v4-multigroup/config/rbac/example.com_busybox_editor_role.yaml new file mode 100644 index 00000000000..ceae6910a0e --- /dev/null +++ b/testdata/project-v4-multigroup/config/rbac/example.com_busybox_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit busyboxes. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: busybox-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: project-v4-multigroup + app.kubernetes.io/part-of: project-v4-multigroup + app.kubernetes.io/managed-by: kustomize + name: busybox-editor-role +rules: +- apiGroups: + - example.com.testproject.org + resources: + - busyboxes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - example.com.testproject.org + resources: + - busyboxes/status + verbs: + - get diff --git a/testdata/project-v4-multigroup/config/rbac/example.com_busybox_viewer_role.yaml b/testdata/project-v4-multigroup/config/rbac/example.com_busybox_viewer_role.yaml new file mode 100644 index 00000000000..b3f6d566507 --- /dev/null +++ b/testdata/project-v4-multigroup/config/rbac/example.com_busybox_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view busyboxes. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: busybox-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: project-v4-multigroup + app.kubernetes.io/part-of: project-v4-multigroup + app.kubernetes.io/managed-by: kustomize + name: busybox-viewer-role +rules: +- apiGroups: + - example.com.testproject.org + resources: + - busyboxes + verbs: + - get + - list + - watch +- apiGroups: + - example.com.testproject.org + resources: + - busyboxes/status + verbs: + - get diff --git a/testdata/project-v4-multigroup/config/rbac/example.com_memcached_editor_role.yaml b/testdata/project-v4-multigroup/config/rbac/example.com_memcached_editor_role.yaml new file mode 100644 index 00000000000..aacaa6ffb8a --- /dev/null +++ b/testdata/project-v4-multigroup/config/rbac/example.com_memcached_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit memcacheds. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: memcached-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: project-v4-multigroup + app.kubernetes.io/part-of: project-v4-multigroup + app.kubernetes.io/managed-by: kustomize + name: memcached-editor-role +rules: +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds/status + verbs: + - get diff --git a/testdata/project-v4-multigroup/config/rbac/example.com_memcached_viewer_role.yaml b/testdata/project-v4-multigroup/config/rbac/example.com_memcached_viewer_role.yaml new file mode 100644 index 00000000000..251117ddb78 --- /dev/null +++ b/testdata/project-v4-multigroup/config/rbac/example.com_memcached_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view memcacheds. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: memcached-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: project-v4-multigroup + app.kubernetes.io/part-of: project-v4-multigroup + app.kubernetes.io/managed-by: kustomize + name: memcached-viewer-role +rules: +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds + verbs: + - get + - list + - watch +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds/status + verbs: + - get diff --git a/testdata/project-v4-multigroup/config/rbac/role.yaml b/testdata/project-v4-multigroup/config/rbac/role.yaml index e2d30b23205..0744d29613d 100644 --- a/testdata/project-v4-multigroup/config/rbac/role.yaml +++ b/testdata/project-v4-multigroup/config/rbac/role.yaml @@ -30,6 +30,21 @@ rules: - get - patch - update +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch - apiGroups: - crew.testproject.org resources: @@ -56,6 +71,58 @@ rules: - get - patch - update +- apiGroups: + - example.com.testproject.org + resources: + - busyboxes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - example.com.testproject.org + resources: + - busyboxes/finalizers + verbs: + - update +- apiGroups: + - example.com.testproject.org + resources: + - busyboxes/status + verbs: + - get + - patch + - update +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds/finalizers + verbs: + - update +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds/status + verbs: + - get + - patch + - update - apiGroups: - fiz.testproject.org resources: diff --git a/testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_busybox.yaml b/testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_busybox.yaml new file mode 100644 index 00000000000..e5ba7642dd1 --- /dev/null +++ b/testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_busybox.yaml @@ -0,0 +1,9 @@ +apiVersion: example.com.testproject.org/v1alpha1 +kind: Busybox +metadata: + name: busybox-sample +spec: + # TODO(user): edit the following value to ensure the number + # of Pods/Instances your Operand must have on cluster + size: 1 + diff --git a/testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_memcached.yaml b/testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_memcached.yaml new file mode 100644 index 00000000000..153d1db36c8 --- /dev/null +++ b/testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_memcached.yaml @@ -0,0 +1,10 @@ +apiVersion: example.com.testproject.org/v1alpha1 +kind: Memcached +metadata: + name: memcached-sample +spec: + # TODO(user): edit the following value to ensure the number + # of Pods/Instances your Operand must have on cluster + size: 1 +# TODO(user): edit the following value to ensure the container has the right port to be initialized + containerPort: 11211 diff --git a/testdata/project-v4-multigroup/config/samples/kustomization.yaml b/testdata/project-v4-multigroup/config/samples/kustomization.yaml index a74ed904341..7018e7905d0 100644 --- a/testdata/project-v4-multigroup/config/samples/kustomization.yaml +++ b/testdata/project-v4-multigroup/config/samples/kustomization.yaml @@ -10,4 +10,6 @@ resources: - foo_v1_bar.yaml - fiz_v1_bar.yaml - _v1_lakers.yaml +- example.com_v1alpha1_memcached.yaml +- example.com_v1alpha1_busybox.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/testdata/project-v4-multigroup/config/webhook/manifests.yaml b/testdata/project-v4-multigroup/config/webhook/manifests.yaml index 62374002227..8537370eb6e 100644 --- a/testdata/project-v4-multigroup/config/webhook/manifests.yaml +++ b/testdata/project-v4-multigroup/config/webhook/manifests.yaml @@ -90,6 +90,26 @@ webhooks: resources: - captains sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-example-com-testproject-org-v1alpha1-memcached + failurePolicy: Fail + name: vmemcached.kb.io + rules: + - apiGroups: + - example.com.testproject.org + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/testdata/project-v4-multigroup/grafana/controller-resources-metrics.json b/testdata/project-v4-multigroup/grafana/controller-resources-metrics.json new file mode 100644 index 00000000000..629e0d3c9b1 --- /dev/null +++ b/testdata/project-v4-multigroup/grafana/controller-resources-metrics.json @@ -0,0 +1,306 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "interval": "1m", + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.4.3", + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "rate(process_cpu_seconds_total{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}[5m]) * 100", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "Pod: {{pod}} | Container: {{container}}", + "refId": "A", + "step": 10 + } + ], + "title": "Controller CPU Usage", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "interval": "1m", + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.4.3", + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "process_resident_memory_bytes{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "Pod: {{pod}} | Container: {{container}}", + "refId": "A", + "step": 10 + } + ], + "title": "Controller Memory Usage", + "type": "timeseries" + } + ], + "refresh": "", + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "datasource": "${DS_PROMETHEUS}", + "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "job", + "options": [], + "query": { + "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "observability", + "value": "observability" + }, + "datasource": "${DS_PROMETHEUS}", + "definition": "label_values(controller_runtime_reconcile_total, namespace)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(controller_runtime_reconcile_total, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": "${DS_PROMETHEUS}", + "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)", + "hide": 2, + "includeAll": true, + "label": "pod", + "multi": true, + "name": "pod", + "options": [], + "query": { + "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Controller-Resources-Metrics", + "weekStart": "" +} diff --git a/testdata/project-v4-multigroup/grafana/controller-runtime-metrics.json b/testdata/project-v4-multigroup/grafana/controller-runtime-metrics.json new file mode 100644 index 00000000000..70023a42d82 --- /dev/null +++ b/testdata/project-v4-multigroup/grafana/controller-runtime-metrics.json @@ -0,0 +1,710 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 9, + "panels": [], + "title": "Reconciliation Metrics", + "type": "row" + }, + { + "datasource": "${DS_PROMETHEUS}", + "description": "Total number of reconciliations per controller", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "cpm" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(controller_runtime_reconcile_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)", + "interval": "", + "legendFormat": "{{instance}} {{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Total Reconciliation Count Per Controller", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "description": "Total number of reconciliation errors per controller", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "cpm" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(controller_runtime_reconcile_errors_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)", + "interval": "", + "legendFormat": "{{instance}} {{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Reconciliation Error Count Per Controller", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 11, + "panels": [], + "title": "Work Queue Metrics", + "type": "row" + }, + { + "datasource": "${DS_PROMETHEUS}", + "description": "How long in seconds an item stays in workqueue before being requested", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 13, + "options": { + "legend": { + "calcs": [ + "max", + "mean" + ], + "displayMode": "list", + "placement": "right" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "histogram_quantile(0.50, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))", + "interval": "", + "legendFormat": "P50 {{name}} {{instance}} ", + "refId": "A" + }, + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))", + "hide": false, + "interval": "", + "legendFormat": "P90 {{name}} {{instance}} ", + "refId": "B" + }, + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "histogram_quantile(0.99, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))", + "hide": false, + "interval": "", + "legendFormat": "P99 {{name}} {{instance}} ", + "refId": "C" + } + ], + "title": "Seconds For Items Stay In Queue (before being requested) (P50, P90, P99)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.4.3", + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "sum(rate(workqueue_adds_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)", + "interval": "", + "legendFormat": "{{name}} {{instance}}", + "refId": "A" + } + ], + "title": "Work Queue Add Rate", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "description": "How long in seconds processing an item from workqueue takes.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 19, + "options": { + "legend": { + "calcs": [ + "max", + "mean" + ], + "displayMode": "table", + "placement": "right" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "histogram_quantile(0.50, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))", + "interval": "", + "legendFormat": "P50 {{name}} {{instance}} ", + "refId": "A" + }, + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))", + "hide": false, + "interval": "", + "legendFormat": "P90 {{name}} {{instance}} ", + "refId": "B" + }, + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "histogram_quantile(0.99, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))", + "hide": false, + "interval": "", + "legendFormat": "P99 {{name}} {{instance}} ", + "refId": "C" + } + ], + "title": "Seconds Processing Items From WorkQueue (P50, P90, P99)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "description": "Total number of retries handled by workqueue", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "exemplar": true, + "expr": "sum(rate(workqueue_retries_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)", + "interval": "", + "legendFormat": "{{name}} {{instance}} ", + "refId": "A" + } + ], + "title": "Work Queue Retries Rate", + "type": "timeseries" + } + ], + "refresh": "", + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "datasource": "${DS_PROMETHEUS}", + "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "job", + "options": [], + "query": { + "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "datasource": "${DS_PROMETHEUS}", + "definition": "label_values(controller_runtime_reconcile_total, namespace)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(controller_runtime_reconcile_total, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": "${DS_PROMETHEUS}", + "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)", + "hide": 2, + "includeAll": true, + "label": "pod", + "multi": true, + "name": "pod", + "options": [], + "query": { + "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Controller-Runtime-Metrics", + "weekStart": "" +} diff --git a/testdata/project-v4-multigroup/grafana/custom-metrics/config.yaml b/testdata/project-v4-multigroup/grafana/custom-metrics/config.yaml new file mode 100644 index 00000000000..3ee1bebdf24 --- /dev/null +++ b/testdata/project-v4-multigroup/grafana/custom-metrics/config.yaml @@ -0,0 +1,15 @@ +--- +customMetrics: +# - metric: # Raw custom metric (required) +# type: # Metric type: counter/gauge/histogram (required) +# expr: # Prom_ql for the metric (optional) +# unit: # Unit of measurement, examples: s,none,bytes,percent,etc. (optional) +# +# +# Example: +# --- +# customMetrics: +# - metric: foo_bar +# unit: none +# type: histogram +# expr: histogram_quantile(0.90, sum by(instance, le) (rate(foo_bar{job=\"$job\", namespace=\"$namespace\"}[5m]))) diff --git a/testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller.go b/testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller.go new file mode 100644 index 00000000000..a9d30834682 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller.go @@ -0,0 +1,434 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package examplecom + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" +) + +const busyboxFinalizer = "example.com.testproject.org/finalizer" + +// Definitions to manage status conditions +const ( + // typeAvailableBusybox represents the status of the Deployment reconciliation + typeAvailableBusybox = "Available" + // typeDegradedBusybox represents the status used when the custom resource is deleted and the finalizer operations are must to occur. + typeDegradedBusybox = "Degraded" +) + +// BusyboxReconciler reconciles a Busybox object +type BusyboxReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder +} + +// The following markers are used to generate the rules permissions (RBAC) on config/rbac using controller-gen +// when the command is executed. +// To know more about markers see: https://book.kubebuilder.io/reference/markers.html + +//+kubebuilder:rbac:groups=example.com.testproject.org,resources=busyboxes,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=example.com.testproject.org,resources=busyboxes/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=example.com.testproject.org,resources=busyboxes/finalizers,verbs=update +//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. + +// It is essential for the controller's reconciliation loop to be idempotent. By following the Operator +// pattern you will create Controllers which provide a reconcile function +// responsible for synchronizing resources until the desired state is reached on the cluster. +// Breaking this recommendation goes against the design principles of controller-runtime. +// and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention. +// For further info: +// - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ +// - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/ +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile +func (r *BusyboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + // Fetch the Busybox instance + // The purpose is check if the Custom Resource for the Kind Busybox + // is applied on the cluster if not we return nil to stop the reconciliation + busybox := &examplecomv1alpha1.Busybox{} + err := r.Get(ctx, req.NamespacedName, busybox) + if err != nil { + if apierrors.IsNotFound(err) { + // If the custom resource is not found then, it usually means that it was deleted or not created + // In this way, we will stop the reconciliation + log.Info("busybox resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get busybox") + return ctrl.Result{}, err + } + + // Let's just set the status as Unknown when no status are available + if busybox.Status.Conditions == nil || len(busybox.Status.Conditions) == 0 { + meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"}) + if err = r.Status().Update(ctx, busybox); err != nil { + log.Error(err, "Failed to update Busybox status") + return ctrl.Result{}, err + } + + // Let's re-fetch the busybox Custom Resource after update the status + // so that we have the latest state of the resource on the cluster and we will avoid + // raise the issue "the object has been modified, please apply + // your changes to the latest version and try again" which would re-trigger the reconciliation + // if we try to update it again in the following operations + if err := r.Get(ctx, req.NamespacedName, busybox); err != nil { + log.Error(err, "Failed to re-fetch busybox") + return ctrl.Result{}, err + } + } + + // Let's add a finalizer. Then, we can define some operations which should + // occurs before the custom resource to be deleted. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers + if !controllerutil.ContainsFinalizer(busybox, busyboxFinalizer) { + log.Info("Adding Finalizer for Busybox") + if ok := controllerutil.AddFinalizer(busybox, busyboxFinalizer); !ok { + log.Error(err, "Failed to add finalizer into the custom resource") + return ctrl.Result{Requeue: true}, nil + } + + if err = r.Update(ctx, busybox); err != nil { + log.Error(err, "Failed to update custom resource to add finalizer") + return ctrl.Result{}, err + } + } + + // Check if the Busybox instance is marked to be deleted, which is + // indicated by the deletion timestamp being set. + isBusyboxMarkedToBeDeleted := busybox.GetDeletionTimestamp() != nil + if isBusyboxMarkedToBeDeleted { + if controllerutil.ContainsFinalizer(busybox, busyboxFinalizer) { + log.Info("Performing Finalizer Operations for Busybox before delete CR") + + // Let's add here an status "Downgrade" to define that this resource begin its process to be terminated. + meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeDegradedBusybox, + Status: metav1.ConditionUnknown, Reason: "Finalizing", + Message: fmt.Sprintf("Performing finalizer operations for the custom resource: %s ", busybox.Name)}) + + if err := r.Status().Update(ctx, busybox); err != nil { + log.Error(err, "Failed to update Busybox status") + return ctrl.Result{}, err + } + + // Perform all operations required before remove the finalizer and allow + // the Kubernetes API to remove the custom resource. + r.doFinalizerOperationsForBusybox(busybox) + + // TODO(user): If you add operations to the doFinalizerOperationsForBusybox method + // then you need to ensure that all worked fine before deleting and updating the Downgrade status + // otherwise, you should requeue here. + + // Re-fetch the busybox Custom Resource before update the status + // so that we have the latest state of the resource on the cluster and we will avoid + // raise the issue "the object has been modified, please apply + // your changes to the latest version and try again" which would re-trigger the reconciliation + if err := r.Get(ctx, req.NamespacedName, busybox); err != nil { + log.Error(err, "Failed to re-fetch busybox") + return ctrl.Result{}, err + } + + meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeDegradedBusybox, + Status: metav1.ConditionTrue, Reason: "Finalizing", + Message: fmt.Sprintf("Finalizer operations for custom resource %s name were successfully accomplished", busybox.Name)}) + + if err := r.Status().Update(ctx, busybox); err != nil { + log.Error(err, "Failed to update Busybox status") + return ctrl.Result{}, err + } + + log.Info("Removing Finalizer for Busybox after successfully perform the operations") + if ok := controllerutil.RemoveFinalizer(busybox, busyboxFinalizer); !ok { + log.Error(err, "Failed to remove finalizer for Busybox") + return ctrl.Result{Requeue: true}, nil + } + + if err := r.Update(ctx, busybox); err != nil { + log.Error(err, "Failed to remove finalizer for Busybox") + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + + // Check if the deployment already exists, if not create a new one + found := &appsv1.Deployment{} + err = r.Get(ctx, types.NamespacedName{Name: busybox.Name, Namespace: busybox.Namespace}, found) + if err != nil && apierrors.IsNotFound(err) { + // Define a new deployment + dep, err := r.deploymentForBusybox(busybox) + if err != nil { + log.Error(err, "Failed to define new Deployment resource for Busybox") + + // The following implementation will update the status + meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox, + Status: metav1.ConditionFalse, Reason: "Reconciling", + Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", busybox.Name, err)}) + + if err := r.Status().Update(ctx, busybox); err != nil { + log.Error(err, "Failed to update Busybox status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, err + } + + log.Info("Creating a new Deployment", + "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + if err = r.Create(ctx, dep); err != nil { + log.Error(err, "Failed to create new Deployment", + "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + return ctrl.Result{}, err + } + + // Deployment created successfully + // We will requeue the reconciliation so that we can ensure the state + // and move forward for the next operations + return ctrl.Result{RequeueAfter: time.Minute}, nil + } else if err != nil { + log.Error(err, "Failed to get Deployment") + // Let's return the error for the reconciliation be re-trigged again + return ctrl.Result{}, err + } + + // The CRD API is defining that the Busybox type, have a BusyboxSpec.Size field + // to set the quantity of Deployment instances is the desired state on the cluster. + // Therefore, the following code will ensure the Deployment size is the same as defined + // via the Size spec of the Custom Resource which we are reconciling. + size := busybox.Spec.Size + if *found.Spec.Replicas != size { + found.Spec.Replicas = &size + if err = r.Update(ctx, found); err != nil { + log.Error(err, "Failed to update Deployment", + "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) + + // Re-fetch the busybox Custom Resource before update the status + // so that we have the latest state of the resource on the cluster and we will avoid + // raise the issue "the object has been modified, please apply + // your changes to the latest version and try again" which would re-trigger the reconciliation + if err := r.Get(ctx, req.NamespacedName, busybox); err != nil { + log.Error(err, "Failed to re-fetch busybox") + return ctrl.Result{}, err + } + + // The following implementation will update the status + meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox, + Status: metav1.ConditionFalse, Reason: "Resizing", + Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", busybox.Name, err)}) + + if err := r.Status().Update(ctx, busybox); err != nil { + log.Error(err, "Failed to update Busybox status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, err + } + + // Now, that we update the size we want to requeue the reconciliation + // so that we can ensure that we have the latest state of the resource before + // update. Also, it will help ensure the desired state on the cluster + return ctrl.Result{Requeue: true}, nil + } + + // The following implementation will update the status + meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox, + Status: metav1.ConditionTrue, Reason: "Reconciling", + Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", busybox.Name, size)}) + + if err := r.Status().Update(ctx, busybox); err != nil { + log.Error(err, "Failed to update Busybox status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// finalizeBusybox will perform the required operations before delete the CR. +func (r *BusyboxReconciler) doFinalizerOperationsForBusybox(cr *examplecomv1alpha1.Busybox) { + // TODO(user): Add the cleanup steps that the operator + // needs to do before the CR can be deleted. Examples + // of finalizers include performing backups and deleting + // resources that are not owned by this CR, like a PVC. + + // Note: It is not recommended to use finalizers with the purpose of delete resources which are + // created and managed in the reconciliation. These ones, such as the Deployment created on this reconcile, + // are defined as depended of the custom resource. See that we use the method ctrl.SetControllerReference. + // to set the ownerRef which means that the Deployment will be deleted by the Kubernetes API. + // More info: https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/ + + // The following implementation will raise an event + r.Recorder.Event(cr, "Warning", "Deleting", + fmt.Sprintf("Custom Resource %s is being deleted from the namespace %s", + cr.Name, + cr.Namespace)) +} + +// deploymentForBusybox returns a Busybox Deployment object +func (r *BusyboxReconciler) deploymentForBusybox( + busybox *examplecomv1alpha1.Busybox) (*appsv1.Deployment, error) { + ls := labelsForBusybox(busybox.Name) + replicas := busybox.Spec.Size + + // Get the Operand image + image, err := imageForBusybox() + if err != nil { + return nil, err + } + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: busybox.Name, + Namespace: busybox.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: ls, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ls, + }, + Spec: corev1.PodSpec{ + // TODO(user): Uncomment the following code to configure the nodeAffinity expression + // according to the platforms which are supported by your solution. It is considered + // best practice to support multiple architectures. build your manager image using the + // makefile target docker-buildx. Also, you can use docker manifest inspect + // to check what are the platforms supported. + // More info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity + //Affinity: &corev1.Affinity{ + // NodeAffinity: &corev1.NodeAffinity{ + // RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + // NodeSelectorTerms: []corev1.NodeSelectorTerm{ + // { + // MatchExpressions: []corev1.NodeSelectorRequirement{ + // { + // Key: "kubernetes.io/arch", + // Operator: "In", + // Values: []string{"amd64", "arm64", "ppc64le", "s390x"}, + // }, + // { + // Key: "kubernetes.io/os", + // Operator: "In", + // Values: []string{"linux"}, + // }, + // }, + // }, + // }, + // }, + // }, + //}, + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + // IMPORTANT: seccomProfile was introduced with Kubernetes 1.19 + // If you are looking for to produce solutions to be supported + // on lower versions you must remove this option. + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + Containers: []corev1.Container{{ + Image: image, + Name: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + // Ensure restrictive context for the container + // More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + }, + }}, + }, + }, + }, + } + + // Set the ownerRef for the Deployment + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ + if err := ctrl.SetControllerReference(busybox, dep, r.Scheme); err != nil { + return nil, err + } + return dep, nil +} + +// labelsForBusybox returns the labels for selecting the resources +// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ +func labelsForBusybox(name string) map[string]string { + var imageTag string + image, err := imageForBusybox() + if err == nil { + imageTag = strings.Split(image, ":")[1] + } + return map[string]string{"app.kubernetes.io/name": "Busybox", + "app.kubernetes.io/instance": name, + "app.kubernetes.io/version": imageTag, + "app.kubernetes.io/part-of": "project-v4-multigroup", + "app.kubernetes.io/created-by": "controller-manager", + } +} + +// imageForBusybox gets the Operand image which is managed by this controller +// from the BUSYBOX_IMAGE environment variable defined in the config/manager/manager.yaml +func imageForBusybox() (string, error) { + var imageEnvVar = "BUSYBOX_IMAGE" + image, found := os.LookupEnv(imageEnvVar) + if !found { + return "", fmt.Errorf("Unable to find %s environment variable with the image", imageEnvVar) + } + return image, nil +} + +// SetupWithManager sets up the controller with the Manager. +// Note that the Deployment will be also watched in order to ensure its +// desirable state on the cluster +func (r *BusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&examplecomv1alpha1.Busybox{}). + Owns(&appsv1.Deployment{}). + Complete(r) +} diff --git a/testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller_test.go b/testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller_test.go new file mode 100644 index 00000000000..0a3496134dd --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller_test.go @@ -0,0 +1,142 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package examplecom + +import ( + "context" + "fmt" + "os" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" +) + +var _ = Describe("Busybox controller", func() { + Context("Busybox controller test", func() { + + const BusyboxName = "test-busybox" + + ctx := context.Background() + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: BusyboxName, + Namespace: BusyboxName, + }, + } + + typeNamespaceName := types.NamespacedName{Name: BusyboxName, Namespace: BusyboxName} + busybox := &examplecomv1alpha1.Busybox{} + + BeforeEach(func() { + By("Creating the Namespace to perform the tests") + err := k8sClient.Create(ctx, namespace) + Expect(err).To(Not(HaveOccurred())) + + By("Setting the Image ENV VAR which stores the Operand image") + err = os.Setenv("BUSYBOX_IMAGE", "example.com/image:test") + Expect(err).To(Not(HaveOccurred())) + + By("creating the custom resource for the Kind Busybox") + err = k8sClient.Get(ctx, typeNamespaceName, busybox) + if err != nil && errors.IsNotFound(err) { + // Let's mock our custom resource at the same way that we would + // apply on the cluster the manifest under config/samples + busybox := &examplecomv1alpha1.Busybox{ + ObjectMeta: metav1.ObjectMeta{ + Name: BusyboxName, + Namespace: namespace.Name, + }, + Spec: examplecomv1alpha1.BusyboxSpec{ + Size: 1, + }, + } + + err = k8sClient.Create(ctx, busybox) + Expect(err).To(Not(HaveOccurred())) + } + }) + + AfterEach(func() { + By("removing the custom resource for the Kind Busybox") + found := &examplecomv1alpha1.Busybox{} + err := k8sClient.Get(ctx, typeNamespaceName, found) + Expect(err).To(Not(HaveOccurred())) + + Eventually(func() error { + return k8sClient.Delete(context.TODO(), found) + }, 2*time.Minute, time.Second).Should(Succeed()) + + // TODO(user): Attention if you improve this code by adding other context test you MUST + // be aware of the current delete namespace limitations. + // More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations + By("Deleting the Namespace to perform the tests") + _ = k8sClient.Delete(ctx, namespace) + + By("Removing the Image ENV VAR which stores the Operand image") + _ = os.Unsetenv("BUSYBOX_IMAGE") + }) + + It("should successfully reconcile a custom resource for Busybox", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &examplecomv1alpha1.Busybox{} + return k8sClient.Get(ctx, typeNamespaceName, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + busyboxReconciler := &BusyboxReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := busyboxReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespaceName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking if Deployment was successfully created in the reconciliation") + Eventually(func() error { + found := &appsv1.Deployment{} + return k8sClient.Get(ctx, typeNamespaceName, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the latest Status Condition added to the Busybox instance") + Eventually(func() error { + if busybox.Status.Conditions != nil && len(busybox.Status.Conditions) != 0 { + latestStatusCondition := busybox.Status.Conditions[len(busybox.Status.Conditions)-1] + expectedLatestStatusCondition := metav1.Condition{Type: typeAvailableBusybox, + Status: metav1.ConditionTrue, Reason: "Reconciling", + Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", busybox.Name, busybox.Spec.Size)} + if latestStatusCondition != expectedLatestStatusCondition { + return fmt.Errorf("The latest status condition added to the busybox instance is not as expected") + } + } + return nil + }, time.Minute, time.Second).Should(Succeed()) + }) + }) +}) diff --git a/testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller.go b/testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller.go new file mode 100644 index 00000000000..a21240db732 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller.go @@ -0,0 +1,440 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package examplecom + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" +) + +const memcachedFinalizer = "example.com.testproject.org/finalizer" + +// Definitions to manage status conditions +const ( + // typeAvailableMemcached represents the status of the Deployment reconciliation + typeAvailableMemcached = "Available" + // typeDegradedMemcached represents the status used when the custom resource is deleted and the finalizer operations are must to occur. + typeDegradedMemcached = "Degraded" +) + +// MemcachedReconciler reconciles a Memcached object +type MemcachedReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder +} + +// The following markers are used to generate the rules permissions (RBAC) on config/rbac using controller-gen +// when the command is executed. +// To know more about markers see: https://book.kubebuilder.io/reference/markers.html + +//+kubebuilder:rbac:groups=example.com.testproject.org,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=example.com.testproject.org,resources=memcacheds/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=example.com.testproject.org,resources=memcacheds/finalizers,verbs=update +//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. + +// It is essential for the controller's reconciliation loop to be idempotent. By following the Operator +// pattern you will create Controllers which provide a reconcile function +// responsible for synchronizing resources until the desired state is reached on the cluster. +// Breaking this recommendation goes against the design principles of controller-runtime. +// and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention. +// For further info: +// - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ +// - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/ +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile +func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + // Fetch the Memcached instance + // The purpose is check if the Custom Resource for the Kind Memcached + // is applied on the cluster if not we return nil to stop the reconciliation + memcached := &examplecomv1alpha1.Memcached{} + err := r.Get(ctx, req.NamespacedName, memcached) + if err != nil { + if apierrors.IsNotFound(err) { + // If the custom resource is not found then, it usually means that it was deleted or not created + // In this way, we will stop the reconciliation + log.Info("memcached resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get memcached") + return ctrl.Result{}, err + } + + // Let's just set the status as Unknown when no status are available + if memcached.Status.Conditions == nil || len(memcached.Status.Conditions) == 0 { + meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"}) + if err = r.Status().Update(ctx, memcached); err != nil { + log.Error(err, "Failed to update Memcached status") + return ctrl.Result{}, err + } + + // Let's re-fetch the memcached Custom Resource after update the status + // so that we have the latest state of the resource on the cluster and we will avoid + // raise the issue "the object has been modified, please apply + // your changes to the latest version and try again" which would re-trigger the reconciliation + // if we try to update it again in the following operations + if err := r.Get(ctx, req.NamespacedName, memcached); err != nil { + log.Error(err, "Failed to re-fetch memcached") + return ctrl.Result{}, err + } + } + + // Let's add a finalizer. Then, we can define some operations which should + // occurs before the custom resource to be deleted. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers + if !controllerutil.ContainsFinalizer(memcached, memcachedFinalizer) { + log.Info("Adding Finalizer for Memcached") + if ok := controllerutil.AddFinalizer(memcached, memcachedFinalizer); !ok { + log.Error(err, "Failed to add finalizer into the custom resource") + return ctrl.Result{Requeue: true}, nil + } + + if err = r.Update(ctx, memcached); err != nil { + log.Error(err, "Failed to update custom resource to add finalizer") + return ctrl.Result{}, err + } + } + + // Check if the Memcached instance is marked to be deleted, which is + // indicated by the deletion timestamp being set. + isMemcachedMarkedToBeDeleted := memcached.GetDeletionTimestamp() != nil + if isMemcachedMarkedToBeDeleted { + if controllerutil.ContainsFinalizer(memcached, memcachedFinalizer) { + log.Info("Performing Finalizer Operations for Memcached before delete CR") + + // Let's add here an status "Downgrade" to define that this resource begin its process to be terminated. + meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeDegradedMemcached, + Status: metav1.ConditionUnknown, Reason: "Finalizing", + Message: fmt.Sprintf("Performing finalizer operations for the custom resource: %s ", memcached.Name)}) + + if err := r.Status().Update(ctx, memcached); err != nil { + log.Error(err, "Failed to update Memcached status") + return ctrl.Result{}, err + } + + // Perform all operations required before remove the finalizer and allow + // the Kubernetes API to remove the custom resource. + r.doFinalizerOperationsForMemcached(memcached) + + // TODO(user): If you add operations to the doFinalizerOperationsForMemcached method + // then you need to ensure that all worked fine before deleting and updating the Downgrade status + // otherwise, you should requeue here. + + // Re-fetch the memcached Custom Resource before update the status + // so that we have the latest state of the resource on the cluster and we will avoid + // raise the issue "the object has been modified, please apply + // your changes to the latest version and try again" which would re-trigger the reconciliation + if err := r.Get(ctx, req.NamespacedName, memcached); err != nil { + log.Error(err, "Failed to re-fetch memcached") + return ctrl.Result{}, err + } + + meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeDegradedMemcached, + Status: metav1.ConditionTrue, Reason: "Finalizing", + Message: fmt.Sprintf("Finalizer operations for custom resource %s name were successfully accomplished", memcached.Name)}) + + if err := r.Status().Update(ctx, memcached); err != nil { + log.Error(err, "Failed to update Memcached status") + return ctrl.Result{}, err + } + + log.Info("Removing Finalizer for Memcached after successfully perform the operations") + if ok := controllerutil.RemoveFinalizer(memcached, memcachedFinalizer); !ok { + log.Error(err, "Failed to remove finalizer for Memcached") + return ctrl.Result{Requeue: true}, nil + } + + if err := r.Update(ctx, memcached); err != nil { + log.Error(err, "Failed to remove finalizer for Memcached") + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + + // Check if the deployment already exists, if not create a new one + found := &appsv1.Deployment{} + err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found) + if err != nil && apierrors.IsNotFound(err) { + // Define a new deployment + dep, err := r.deploymentForMemcached(memcached) + if err != nil { + log.Error(err, "Failed to define new Deployment resource for Memcached") + + // The following implementation will update the status + meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, + Status: metav1.ConditionFalse, Reason: "Reconciling", + Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", memcached.Name, err)}) + + if err := r.Status().Update(ctx, memcached); err != nil { + log.Error(err, "Failed to update Memcached status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, err + } + + log.Info("Creating a new Deployment", + "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + if err = r.Create(ctx, dep); err != nil { + log.Error(err, "Failed to create new Deployment", + "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + return ctrl.Result{}, err + } + + // Deployment created successfully + // We will requeue the reconciliation so that we can ensure the state + // and move forward for the next operations + return ctrl.Result{RequeueAfter: time.Minute}, nil + } else if err != nil { + log.Error(err, "Failed to get Deployment") + // Let's return the error for the reconciliation be re-trigged again + return ctrl.Result{}, err + } + + // The CRD API is defining that the Memcached type, have a MemcachedSpec.Size field + // to set the quantity of Deployment instances is the desired state on the cluster. + // Therefore, the following code will ensure the Deployment size is the same as defined + // via the Size spec of the Custom Resource which we are reconciling. + size := memcached.Spec.Size + if *found.Spec.Replicas != size { + found.Spec.Replicas = &size + if err = r.Update(ctx, found); err != nil { + log.Error(err, "Failed to update Deployment", + "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) + + // Re-fetch the memcached Custom Resource before update the status + // so that we have the latest state of the resource on the cluster and we will avoid + // raise the issue "the object has been modified, please apply + // your changes to the latest version and try again" which would re-trigger the reconciliation + if err := r.Get(ctx, req.NamespacedName, memcached); err != nil { + log.Error(err, "Failed to re-fetch memcached") + return ctrl.Result{}, err + } + + // The following implementation will update the status + meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, + Status: metav1.ConditionFalse, Reason: "Resizing", + Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", memcached.Name, err)}) + + if err := r.Status().Update(ctx, memcached); err != nil { + log.Error(err, "Failed to update Memcached status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, err + } + + // Now, that we update the size we want to requeue the reconciliation + // so that we can ensure that we have the latest state of the resource before + // update. Also, it will help ensure the desired state on the cluster + return ctrl.Result{Requeue: true}, nil + } + + // The following implementation will update the status + meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, + Status: metav1.ConditionTrue, Reason: "Reconciling", + Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", memcached.Name, size)}) + + if err := r.Status().Update(ctx, memcached); err != nil { + log.Error(err, "Failed to update Memcached status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// finalizeMemcached will perform the required operations before delete the CR. +func (r *MemcachedReconciler) doFinalizerOperationsForMemcached(cr *examplecomv1alpha1.Memcached) { + // TODO(user): Add the cleanup steps that the operator + // needs to do before the CR can be deleted. Examples + // of finalizers include performing backups and deleting + // resources that are not owned by this CR, like a PVC. + + // Note: It is not recommended to use finalizers with the purpose of delete resources which are + // created and managed in the reconciliation. These ones, such as the Deployment created on this reconcile, + // are defined as depended of the custom resource. See that we use the method ctrl.SetControllerReference. + // to set the ownerRef which means that the Deployment will be deleted by the Kubernetes API. + // More info: https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/ + + // The following implementation will raise an event + r.Recorder.Event(cr, "Warning", "Deleting", + fmt.Sprintf("Custom Resource %s is being deleted from the namespace %s", + cr.Name, + cr.Namespace)) +} + +// deploymentForMemcached returns a Memcached Deployment object +func (r *MemcachedReconciler) deploymentForMemcached( + memcached *examplecomv1alpha1.Memcached) (*appsv1.Deployment, error) { + ls := labelsForMemcached(memcached.Name) + replicas := memcached.Spec.Size + + // Get the Operand image + image, err := imageForMemcached() + if err != nil { + return nil, err + } + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: memcached.Name, + Namespace: memcached.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: ls, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ls, + }, + Spec: corev1.PodSpec{ + // TODO(user): Uncomment the following code to configure the nodeAffinity expression + // according to the platforms which are supported by your solution. It is considered + // best practice to support multiple architectures. build your manager image using the + // makefile target docker-buildx. Also, you can use docker manifest inspect + // to check what are the platforms supported. + // More info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity + //Affinity: &corev1.Affinity{ + // NodeAffinity: &corev1.NodeAffinity{ + // RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + // NodeSelectorTerms: []corev1.NodeSelectorTerm{ + // { + // MatchExpressions: []corev1.NodeSelectorRequirement{ + // { + // Key: "kubernetes.io/arch", + // Operator: "In", + // Values: []string{"amd64", "arm64", "ppc64le", "s390x"}, + // }, + // { + // Key: "kubernetes.io/os", + // Operator: "In", + // Values: []string{"linux"}, + // }, + // }, + // }, + // }, + // }, + // }, + //}, + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + // IMPORTANT: seccomProfile was introduced with Kubernetes 1.19 + // If you are looking for to produce solutions to be supported + // on lower versions you must remove this option. + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + Containers: []corev1.Container{{ + Image: image, + Name: "memcached", + ImagePullPolicy: corev1.PullIfNotPresent, + // Ensure restrictive context for the container + // More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + RunAsUser: &[]int64{1001}[0], + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + }, + Ports: []corev1.ContainerPort{{ + ContainerPort: memcached.Spec.ContainerPort, + Name: "memcached", + }}, + Command: []string{"memcached", "-m=64", "-o", "modern", "-v"}, + }}, + }, + }, + }, + } + + // Set the ownerRef for the Deployment + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ + if err := ctrl.SetControllerReference(memcached, dep, r.Scheme); err != nil { + return nil, err + } + return dep, nil +} + +// labelsForMemcached returns the labels for selecting the resources +// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ +func labelsForMemcached(name string) map[string]string { + var imageTag string + image, err := imageForMemcached() + if err == nil { + imageTag = strings.Split(image, ":")[1] + } + return map[string]string{"app.kubernetes.io/name": "Memcached", + "app.kubernetes.io/instance": name, + "app.kubernetes.io/version": imageTag, + "app.kubernetes.io/part-of": "project-v4-multigroup", + "app.kubernetes.io/created-by": "controller-manager", + } +} + +// imageForMemcached gets the Operand image which is managed by this controller +// from the MEMCACHED_IMAGE environment variable defined in the config/manager/manager.yaml +func imageForMemcached() (string, error) { + var imageEnvVar = "MEMCACHED_IMAGE" + image, found := os.LookupEnv(imageEnvVar) + if !found { + return "", fmt.Errorf("Unable to find %s environment variable with the image", imageEnvVar) + } + return image, nil +} + +// SetupWithManager sets up the controller with the Manager. +// Note that the Deployment will be also watched in order to ensure its +// desirable state on the cluster +func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&examplecomv1alpha1.Memcached{}). + Owns(&appsv1.Deployment{}). + Complete(r) +} diff --git a/testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller_test.go b/testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller_test.go new file mode 100644 index 00000000000..f96146c9b74 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller_test.go @@ -0,0 +1,143 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package examplecom + +import ( + "context" + "fmt" + "os" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" +) + +var _ = Describe("Memcached controller", func() { + Context("Memcached controller test", func() { + + const MemcachedName = "test-memcached" + + ctx := context.Background() + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: MemcachedName, + Namespace: MemcachedName, + }, + } + + typeNamespaceName := types.NamespacedName{Name: MemcachedName, Namespace: MemcachedName} + memcached := &examplecomv1alpha1.Memcached{} + + BeforeEach(func() { + By("Creating the Namespace to perform the tests") + err := k8sClient.Create(ctx, namespace) + Expect(err).To(Not(HaveOccurred())) + + By("Setting the Image ENV VAR which stores the Operand image") + err = os.Setenv("MEMCACHED_IMAGE", "example.com/image:test") + Expect(err).To(Not(HaveOccurred())) + + By("creating the custom resource for the Kind Memcached") + err = k8sClient.Get(ctx, typeNamespaceName, memcached) + if err != nil && errors.IsNotFound(err) { + // Let's mock our custom resource at the same way that we would + // apply on the cluster the manifest under config/samples + memcached := &examplecomv1alpha1.Memcached{ + ObjectMeta: metav1.ObjectMeta{ + Name: MemcachedName, + Namespace: namespace.Name, + }, + Spec: examplecomv1alpha1.MemcachedSpec{ + Size: 1, + ContainerPort: 11211, + }, + } + + err = k8sClient.Create(ctx, memcached) + Expect(err).To(Not(HaveOccurred())) + } + }) + + AfterEach(func() { + By("removing the custom resource for the Kind Memcached") + found := &examplecomv1alpha1.Memcached{} + err := k8sClient.Get(ctx, typeNamespaceName, found) + Expect(err).To(Not(HaveOccurred())) + + Eventually(func() error { + return k8sClient.Delete(context.TODO(), found) + }, 2*time.Minute, time.Second).Should(Succeed()) + + // TODO(user): Attention if you improve this code by adding other context test you MUST + // be aware of the current delete namespace limitations. + // More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations + By("Deleting the Namespace to perform the tests") + _ = k8sClient.Delete(ctx, namespace) + + By("Removing the Image ENV VAR which stores the Operand image") + _ = os.Unsetenv("MEMCACHED_IMAGE") + }) + + It("should successfully reconcile a custom resource for Memcached", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &examplecomv1alpha1.Memcached{} + return k8sClient.Get(ctx, typeNamespaceName, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + memcachedReconciler := &MemcachedReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := memcachedReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespaceName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking if Deployment was successfully created in the reconciliation") + Eventually(func() error { + found := &appsv1.Deployment{} + return k8sClient.Get(ctx, typeNamespaceName, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the latest Status Condition added to the Memcached instance") + Eventually(func() error { + if memcached.Status.Conditions != nil && len(memcached.Status.Conditions) != 0 { + latestStatusCondition := memcached.Status.Conditions[len(memcached.Status.Conditions)-1] + expectedLatestStatusCondition := metav1.Condition{Type: typeAvailableMemcached, + Status: metav1.ConditionTrue, Reason: "Reconciling", + Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", memcached.Name, memcached.Spec.Size)} + if latestStatusCondition != expectedLatestStatusCondition { + return fmt.Errorf("The latest status condition added to the memcached instance is not as expected") + } + } + return nil + }, time.Minute, time.Second).Should(Succeed()) + }) + }) +}) diff --git a/testdata/project-v4-multigroup/internal/controller/example.com/suite_test.go b/testdata/project-v4-multigroup/internal/controller/example.com/suite_test.go new file mode 100644 index 00000000000..2d10eaa4f7f --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/example.com/suite_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2023 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package examplecom + +import ( + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", + fmt.Sprintf("1.27.1-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = examplecomv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +})