From e76297d477fe9e7e137d062d5b0ad1300c2ab8ae Mon Sep 17 00:00:00 2001 From: yossig-runai <143929074+yossig-runai@users.noreply.github.com> Date: Sun, 22 Sep 2024 10:27:16 +0300 Subject: [PATCH] task: adding another api `ArgoRolloutConfigKeeperClusterScope` for managing configs in cluster scope. --- PROJECT | 10 +- ...gorolloutconfigkeeperclusterscope_types.go | 83 ++++ api/v1alpha1/groupversion_info.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 101 +++++ cmd/main.go | 11 +- ..._argorolloutconfigkeeperclusterscopes.yaml | 83 ++++ config/crd/kustomization.yaml | 3 + ...tconfigkeeperclusterscope_editor_role.yaml | 31 ++ ...tconfigkeeperclusterscope_viewer_role.yaml | 27 ++ config/rbac/role.yaml | 26 ++ ...per_v1alpha1_argorolloutconfigkeeper.yaml} | 0 ...1_argorolloutconfigkeeperclusterscope.yaml | 18 + config/samples/kustomization.yaml | 1 + go.mod | 1 - internal/common/common.go | 358 ++++++++++++++++++ .../argorolloutconfigkeeper_controller.go | 314 ++------------- ...argorolloutconfigkeeper_controller_test.go | 71 ++-- ...loutconfigkeeperclusterscope_controller.go | 108 ++++++ ...onfigkeeperclusterscope_controller_test.go | 217 +++++++++++ internal/controller/suite_test.go | 7 +- internal/tools.go | 8 + 21 files changed, 1161 insertions(+), 319 deletions(-) create mode 100644 api/v1alpha1/argorolloutconfigkeeperclusterscope_types.go create mode 100644 config/crd/bases/configkeeper.run.ai_argorolloutconfigkeeperclusterscopes.yaml create mode 100644 config/rbac/argorolloutconfigkeeperclusterscope_editor_role.yaml create mode 100644 config/rbac/argorolloutconfigkeeperclusterscope_viewer_role.yaml rename config/samples/{keeper_v1alpha1_argorolloutconfigkeeper.yaml => configkeeper_v1alpha1_argorolloutconfigkeeper.yaml} (100%) create mode 100644 config/samples/configkeeper_v1alpha1_argorolloutconfigkeeperclusterscope.yaml create mode 100644 internal/common/common.go create mode 100644 internal/controller/argorolloutconfigkeeperclusterscope_controller.go create mode 100644 internal/controller/argorolloutconfigkeeperclusterscope_controller_test.go diff --git a/PROJECT b/PROJECT index 005f2dd..66fbe70 100644 --- a/PROJECT +++ b/PROJECT @@ -16,8 +16,16 @@ resources: namespaced: true controller: true domain: run.ai - group: keeper + group: configkeeper kind: ArgoRolloutConfigKeeper path: github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + controller: true + domain: run.ai + group: configkeeper + kind: ArgoRolloutConfigKeeperClusterScope + path: github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/argorolloutconfigkeeperclusterscope_types.go b/api/v1alpha1/argorolloutconfigkeeperclusterscope_types.go new file mode 100644 index 0000000..a88a64f --- /dev/null +++ b/api/v1alpha1/argorolloutconfigkeeperclusterscope_types.go @@ -0,0 +1,83 @@ +/* +Copyright 2024. + +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 ( + "encoding/json" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ArgoRolloutConfigKeeperClusterScopeSpec defines the desired state of ArgoRolloutConfigKeeperClusterScope +type ArgoRolloutConfigKeeperClusterScopeSpec struct { + FinalizerName string `json:"finalizerName"` + AppLabel string `json:"appLabel,omitempty"` + AppVersionLabel string `json:"appVersionLabel,omitempty"` + ConfigLabelSelector map[string]string `json:"configLabelSelector,omitempty"` + IgnoredNamespaces []string `json:"ignoredNamespaces,omitempty"` +} + +func (in *ArgoRolloutConfigKeeperClusterScopeSpec) UnmarshalJSON(b []byte) error { + type alias ArgoRolloutConfigKeeperClusterScopeSpec + tmp := struct { + *alias + }{ + alias: (*alias)(in), + } + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + if tmp.alias.AppVersionLabel == "" { + tmp.alias.AppVersionLabel = "app.kubernetes.io/version" + } + if tmp.alias.AppLabel == "" { + tmp.alias.AppLabel = "app.kubernetes.io/name" + } + return nil +} + +// ArgoRolloutConfigKeeperClusterScopeStatus defines the observed state of ArgoRolloutConfigKeeperClusterScope +type ArgoRolloutConfigKeeperClusterScopeStatus struct { + State string `json:"state"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:scope=Cluster +//+kubebuilder:printcolumn:name="FinalizerName",type="string",JSONPath=".spec.finalizerName",description="The name of managed Finalizer" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath="..metadata.creationTimestamp",description="Aqua Database Age" + +// ArgoRolloutConfigKeeperClusterScope is the Schema for the argorolloutconfigkeeperclusterscopes API +type ArgoRolloutConfigKeeperClusterScope struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ArgoRolloutConfigKeeperClusterScopeSpec `json:"spec,omitempty"` + Status ArgoRolloutConfigKeeperClusterScopeStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ArgoRolloutConfigKeeperClusterScopeList contains a list of ArgoRolloutConfigKeeperClusterScope +type ArgoRolloutConfigKeeperClusterScopeList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ArgoRolloutConfigKeeperClusterScope `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ArgoRolloutConfigKeeperClusterScope{}, &ArgoRolloutConfigKeeperClusterScopeList{}) +} diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index c41cbee..57a6196 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1alpha1 contains API Schema definitions for the keeper v1alpha1 API group +// Package v1alpha1 contains API Schema definitions for the configkeeper v1alpha1 API group // +kubebuilder:object:generate=true // +groupName=configkeeper.run.ai package v1alpha1 diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8e4553a..46edac8 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -51,6 +51,107 @@ func (in *ArgoRolloutConfigKeeper) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArgoRolloutConfigKeeperClusterScope) DeepCopyInto(out *ArgoRolloutConfigKeeperClusterScope) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoRolloutConfigKeeperClusterScope. +func (in *ArgoRolloutConfigKeeperClusterScope) DeepCopy() *ArgoRolloutConfigKeeperClusterScope { + if in == nil { + return nil + } + out := new(ArgoRolloutConfigKeeperClusterScope) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ArgoRolloutConfigKeeperClusterScope) 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 *ArgoRolloutConfigKeeperClusterScopeList) DeepCopyInto(out *ArgoRolloutConfigKeeperClusterScopeList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ArgoRolloutConfigKeeperClusterScope, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoRolloutConfigKeeperClusterScopeList. +func (in *ArgoRolloutConfigKeeperClusterScopeList) DeepCopy() *ArgoRolloutConfigKeeperClusterScopeList { + if in == nil { + return nil + } + out := new(ArgoRolloutConfigKeeperClusterScopeList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ArgoRolloutConfigKeeperClusterScopeList) 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 *ArgoRolloutConfigKeeperClusterScopeSpec) DeepCopyInto(out *ArgoRolloutConfigKeeperClusterScopeSpec) { + *out = *in + if in.ConfigLabelSelector != nil { + in, out := &in.ConfigLabelSelector, &out.ConfigLabelSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.IgnoredNamespaces != nil { + in, out := &in.IgnoredNamespaces, &out.IgnoredNamespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoRolloutConfigKeeperClusterScopeSpec. +func (in *ArgoRolloutConfigKeeperClusterScopeSpec) DeepCopy() *ArgoRolloutConfigKeeperClusterScopeSpec { + if in == nil { + return nil + } + out := new(ArgoRolloutConfigKeeperClusterScopeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArgoRolloutConfigKeeperClusterScopeStatus) DeepCopyInto(out *ArgoRolloutConfigKeeperClusterScopeStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoRolloutConfigKeeperClusterScopeStatus. +func (in *ArgoRolloutConfigKeeperClusterScopeStatus) DeepCopy() *ArgoRolloutConfigKeeperClusterScopeStatus { + if in == nil { + return nil + } + out := new(ArgoRolloutConfigKeeperClusterScopeStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ArgoRolloutConfigKeeperList) DeepCopyInto(out *ArgoRolloutConfigKeeperList) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 246b0f3..ee7b401 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -32,7 +32,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" - keeperv1alpha1 "github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1" + configkeeperv1alpha1 "github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1" "github.com/run-ai/argo-rollout-config-keeper/internal/controller" //+kubebuilder:scaffold:imports ) @@ -45,7 +45,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(keeperv1alpha1.AddToScheme(scheme)) + utilruntime.Must(configkeeperv1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -126,6 +126,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "ArgoRolloutConfigKeeper") os.Exit(1) } + if err = (&controller.ArgoRolloutConfigKeeperClusterScopeReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ArgoRolloutConfigKeeperClusterScope") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/configkeeper.run.ai_argorolloutconfigkeeperclusterscopes.yaml b/config/crd/bases/configkeeper.run.ai_argorolloutconfigkeeperclusterscopes.yaml new file mode 100644 index 0000000..d900c59 --- /dev/null +++ b/config/crd/bases/configkeeper.run.ai_argorolloutconfigkeeperclusterscopes.yaml @@ -0,0 +1,83 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: argorolloutconfigkeeperclusterscopes.configkeeper.run.ai +spec: + group: configkeeper.run.ai + names: + kind: ArgoRolloutConfigKeeperClusterScope + listKind: ArgoRolloutConfigKeeperClusterScopeList + plural: argorolloutconfigkeeperclusterscopes + singular: argorolloutconfigkeeperclusterscope + scope: Cluster + versions: + - additionalPrinterColumns: + - description: The name of managed Finalizer + jsonPath: .spec.finalizerName + name: FinalizerName + type: string + - description: Aqua Database Age + jsonPath: ..metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ArgoRolloutConfigKeeperClusterScope is the Schema for the argorolloutconfigkeeperclusterscopes + 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: ArgoRolloutConfigKeeperClusterScopeSpec defines the desired + state of ArgoRolloutConfigKeeperClusterScope + properties: + appLabel: + type: string + appVersionLabel: + type: string + configLabelSelector: + additionalProperties: + type: string + type: object + finalizerName: + type: string + ignoredNamespaces: + items: + type: string + type: array + required: + - finalizerName + type: object + status: + description: ArgoRolloutConfigKeeperClusterScopeStatus defines the observed + state of ArgoRolloutConfigKeeperClusterScope + properties: + state: + type: string + required: + - state + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index bd6d840..2008508 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,17 +3,20 @@ # It should be run by config/default resources: - bases/configkeeper.run.ai_argorolloutconfigkeepers.yaml +- bases/configkeeper.run.ai_argorolloutconfigkeeperclusterscopes.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- path: patches/webhook_in_argorolloutconfigkeepers.yaml +#- path: patches/webhook_in_argorolloutconfigkeeperclusterscopes.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- path: patches/cainjection_in_argorolloutconfigkeepers.yaml +#- path: patches/cainjection_in_argorolloutconfigkeeperclusterscopes.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # [WEBHOOK] To enable webhook, uncomment the following section diff --git a/config/rbac/argorolloutconfigkeeperclusterscope_editor_role.yaml b/config/rbac/argorolloutconfigkeeperclusterscope_editor_role.yaml new file mode 100644 index 0000000..2b1726d --- /dev/null +++ b/config/rbac/argorolloutconfigkeeperclusterscope_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit argorolloutconfigkeeperclusterscopes. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: argorolloutconfigkeeperclusterscope-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: argo-rollout-config-keeper + app.kubernetes.io/part-of: argo-rollout-config-keeper + app.kubernetes.io/managed-by: kustomize + name: argorolloutconfigkeeperclusterscope-editor-role +rules: +- apiGroups: + - configkeeper.run.ai + resources: + - argorolloutconfigkeeperclusterscopes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - configkeeper.run.ai + resources: + - argorolloutconfigkeeperclusterscopes/status + verbs: + - get diff --git a/config/rbac/argorolloutconfigkeeperclusterscope_viewer_role.yaml b/config/rbac/argorolloutconfigkeeperclusterscope_viewer_role.yaml new file mode 100644 index 0000000..903d026 --- /dev/null +++ b/config/rbac/argorolloutconfigkeeperclusterscope_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view argorolloutconfigkeeperclusterscopes. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: argorolloutconfigkeeperclusterscope-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: argo-rollout-config-keeper + app.kubernetes.io/part-of: argo-rollout-config-keeper + app.kubernetes.io/managed-by: kustomize + name: argorolloutconfigkeeperclusterscope-viewer-role +rules: +- apiGroups: + - configkeeper.run.ai + resources: + - argorolloutconfigkeeperclusterscopes + verbs: + - get + - list + - watch +- apiGroups: + - configkeeper.run.ai + resources: + - argorolloutconfigkeeperclusterscopes/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 026b493..d657b03 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -24,6 +24,32 @@ rules: - patch - update - watch +- apiGroups: + - configkeeper.run.ai + resources: + - argorolloutconfigkeeperclusterscopes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - configkeeper.run.ai + resources: + - argorolloutconfigkeeperclusterscopes/finalizers + verbs: + - update +- apiGroups: + - configkeeper.run.ai + resources: + - argorolloutconfigkeeperclusterscopes/status + verbs: + - get + - patch + - update - apiGroups: - configkeeper.run.ai resources: diff --git a/config/samples/keeper_v1alpha1_argorolloutconfigkeeper.yaml b/config/samples/configkeeper_v1alpha1_argorolloutconfigkeeper.yaml similarity index 100% rename from config/samples/keeper_v1alpha1_argorolloutconfigkeeper.yaml rename to config/samples/configkeeper_v1alpha1_argorolloutconfigkeeper.yaml diff --git a/config/samples/configkeeper_v1alpha1_argorolloutconfigkeeperclusterscope.yaml b/config/samples/configkeeper_v1alpha1_argorolloutconfigkeeperclusterscope.yaml new file mode 100644 index 0000000..d48141a --- /dev/null +++ b/config/samples/configkeeper_v1alpha1_argorolloutconfigkeeperclusterscope.yaml @@ -0,0 +1,18 @@ +apiVersion: configkeeper.run.ai/v1alpha1 +kind: ArgoRolloutConfigKeeperClusterScope +metadata: + labels: + app.kubernetes.io/name: argorolloutconfigkeeperclusterscope + app.kubernetes.io/instance: argorolloutconfigkeeperclusterscope-sample + app.kubernetes.io/part-of: argo-rollout-config-keeper + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: argo-rollout-config-keeper + name: argorolloutconfigkeeperclusterscope-sample +spec: + finalizerName: argorolloutconfigkeeper.app.test + appLabel: app.kubernetes.io/name + appVersionLabel: app.kubernetes.io/version + configLabelSelector: + "app.kubernetes.io/part-of": keeper-testing + ignoredNamespaces: + - kube-system diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index fbda8d8..08c2272 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,4 +1,5 @@ ## Append samples of your project ## resources: - keeper_v1alpha1_argorolloutconfigkeeper.yaml +- configkeeper_v1alpha1_argorolloutconfigkeeperclusterscope.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/go.mod b/go.mod index 1a09a4f..d8b24f6 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/run-ai/argo-rollout-config-keeper go 1.22.4 require ( - github.com/blang/semver/v4 v4.0.0 github.com/go-logr/logr v1.3.0 github.com/hashicorp/go-version v1.7.0 github.com/onsi/ginkgo/v2 v2.13.0 diff --git a/internal/common/common.go b/internal/common/common.go new file mode 100644 index 0000000..3a9cb9e --- /dev/null +++ b/internal/common/common.go @@ -0,0 +1,358 @@ +package common + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + goversion "github.com/hashicorp/go-version" + keeperv1alpha1 "github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1" + "github.com/run-ai/argo-rollout-config-keeper/internal" + "github.com/run-ai/argo-rollout-config-keeper/internal/metrics" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "strings" + "time" +) + +type ArgoRolloutConfigKeeperLabels struct { + AppLabel string + AppVersionLabel string +} + +type ArgoRolloutConfigKeeperCommon struct { + client.Client + Scheme *runtime.Scheme + Logger logr.Logger + Labels *ArgoRolloutConfigKeeperLabels + FinalizerName string +} + +func (r *ArgoRolloutConfigKeeperCommon) ReconcileConfigMaps(ctx context.Context, namespace string, labelSelector map[string]string, ignoredNamespaces map[string]bool) error { + defer func() { + metrics.ConfigMapReconcileDuration.Observe(time.Since(time.Now()).Seconds()) + }() + metrics.ManagedConfigMapCount.Set(0) + + if configMaps, err := r.listConfigMaps(ctx, namespace, labelSelector); err != nil { + return err + } else { + metrics.DiscoveredConfigMapCount.Set(float64(len(configMaps.Items))) + if configMaps.Items != nil { + for _, c := range configMaps.Items { + r.Logger.Info(fmt.Sprintf("configmap, name: %s", c.Name)) + if _, ok := ignoredNamespaces[c.Namespace]; ok { + r.Logger.Info(fmt.Sprintf("skipping %s configmap, reason: namespace is ignored", c.Name)) + continue + } + if c.Finalizers != nil { + // check if the finalizer is in the finalizers list + finalizerFullName, havingManagedFinalizer := internal.ContainsString(c.GetFinalizers(), r.FinalizerName) + + if havingManagedFinalizer { + metrics.ManagedConfigMapCount.Inc() + if err := r.finalizerOperation(ctx, &c, finalizerFullName); err != nil { + return err + } + + if latestVersion, err := r.getLatestVersionOfReplicaSet(ctx, c.Namespace, strings.Split(finalizerFullName, "/")[1]); err != nil { + r.Logger.Error(err, "unable to get latest version of replicaset") + return err + } else { + if err := r.ignoreExtraneousOperation(ctx, &c, latestVersion); err != nil { + return err + } + } + } else { + r.Logger.Info(fmt.Sprintf("skipping %s configmap, reason: no manageable finalizer", c.Name)) + } + continue + } + r.Logger.Info(fmt.Sprintf("skipping %s configmap, reason: no finalizers", c.Name)) + } + } else { + if namespace != "" { + r.Logger.Info(fmt.Sprintf("no configmaps found in %s namespace", namespace)) + } else { + r.Logger.Info("no configmaps found") + } + } + } + + return nil +} + +func (r *ArgoRolloutConfigKeeperCommon) ReconcileSecrets(ctx context.Context, namespace string, labelSelector map[string]string, ignoredNamespaces map[string]bool) error { + defer func() { + metrics.SecretReconcileDuration.Observe(time.Since(time.Now()).Seconds()) + }() + metrics.ManagedSecretCount.Set(0) + + if secrets, err := r.listSecrets(ctx, namespace, labelSelector); err != nil { + return err + } else { + metrics.DiscoveredSecretCount.Set(float64(len(secrets.Items))) + if secrets.Items != nil { + for _, s := range secrets.Items { + r.Logger.Info(fmt.Sprintf("secret, name: %s", s.Name)) + if _, ok := ignoredNamespaces[s.Namespace]; ok { + r.Logger.Info(fmt.Sprintf("skipping %s secret, reason: namespace is ignored", s.Name)) + continue + } + + if s.Finalizers != nil { + finalizerFullName, havingManagedFinalizer := internal.ContainsString(s.GetFinalizers(), r.FinalizerName) + + if havingManagedFinalizer { + metrics.ManagedSecretCount.Inc() + if err := r.finalizerOperation(ctx, &s, finalizerFullName); err != nil { + return err + } + + if latestVersion, err := r.getLatestVersionOfReplicaSet(ctx, s.Namespace, strings.Split(finalizerFullName, "/")[1]); err != nil { + r.Logger.Error(err, "unable to get latest version of replicaset") + return err + } else { + if err := r.ignoreExtraneousOperation(ctx, &s, latestVersion); err != nil { + return err + } + } + } else { + r.Logger.Info(fmt.Sprintf("skipping %s secret, reason: no manageable finalizer", s.Name)) + } + continue + } + r.Logger.Info(fmt.Sprintf("skipping %s secret, reason: no finalizers", s.Name)) + } + } else { + if namespace != "" { + r.Logger.Info(fmt.Sprintf("no secrets found in %s namespace", namespace)) + } else { + r.Logger.Info("no secrets found") + } + } + } + + return nil +} + +func (r *ArgoRolloutConfigKeeperCommon) finalizerOperation(ctx context.Context, T interface{}, finalizer string) error { + // Check the type of the object + switch t := T.(type) { + case *corev1.ConfigMap: + r.Logger.Info(fmt.Sprintf("finalizer operation on configmap object, name: %s", t.Name)) + inUse, err := r.checkIfFinalizerInUse(ctx, t.Namespace, strings.Split(finalizer, "/")[1], t.Labels[r.Labels.AppVersionLabel]) + if err != nil { + return err + } + if !inUse { + r.Logger.Info(fmt.Sprintf("removing finalizer from configmap, name: %s, reason: finalizer not in use", t.Name)) + t.ObjectMeta.Finalizers = internal.RemoveString(t.ObjectMeta.Finalizers, finalizer) + err = r.Update(ctx, t) + if err != nil { + r.Logger.Error(err, "unable to remove finalizer from configmap", "name", t.Name) + } + metrics.ManagedConfigMapCount.Dec() + } + return err + case *corev1.Secret: + r.Logger.Info(fmt.Sprintf("finalizer operation on secret object, name: %s", t.Name)) + inUse, err := r.checkIfFinalizerInUse(ctx, t.Namespace, strings.Split(finalizer, "/")[1], t.Labels[r.Labels.AppVersionLabel]) + if err != nil { + return err + } + if !inUse { + r.Logger.Info(fmt.Sprintf("removing finalizer from secret, name: %s, reason: finalizer not in use", t.Name)) + t.ObjectMeta.Finalizers = internal.RemoveString(t.ObjectMeta.Finalizers, finalizer) + err = r.Update(ctx, t) + if err != nil { + r.Logger.Error(err, "unable to remove finalizer from secret", "name", t.Name) + } + metrics.ManagedSecretCount.Dec() + } + return err + default: + return fmt.Errorf("unsupported type: %T", T) + } + +} + +func (r *ArgoRolloutConfigKeeperCommon) ignoreExtraneousOperation(ctx context.Context, T interface{}, latestVersion *goversion.Version) error { + switch t := T.(type) { + case *corev1.ConfigMap: + configMapVersion, err := goversion.NewVersion(t.Labels[r.Labels.AppVersionLabel]) + if err != nil { + r.Logger.Error(err, "unable to parse version") + return err + } + if configMapVersion.LessThan(latestVersion) { + r.Logger.Info(fmt.Sprintf("adding IgnoreExtraneous annotation to %s configmap, reason: version is less than latest version", t.Name)) + if t.Annotations != nil { + t.Annotations["argocd.argoproj.io/compare-options"] = "IgnoreExtraneous" + } else { + t.Annotations = map[string]string{"argocd.argoproj.io/compare-options": "IgnoreExtraneous"} + } + + err = r.Update(ctx, t) + if err != nil { + r.Logger.Error(err, "unable to update configmap") + return err + } + } + return nil + case *corev1.Secret: + secretVersion, err := goversion.NewVersion(t.Labels[r.Labels.AppVersionLabel]) + if err != nil { + r.Logger.Error(err, "unable to parse version") + return err + } + if secretVersion.LessThan(latestVersion) { + r.Logger.Info(fmt.Sprintf("adding IgnoreExtraneous annotation to %s secret, reason: version is less than latest version", t.Name)) + if t.Annotations != nil { + t.Annotations["argocd.argoproj.io/compare-options"] = "IgnoreExtraneous" + } else { + t.Annotations = map[string]string{"argocd.argoproj.io/compare-options": "IgnoreExtraneous"} + } + err = r.Update(ctx, t) + if err != nil { + r.Logger.Error(err, "unable to update secret") + return err + } + } + return nil + default: + return fmt.Errorf("unsupported type: %T", T) + } +} + +func (r *ArgoRolloutConfigKeeperCommon) getFilteredReplicaSets(ctx context.Context, namespace string, labelSelector map[string]string) (*appsv1.ReplicaSetList, error) { + // need to list all ReplicaSets in namespace and filter by label + replicaSets := &appsv1.ReplicaSetList{} + + if err := r.List(ctx, replicaSets, client.InNamespace(namespace), client.MatchingLabels(labelSelector)); err != nil { + r.Logger.Error(err, fmt.Sprintf("unable to list replicasets in namespace %s", namespace)) + return nil, client.IgnoreNotFound(err) + } + + return replicaSets, nil +} + +func (r *ArgoRolloutConfigKeeperCommon) checkIfFinalizerInUse(ctx context.Context, namespace, appLabelValue, chartVersion string) (bool, error) { + labelSelector := map[string]string{ + r.Labels.AppLabel: appLabelValue, + r.Labels.AppVersionLabel: chartVersion, + } + replicaSets, err := r.getFilteredReplicaSets(ctx, namespace, labelSelector) + + if err != nil { + r.Logger.Error(err, "unable to get filtered replicasets") + return false, err + } + + for _, replicaSet := range replicaSets.Items { + replicaNum := int32(0) + + if replicaSet.Labels[r.Labels.AppLabel] == appLabelValue && replicaSet.Labels[r.Labels.AppVersionLabel] == chartVersion && (*replicaSet.Spec.Replicas != replicaNum || replicaSet.Status.Replicas != replicaNum) { + r.Logger.Info(fmt.Sprintf("finalizer in use by %s replicaset", replicaSet.Name)) + return true, nil + } + } + return false, nil +} + +func (r *ArgoRolloutConfigKeeperCommon) getLatestVersionOfReplicaSet(ctx context.Context, namespace, appLabelValue string) (*goversion.Version, error) { + // need to list all ReplicaSets in namespace and filter by label + labelSelector := map[string]string{ + r.Labels.AppLabel: appLabelValue, + } + replicaSets, err := r.getFilteredReplicaSets(ctx, namespace, labelSelector) + + if err != nil { + r.Logger.Error(err, "unable to get filtered replicasets") + return nil, err + } + + var latestVersion *goversion.Version + + for _, replicaSet := range replicaSets.Items { + + if val, ok := replicaSet.Labels[r.Labels.AppVersionLabel]; ok { + ver, err := goversion.NewVersion(val) + if err != nil { + r.Logger.Error(err, "unable to parse version") + continue + } + if latestVersion == nil || ver.GreaterThan(latestVersion) { + latestVersion = ver + } + } else { + r.Logger.Info(fmt.Sprintf("replicaset %s does not have %s label", replicaSet.Name, r.Labels.AppVersionLabel)) + continue + } + } + return latestVersion, nil +} + +func (r *ArgoRolloutConfigKeeperCommon) UpdateStatus(ctx context.Context, T interface{}, status string) error { + switch t := T.(type) { + case *keeperv1alpha1.ArgoRolloutConfigKeeper: + if t.Status.State != status { + r.Logger.Info(fmt.Sprintf("updating %s ArgoRolloutConfigKeeper status from %s to %s", t.Name, t.Status.State, status)) + t.Status.State = status + err := r.Status().Update(ctx, t) + if err != nil { + r.Logger.Error(err, "unable to update status") + return err + } + // the sleep is to allow the status to be updated before the next reconcile + time.Sleep(1 * time.Second) + } + return nil + case *keeperv1alpha1.ArgoRolloutConfigKeeperClusterScope: + if t.Status.State != status { + r.Logger.Info(fmt.Sprintf("updating %s ArgoRolloutConfigKeeper status from %s to %s", t.Name, t.Status.State, status)) + t.Status.State = status + err := r.Status().Update(ctx, t) + if err != nil { + r.Logger.Error(err, "unable to update status") + return err + } + // the sleep is to allow the status to be updated before the next reconcile + time.Sleep(1 * time.Second) + } + return nil + default: + return fmt.Errorf("unsupported type: %T", T) + } +} + +func (r *ArgoRolloutConfigKeeperCommon) listConfigMaps(ctx context.Context, namespace string, labelSelector map[string]string) (*corev1.ConfigMapList, error) { + configmaps := &corev1.ConfigMapList{} + + if err := r.List(ctx, configmaps, client.InNamespace(namespace), client.MatchingLabels(labelSelector)); err != nil { + if namespace != "" { + r.Logger.Error(err, fmt.Sprintf("unable to list configmaps in %s namespace", namespace)) + } else { + r.Logger.Error(err, "unable to list configmaps") + } + return nil, client.IgnoreNotFound(err) + } + + return configmaps, nil +} + +func (r *ArgoRolloutConfigKeeperCommon) listSecrets(ctx context.Context, namespace string, labelSelector map[string]string) (*corev1.SecretList, error) { + secrets := &corev1.SecretList{} + + if err := r.List(ctx, secrets, client.InNamespace(namespace), client.MatchingLabels(labelSelector)); err != nil { + if namespace != "" { + r.Logger.Error(err, fmt.Sprintf("unable to list secrets in %s namespace", namespace)) + } else { + r.Logger.Error(err, "unable to list secrets") + } + return nil, client.IgnoreNotFound(err) + } + + return secrets, nil +} diff --git a/internal/controller/argorolloutconfigkeeper_controller.go b/internal/controller/argorolloutconfigkeeper_controller.go index ecd9748..168827d 100644 --- a/internal/controller/argorolloutconfigkeeper_controller.go +++ b/internal/controller/argorolloutconfigkeeper_controller.go @@ -18,17 +18,12 @@ package controller import ( "context" - "fmt" - "strings" "time" "github.com/go-logr/logr" - goversion "github.com/hashicorp/go-version" keeperv1alpha1 "github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1" - "github.com/run-ai/argo-rollout-config-keeper/internal" + "github.com/run-ai/argo-rollout-config-keeper/internal/common" "github.com/run-ai/argo-rollout-config-keeper/internal/metrics" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -37,24 +32,20 @@ import ( ) // ArgoRolloutConfigKeeperReconciler reconciles a ArgoRolloutConfigKeeper object - -type ArgoRolloutConfigKeeperLabels struct { - AppLabel string - AppVersionLabel string -} - type ArgoRolloutConfigKeeperReconciler struct { client.Client Scheme *runtime.Scheme logger logr.Logger - Labels *ArgoRolloutConfigKeeperLabels } const ( - ArgoRolloutConfigStateInitializing = "initializing" - ArgoRolloutConfigStateReconcilingConfigmaps = "reconciling configmaps" - ArgoRolloutConfigStateReconcilingSecrets = "reconciling secrets" - ArgoRolloutConfigStateFinished = "finished" + ArgoRolloutConfigStateInitializing = "initializing" + ArgoRolloutConfigStateReconcilingNamespace = "reconciling namespace %s" + ArgoRolloutConfigStateReconcilingConfigmapsInNamespace = "reconciling configmaps in namespace %s" + ArgoRolloutConfigStateReconcilingSecretsInNamespace = "reconciling secrets in namespace %s" + ArgoRolloutConfigStateReconcilingConfigmaps = "reconciling configmaps" + ArgoRolloutConfigStateReconcilingSecrets = "reconciling secrets" + ArgoRolloutConfigStateFinished = "finished" ) //+kubebuilder:rbac:groups=configkeeper.run.ai,resources=argorolloutconfigkeepers,verbs=get;list;watch;create;update;patch;delete @@ -68,63 +59,49 @@ func (r *ArgoRolloutConfigKeeperReconciler) Reconcile(ctx context.Context, req c defer func() { metrics.OverallReconcileDuration.Observe(time.Since(time.Now()).Seconds()) }() + configKeeperCommon := common.ArgoRolloutConfigKeeperCommon{ + Client: r.Client, + Scheme: r.Scheme, + Logger: r.logger, + } - //r.logger.Error(nil, "reconciling ArgoRolloutConfigKeeper") configKeeper := &keeperv1alpha1.ArgoRolloutConfigKeeper{} if err := r.Get(ctx, req.NamespacedName, configKeeper); err != nil { return ctrl.Result{RequeueAfter: 1 * time.Minute}, client.IgnoreNotFound(err) } - if err := r.updateStatus(ctx, configKeeper, ArgoRolloutConfigStateInitializing); err != nil { + if err := configKeeperCommon.UpdateStatus(ctx, configKeeper, ArgoRolloutConfigStateInitializing); err != nil { return ctrl.Result{RequeueAfter: 1 * time.Minute}, err } - r.Labels = &ArgoRolloutConfigKeeperLabels{ + configKeeperCommon.Labels = &common.ArgoRolloutConfigKeeperLabels{ AppLabel: configKeeper.Spec.AppLabel, AppVersionLabel: configKeeper.Spec.AppVersionLabel, } - if err := r.updateStatus(ctx, configKeeper, ArgoRolloutConfigStateReconcilingConfigmaps); err != nil { + configKeeperCommon.FinalizerName = configKeeper.Spec.FinalizerName + + if err := configKeeperCommon.UpdateStatus(ctx, configKeeper, ArgoRolloutConfigStateReconcilingConfigmaps); err != nil { return ctrl.Result{RequeueAfter: 1 * time.Minute}, err } - configmaps := &corev1.ConfigMapList{} - r.logger.Info(fmt.Sprintf("listing configmaps in %s namespace", req.Namespace)) labelSelector := map[string]string{} if configKeeper.Spec.ConfigLabelSelector != nil { labelSelector = configKeeper.Spec.ConfigLabelSelector } - if err := r.List(ctx, configmaps, client.InNamespace(req.Namespace), client.MatchingLabels(labelSelector)); err != nil { - r.logger.Error(err, fmt.Sprintf("unable to list configmaps in %s namespace", req.Namespace)) + if err := configKeeperCommon.ReconcileConfigMaps(ctx, req.Namespace, labelSelector, map[string]bool{}); err != nil { return ctrl.Result{RequeueAfter: 1 * time.Minute}, client.IgnoreNotFound(err) } - metrics.DiscoveredConfigMapCount.Set(float64(len(configmaps.Items))) - if configmaps.Items != nil { - if err := r.reconcileConfigMaps(ctx, configmaps, configKeeper); err != nil { - r.logger.Error(err, "unable to reconcile configmaps") - return ctrl.Result{RequeueAfter: 1 * time.Minute}, client.IgnoreNotFound(err) - } - } else { - r.logger.Info(fmt.Sprintf("no configmaps found in %s namespace", req.Namespace)) - } - if err := r.updateStatus(ctx, configKeeper, ArgoRolloutConfigStateReconcilingSecrets); err != nil { + + if err := configKeeperCommon.UpdateStatus(ctx, configKeeper, ArgoRolloutConfigStateReconcilingSecrets); err != nil { return ctrl.Result{RequeueAfter: 1 * time.Minute}, err } + // need to list all secrets in namespace - secrets := &corev1.SecretList{} - if err := r.List(ctx, secrets, client.InNamespace(req.Namespace), client.MatchingLabels(labelSelector)); err != nil { - r.logger.Error(err, fmt.Sprintf("unable to list secrets in %s namespace", req.Namespace)) + if err := configKeeperCommon.ReconcileSecrets(ctx, req.Namespace, labelSelector, map[string]bool{}); err != nil { return ctrl.Result{RequeueAfter: 1 * time.Minute}, client.IgnoreNotFound(err) } - metrics.DiscoveredSecretCount.Set(float64(len(secrets.Items))) - if secrets.Items != nil { - if err := r.reconcileSecrets(ctx, secrets, configKeeper); err != nil { - r.logger.Error(err, "unable to reconcile secrets") - return ctrl.Result{RequeueAfter: 1 * time.Minute}, client.IgnoreNotFound(err) - } - } else { - r.logger.Info(fmt.Sprintf("no secrets found in %s namespace", req.Namespace)) - } - if err := r.updateStatus(ctx, configKeeper, ArgoRolloutConfigStateFinished); err != nil { + + if err := configKeeperCommon.UpdateStatus(ctx, configKeeper, ArgoRolloutConfigStateFinished); err != nil { return ctrl.Result{RequeueAfter: 1 * time.Minute}, err } return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil @@ -137,244 +114,3 @@ func (r *ArgoRolloutConfigKeeperReconciler) SetupWithManager(mgr ctrl.Manager) e WithEventFilter(predicate.GenerationChangedPredicate{}). Complete(r) } - -func (r *ArgoRolloutConfigKeeperReconciler) reconcileConfigMaps(ctx context.Context, configmaps *corev1.ConfigMapList, configKeeper *keeperv1alpha1.ArgoRolloutConfigKeeper) error { - defer func() { - metrics.ConfigMapReconcileDuration.Observe(time.Since(time.Now()).Seconds()) - }() - metrics.ManagedConfigMapCount.Set(0) - for _, c := range configmaps.Items { - r.logger.Info(fmt.Sprintf("configmap, name: %s", c.Name)) - if c.Finalizers != nil { - // check if the finalizer is in the finalizers list - finalizerFullName, havingManagedFinalizer := internal.ContainsString(c.GetFinalizers(), configKeeper.Spec.FinalizerName) - - if havingManagedFinalizer { - metrics.ManagedConfigMapCount.Inc() - if err := r.finalizerOperation(ctx, &c, finalizerFullName); err != nil { - return err - } - - if latestVersion, err := r.getLatestVersionOfReplicaSet(ctx, c.Namespace, strings.Split(finalizerFullName, "/")[1]); err != nil { - r.logger.Error(err, "unable to get latest version of replicaset") - return err - } else { - if err := r.ignoreExtraneousOperation(ctx, &c, latestVersion); err != nil { - return err - } - } - } else { - r.logger.Info(fmt.Sprintf("skipping %s configmap, reason: no manageable finalizer", c.Name)) - } - continue - } - r.logger.Info(fmt.Sprintf("skipping %s configmap, reason: no finalizers", c.Name)) - } - return nil -} - -func (r *ArgoRolloutConfigKeeperReconciler) reconcileSecrets(ctx context.Context, secrets *corev1.SecretList, configKeeper *keeperv1alpha1.ArgoRolloutConfigKeeper) error { - defer func() { - metrics.SecretReconcileDuration.Observe(time.Since(time.Now()).Seconds()) - }() - metrics.ManagedSecretCount.Set(0) - for _, s := range secrets.Items { - r.logger.Info(fmt.Sprintf("secret, name: %s", s.Name)) - if s.Finalizers != nil { - finalizerFullName, havingManagedFinalizer := internal.ContainsString(s.GetFinalizers(), configKeeper.Spec.FinalizerName) - - if havingManagedFinalizer { - metrics.ManagedSecretCount.Inc() - if err := r.finalizerOperation(ctx, &s, finalizerFullName); err != nil { - return err - } - - if latestVersion, err := r.getLatestVersionOfReplicaSet(ctx, s.Namespace, strings.Split(finalizerFullName, "/")[1]); err != nil { - r.logger.Error(err, "unable to get latest version of replicaset") - return err - } else { - if err := r.ignoreExtraneousOperation(ctx, &s, latestVersion); err != nil { - return err - } - } - } else { - r.logger.Info(fmt.Sprintf("skipping %s secret, reason: no manageable finalizer", s.Name)) - } - continue - } - r.logger.Info(fmt.Sprintf("skipping %s secret, reason: no finalizers", s.Name)) - } - return nil -} - -func (r *ArgoRolloutConfigKeeperReconciler) finalizerOperation(ctx context.Context, T interface{}, finalizer string) error { - // Check the type of the object - switch t := T.(type) { - case *corev1.ConfigMap: - r.logger.Info(fmt.Sprintf("finalizer operation on configmap object, name: %s", t.Name)) - inUse, err := r.checkIfFinalizerInUse(ctx, t.Namespace, strings.Split(finalizer, "/")[1], t.Labels[r.Labels.AppVersionLabel]) - if err != nil { - return err - } - if !inUse { - r.logger.Info(fmt.Sprintf("removing finalizer from configmap, name: %s, reason: finalizer not in use", t.Name)) - t.ObjectMeta.Finalizers = internal.RemoveString(t.ObjectMeta.Finalizers, finalizer) - err = r.Update(ctx, t) - if err != nil { - r.logger.Error(err, "unable to remove finalizer from configmap", "name", t.Name) - } - metrics.ManagedConfigMapCount.Dec() - } - return err - case *corev1.Secret: - r.logger.Info(fmt.Sprintf("finalizer operation on secret object, name: %s", t.Name)) - inUse, err := r.checkIfFinalizerInUse(ctx, t.Namespace, strings.Split(finalizer, "/")[1], t.Labels[r.Labels.AppVersionLabel]) - if err != nil { - return err - } - if !inUse { - r.logger.Info(fmt.Sprintf("removing finalizer from secret, name: %s, reason: finalizer not in use", t.Name)) - t.ObjectMeta.Finalizers = internal.RemoveString(t.ObjectMeta.Finalizers, finalizer) - err = r.Update(ctx, t) - if err != nil { - r.logger.Error(err, "unable to remove finalizer from secret", "name", t.Name) - } - metrics.ManagedSecretCount.Dec() - } - return err - default: - return fmt.Errorf("unsupported type: %T", T) - } - -} - -func (r *ArgoRolloutConfigKeeperReconciler) ignoreExtraneousOperation(ctx context.Context, T interface{}, latestVersion *goversion.Version) error { - switch t := T.(type) { - case *corev1.ConfigMap: - configMapVersion, err := goversion.NewVersion(t.Labels[r.Labels.AppVersionLabel]) - if err != nil { - r.logger.Error(err, "unable to parse version") - return err - } - if configMapVersion.LessThan(latestVersion) { - r.logger.Info(fmt.Sprintf("adding IgnoreExtraneous annotation to %s configmap, reason: version is less than latest version", t.Name)) - if t.Annotations != nil { - t.Annotations["argocd.argoproj.io/compare-options"] = "IgnoreExtraneous" - } else { - t.Annotations = map[string]string{"argocd.argoproj.io/compare-options": "IgnoreExtraneous"} - } - - err = r.Update(ctx, t) - if err != nil { - r.logger.Error(err, "unable to update configmap") - return err - } - } - return nil - case *corev1.Secret: - secretVersion, err := goversion.NewVersion(t.Labels[r.Labels.AppVersionLabel]) - if err != nil { - r.logger.Error(err, "unable to parse version") - return err - } - if secretVersion.LessThan(latestVersion) { - r.logger.Info(fmt.Sprintf("adding IgnoreExtraneous annotation to %s secret, reason: version is less than latest version", t.Name)) - if t.Annotations != nil { - t.Annotations["argocd.argoproj.io/compare-options"] = "IgnoreExtraneous" - } else { - t.Annotations = map[string]string{"argocd.argoproj.io/compare-options": "IgnoreExtraneous"} - } - err = r.Update(ctx, t) - if err != nil { - r.logger.Error(err, "unable to update secret") - return err - } - } - return nil - default: - return fmt.Errorf("unsupported type: %T", T) - } -} - -func (r *ArgoRolloutConfigKeeperReconciler) getFilteredReplicaSets(ctx context.Context, namespace string, labelSelector map[string]string) (*appsv1.ReplicaSetList, error) { - // need to list all ReplicaSets in namespace and filter by label - replicaSets := &appsv1.ReplicaSetList{} - - if err := r.List(ctx, replicaSets, client.InNamespace(namespace), client.MatchingLabels(labelSelector)); err != nil { - r.logger.Error(err, fmt.Sprintf("unable to list replicasets in namespace %s", namespace)) - return nil, client.IgnoreNotFound(err) - } - - return replicaSets, nil -} - -func (r *ArgoRolloutConfigKeeperReconciler) checkIfFinalizerInUse(ctx context.Context, namespace, appLabelValue, chartVersion string) (bool, error) { - labelSelector := map[string]string{ - r.Labels.AppLabel: appLabelValue, - r.Labels.AppVersionLabel: chartVersion, - } - replicaSets, err := r.getFilteredReplicaSets(ctx, namespace, labelSelector) - - if err != nil { - r.logger.Error(err, "unable to get filtered replicasets") - return false, err - } - - for _, replicaSet := range replicaSets.Items { - replicaNum := int32(0) - - if replicaSet.Labels[r.Labels.AppLabel] == appLabelValue && replicaSet.Labels[r.Labels.AppVersionLabel] == chartVersion && (*replicaSet.Spec.Replicas != replicaNum || replicaSet.Status.Replicas != replicaNum) { - r.logger.Info(fmt.Sprintf("finalizer in use by %s replicaset", replicaSet.Name)) - return true, nil - } - } - return false, nil -} - -func (r *ArgoRolloutConfigKeeperReconciler) getLatestVersionOfReplicaSet(ctx context.Context, namespace, appLabelValue string) (*goversion.Version, error) { - // need to list all ReplicaSets in namespace and filter by label - labelSelector := map[string]string{ - r.Labels.AppLabel: appLabelValue, - } - replicaSets, err := r.getFilteredReplicaSets(ctx, namespace, labelSelector) - - if err != nil { - r.logger.Error(err, "unable to get filtered replicasets") - return nil, err - } - - var latestVersion *goversion.Version - - for _, replicaSet := range replicaSets.Items { - - if val, ok := replicaSet.Labels[r.Labels.AppVersionLabel]; ok { - ver, err := goversion.NewVersion(val) - if err != nil { - r.logger.Error(err, "unable to parse version") - continue - } - if latestVersion == nil || ver.GreaterThan(latestVersion) { - latestVersion = ver - } - } else { - r.logger.Info(fmt.Sprintf("replicaset %s does not have %s label", replicaSet.Name, r.Labels.AppVersionLabel)) - continue - } - } - return latestVersion, nil -} - -func (r *ArgoRolloutConfigKeeperReconciler) updateStatus(ctx context.Context, configKeeper *keeperv1alpha1.ArgoRolloutConfigKeeper, status string) error { - if configKeeper.Status.State != status { - r.logger.Info(fmt.Sprintf("updating %s ArgoRolloutConfigKeeper status from %s to %s", configKeeper.Name, configKeeper.Status.State, status)) - configKeeper.Status.State = status - err := r.Status().Update(ctx, configKeeper) - if err != nil { - r.logger.Error(err, "unable to update status") - return err - } - // the sleep is to allow the status to be updated before the next reconcile - time.Sleep(1 * time.Second) - } - return nil -} diff --git a/internal/controller/argorolloutconfigkeeper_controller_test.go b/internal/controller/argorolloutconfigkeeper_controller_test.go index f930dc4..670280f 100644 --- a/internal/controller/argorolloutconfigkeeper_controller_test.go +++ b/internal/controller/argorolloutconfigkeeper_controller_test.go @@ -35,6 +35,8 @@ var ( ctx = context.Background() name = "argo-rollout-config-keeper" namespace = "default" + configMapNamespace = "test-configmap" + secretNamespace = "test-secret" chartName = "testing-chart" appVersion = "1.11.0-staging1" appPreviewVersion = "1.11.0-staging2" @@ -49,14 +51,14 @@ var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { Name: name, Namespace: namespace, } - argorolloutconfigkeeper := &keeperv1alpha1.ArgoRolloutConfigKeeper{} + argoRolloutConfigKeeper := &keeperv1alpha1.ArgoRolloutConfigKeeper{} Context("Configmap Tests", func() { BeforeAll(func() { - manageConfigmaps(ctx, "create") - manageReplicas(ctx, "create", 1) + manageConfigmaps(ctx, namespace, "create") + manageReplicas(ctx, namespace, "create", 1) By("creating the custom resource for the Kind ArgoRolloutConfigKeeper") - err := k8sClient.Get(ctx, typeNamespacedName, argorolloutconfigkeeper) + err := k8sClient.Get(ctx, typeNamespacedName, argoRolloutConfigKeeper) if err != nil && errors.IsNotFound(err) { resource := &keeperv1alpha1.ArgoRolloutConfigKeeper{ ObjectMeta: metav1.ObjectMeta{ @@ -76,8 +78,8 @@ var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { }) AfterAll(func() { - manageConfigmaps(ctx, "delete") - manageReplicas(ctx, "delete", 0) + manageConfigmaps(ctx, namespace, "delete") + manageReplicas(ctx, namespace, "delete", 0) resource := &keeperv1alpha1.ArgoRolloutConfigKeeper{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -107,19 +109,19 @@ var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { }) Expect(err).NotTo(HaveOccurred()) - configMap, err := getConfigmap(ctx, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + configMap, err := getConfigmap(ctx, namespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) Expect(err).NotTo(HaveOccurred()) Expect(configMap.Finalizers).To(ContainElement(finalizerNameFullName)) }) It("Should remove the finalizer from the configmap and add IgnoreExtraneous annotation", func() { By("Updating the replicaset to 0") - manageReplicas(ctx, "update", 0) + manageReplicas(ctx, namespace, "update", 0) By("Reconciling the created resource") var ( err error configMap *corev1.ConfigMap ) - configMap, err = getConfigmap(ctx, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + configMap, err = getConfigmap(ctx, namespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) controllerReconciler := &ArgoRolloutConfigKeeperReconciler{ Client: k8sClient, @@ -130,7 +132,7 @@ var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { }) Expect(err).NotTo(HaveOccurred()) - configMap, err = getConfigmap(ctx, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + configMap, err = getConfigmap(ctx, namespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) Expect(err).NotTo(HaveOccurred()) Expect(configMap.Finalizers).To(BeNil()) Expect(configMap.Annotations).To(HaveKeyWithValue("argocd.argoproj.io/compare-options", "IgnoreExtraneous")) @@ -139,10 +141,10 @@ var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { Context("Secret Tests", func() { BeforeAll(func() { - manageSecrets(ctx, "create") - manageReplicas(ctx, "create", 1) + manageSecrets(ctx, namespace, "create") + manageReplicas(ctx, namespace, "create", 1) By("creating the custom resource for the Kind ArgoRolloutConfigKeeper") - err := k8sClient.Get(ctx, typeNamespacedName, argorolloutconfigkeeper) + err := k8sClient.Get(ctx, typeNamespacedName, argoRolloutConfigKeeper) if err != nil && errors.IsNotFound(err) { resource := &keeperv1alpha1.ArgoRolloutConfigKeeper{ ObjectMeta: metav1.ObjectMeta{ @@ -159,8 +161,8 @@ var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { }) AfterAll(func() { - manageSecrets(ctx, "delete") - manageReplicas(ctx, "delete", 0) + manageSecrets(ctx, namespace, "delete") + manageReplicas(ctx, namespace, "delete", 0) resource := &keeperv1alpha1.ArgoRolloutConfigKeeper{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -190,20 +192,20 @@ var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { }) Expect(err).NotTo(HaveOccurred()) - secret, err := getSecret(ctx, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + secret, err := getSecret(ctx, namespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) Expect(err).NotTo(HaveOccurred()) Expect(secret.Finalizers).To(ContainElement(finalizerNameFullName)) }) It("Should remove the finalizer from the secret and add IgnoreExtraneous annotation", func() { By("Updating the replicaset to 0") - manageReplicas(ctx, "update", 0) + manageReplicas(ctx, namespace, "update", 0) By("Reconciling the created resource") var ( err error secret *corev1.Secret ) - secret, err = getSecret(ctx, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + secret, err = getSecret(ctx, namespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) controllerReconciler := &ArgoRolloutConfigKeeperReconciler{ Client: k8sClient, @@ -214,7 +216,7 @@ var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { }) Expect(err).NotTo(HaveOccurred()) - secret, err = getSecret(ctx, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + secret, err = getSecret(ctx, namespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) Expect(err).NotTo(HaveOccurred()) Expect(secret.Finalizers).To(BeNil()) Expect(secret.Annotations).To(HaveKeyWithValue("argocd.argoproj.io/compare-options", "IgnoreExtraneous")) @@ -223,7 +225,7 @@ var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { }) -func manageConfigmaps(ctx context.Context, operation string) { +func manageConfigmaps(ctx context.Context, namespace, operation string) { configmap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -257,13 +259,13 @@ func manageConfigmaps(ctx context.Context, operation string) { } } -func getConfigmap(ctx context.Context, name string) (*corev1.ConfigMap, error) { +func getConfigmap(ctx context.Context, namespace, name string) (*corev1.ConfigMap, error) { configmap := &corev1.ConfigMap{} err := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, configmap) return configmap, err } -func manageSecrets(ctx context.Context, operation string) { +func manageSecrets(ctx context.Context, namespace, operation string) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion), @@ -297,13 +299,13 @@ func manageSecrets(ctx context.Context, operation string) { } } -func getSecret(ctx context.Context, name string) (*corev1.Secret, error) { +func getSecret(ctx context.Context, namespace, name string) (*corev1.Secret, error) { secret := &corev1.Secret{} err := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, secret) return secret, err } -func manageReplicas(ctx context.Context, operation string, replicaNumber int) { +func manageReplicas(ctx context.Context, namespace, operation string, replicaNumber int) { replicaNum := int32(replicaNumber) replica := &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ @@ -419,3 +421,24 @@ func manageReplicas(ctx context.Context, operation string, replicaNumber int) { panic("Invalid operation") } } + +func manageNamespace(ctx context.Context, name, operation string) { + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + + switch operation { + case "create": + // create namespace + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + break + case "delete": + // delete namespace + Expect(k8sClient.Delete(ctx, namespace)).To(Succeed()) + break + default: + panic("Invalid operation") + } +} diff --git a/internal/controller/argorolloutconfigkeeperclusterscope_controller.go b/internal/controller/argorolloutconfigkeeperclusterscope_controller.go new file mode 100644 index 0000000..32f714f --- /dev/null +++ b/internal/controller/argorolloutconfigkeeperclusterscope_controller.go @@ -0,0 +1,108 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "github.com/go-logr/logr" + "github.com/run-ai/argo-rollout-config-keeper/internal" + "github.com/run-ai/argo-rollout-config-keeper/internal/common" + "github.com/run-ai/argo-rollout-config-keeper/internal/metrics" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "time" + + configkeeperv1alpha1 "github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1" +) + +// ArgoRolloutConfigKeeperClusterScopeReconciler reconciles a ArgoRolloutConfigKeeperClusterScope object +type ArgoRolloutConfigKeeperClusterScopeReconciler struct { + client.Client + Scheme *runtime.Scheme + logger logr.Logger +} + +//+kubebuilder:rbac:groups=configkeeper.run.ai,resources=argorolloutconfigkeeperclusterscopes,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=configkeeper.run.ai,resources=argorolloutconfigkeeperclusterscopes/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=configkeeper.run.ai,resources=argorolloutconfigkeeperclusterscopes/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;update;patch +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;update;patch + +func (r *ArgoRolloutConfigKeeperClusterScopeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.logger = log.FromContext(ctx) + defer func() { + metrics.OverallReconcileDuration.Observe(time.Since(time.Now()).Seconds()) + }() + + configKeeperCommon := common.ArgoRolloutConfigKeeperCommon{ + Client: r.Client, + Scheme: r.Scheme, + Logger: r.logger, + } + + configKeeperClusterScope := &configkeeperv1alpha1.ArgoRolloutConfigKeeperClusterScope{} + if err := r.Get(ctx, req.NamespacedName, configKeeperClusterScope); err != nil { + return ctrl.Result{RequeueAfter: 1 * time.Minute}, client.IgnoreNotFound(err) + } + + if err := configKeeperCommon.UpdateStatus(ctx, configKeeperClusterScope, ArgoRolloutConfigStateInitializing); err != nil { + return ctrl.Result{RequeueAfter: 1 * time.Minute}, err + } + + configKeeperCommon.Labels = &common.ArgoRolloutConfigKeeperLabels{ + AppLabel: configKeeperClusterScope.Spec.AppLabel, + AppVersionLabel: configKeeperClusterScope.Spec.AppVersionLabel, + } + configKeeperCommon.FinalizerName = configKeeperClusterScope.Spec.FinalizerName + + if err := configKeeperCommon.UpdateStatus(ctx, configKeeperClusterScope, ArgoRolloutConfigStateReconcilingConfigmaps); err != nil { + return ctrl.Result{RequeueAfter: 1 * time.Minute}, err + } + + labelSelector := map[string]string{} + if configKeeperClusterScope.Spec.ConfigLabelSelector != nil { + labelSelector = configKeeperClusterScope.Spec.ConfigLabelSelector + } + + ignoredNamespaces := internal.CreateMapFromStringList(configKeeperClusterScope.Spec.IgnoredNamespaces) + + if err := configKeeperCommon.ReconcileConfigMaps(ctx, "", labelSelector, ignoredNamespaces); err != nil { + return ctrl.Result{RequeueAfter: 1 * time.Minute}, client.IgnoreNotFound(err) + } + + if err := configKeeperCommon.UpdateStatus(ctx, configKeeperClusterScope, ArgoRolloutConfigStateReconcilingSecrets); err != nil { + return ctrl.Result{RequeueAfter: 1 * time.Minute}, err + } + // need to list all secrets in namespace + if err := configKeeperCommon.ReconcileSecrets(ctx, "", labelSelector, ignoredNamespaces); err != nil { + return ctrl.Result{RequeueAfter: 1 * time.Minute}, client.IgnoreNotFound(err) + } + + if err := configKeeperCommon.UpdateStatus(ctx, configKeeperClusterScope, ArgoRolloutConfigStateFinished); err != nil { + return ctrl.Result{RequeueAfter: 1 * time.Minute}, err + } + return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ArgoRolloutConfigKeeperClusterScopeReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&configkeeperv1alpha1.ArgoRolloutConfigKeeperClusterScope{}). + Complete(r) +} diff --git a/internal/controller/argorolloutconfigkeeperclusterscope_controller_test.go b/internal/controller/argorolloutconfigkeeperclusterscope_controller_test.go new file mode 100644 index 0000000..7952fa8 --- /dev/null +++ b/internal/controller/argorolloutconfigkeeperclusterscope_controller_test.go @@ -0,0 +1,217 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "fmt" + corev1 "k8s.io/api/core/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + configkeeperv1alpha1 "github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1" +) + +var _ = Describe("ArgoRolloutConfigKeeper Controller", Ordered, func() { + typeNamespacedName := types.NamespacedName{ + Name: name, + Namespace: namespace, + } + argoRolloutConfigKeeperClusterScope := &configkeeperv1alpha1.ArgoRolloutConfigKeeperClusterScope{} + + Context("Configmap Tests", func() { + BeforeAll(func() { + manageNamespace(ctx, configMapNamespace, "create") + manageConfigmaps(ctx, configMapNamespace, "create") + manageReplicas(ctx, configMapNamespace, "create", 1) + By("creating the custom resource for the Kind ArgoRolloutConfigKeeperClusterScope") + err := k8sClient.Get(ctx, typeNamespacedName, argoRolloutConfigKeeperClusterScope) + if err != nil && errors.IsNotFound(err) { + resource := &configkeeperv1alpha1.ArgoRolloutConfigKeeperClusterScope{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: configkeeperv1alpha1.ArgoRolloutConfigKeeperClusterScopeSpec{ + // Add spec details here + FinalizerName: finalizerName, + ConfigLabelSelector: map[string]string{ + "app.kubernetes.io/part-of": partOf, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterAll(func() { + manageConfigmaps(ctx, configMapNamespace, "delete") + manageReplicas(ctx, configMapNamespace, "delete", 0) + manageNamespace(ctx, configMapNamespace, "delete") + resource := &configkeeperv1alpha1.ArgoRolloutConfigKeeperClusterScope{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + By("Cleanup the specific resource instance ArgoRolloutConfigKeeper") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ArgoRolloutConfigKeeperClusterScopeReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + }) + It("ConfigMap should have the finalizer", func() { + By("Reconciling the created resource") + controllerReconciler := &ArgoRolloutConfigKeeperClusterScopeReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + configMap, err := getConfigmap(ctx, configMapNamespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + Expect(err).NotTo(HaveOccurred()) + Expect(configMap.Finalizers).To(ContainElement(finalizerNameFullName)) + }) + It("Should remove the finalizer from the configmap and add IgnoreExtraneous annotation", func() { + By("Updating the replicaset to 0") + manageReplicas(ctx, configMapNamespace, "update", 0) + By("Reconciling the created resource") + var ( + err error + configMap *corev1.ConfigMap + ) + configMap, err = getConfigmap(ctx, configMapNamespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + + controllerReconciler := &ArgoRolloutConfigKeeperClusterScopeReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + configMap, err = getConfigmap(ctx, configMapNamespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + Expect(err).NotTo(HaveOccurred()) + Expect(configMap.Finalizers).To(BeNil()) + Expect(configMap.Annotations).To(HaveKeyWithValue("argocd.argoproj.io/compare-options", "IgnoreExtraneous")) + }) + }) + + Context("Secret Tests", func() { + BeforeAll(func() { + manageNamespace(ctx, secretNamespace, "create") + manageSecrets(ctx, secretNamespace, "create") + manageReplicas(ctx, secretNamespace, "create", 1) + By("creating the custom resource for the Kind ArgoRolloutConfigKeeperClusterScope") + err := k8sClient.Get(ctx, typeNamespacedName, argoRolloutConfigKeeperClusterScope) + if err != nil && errors.IsNotFound(err) { + resource := &configkeeperv1alpha1.ArgoRolloutConfigKeeperClusterScope{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: configkeeperv1alpha1.ArgoRolloutConfigKeeperClusterScopeSpec{ + // Add spec details here + FinalizerName: finalizerName, + IgnoredNamespaces: []string{ + "kube-system", + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterAll(func() { + manageNamespace(ctx, secretNamespace, "delete") + manageSecrets(ctx, secretNamespace, "delete") + manageReplicas(ctx, secretNamespace, "delete", 0) + resource := &configkeeperv1alpha1.ArgoRolloutConfigKeeperClusterScope{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + By("Cleanup the specific resource instance ArgoRolloutConfigKeeperClusterScope") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ArgoRolloutConfigKeeperClusterScopeReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + }) + It("Secret should have the finalizer", func() { + By("Reconciling the created resource") + controllerReconciler := &ArgoRolloutConfigKeeperClusterScopeReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + secret, err := getSecret(ctx, secretNamespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + Expect(err).NotTo(HaveOccurred()) + Expect(secret.Finalizers).To(ContainElement(finalizerNameFullName)) + }) + It("Should remove the finalizer from the secret and add IgnoreExtraneous annotation", func() { + By("Updating the replicaset to 0") + manageReplicas(ctx, secretNamespace, "update", 0) + By("Reconciling the created resource") + var ( + err error + secret *corev1.Secret + ) + + secret, err = getSecret(ctx, secretNamespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + + controllerReconciler := &ArgoRolloutConfigKeeperClusterScopeReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + secret, err = getSecret(ctx, secretNamespace, fmt.Sprintf("%s-%s-%s", chartName, applicationName, appVersion)) + Expect(err).NotTo(HaveOccurred()) + Expect(secret.Finalizers).To(BeNil()) + Expect(secret.Annotations).To(HaveKeyWithValue("argocd.argoproj.io/compare-options", "IgnoreExtraneous")) + }) + }) +}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index dc806c8..e945ba8 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -25,13 +25,15 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - keeperv1alpha1 "github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1" "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" + + configkeeperv1alpha1 "github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1" + keeperv1alpha1 "github.com/run-ai/argo-rollout-config-keeper/api/v1alpha1" //+kubebuilder:scaffold:imports ) @@ -74,6 +76,9 @@ var _ = BeforeSuite(func() { err = keeperv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = configkeeperv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) diff --git a/internal/tools.go b/internal/tools.go index 75b75bf..63ed235 100644 --- a/internal/tools.go +++ b/internal/tools.go @@ -22,3 +22,11 @@ func RemoveString(slice []string, s string) []string { } return result } + +func CreateMapFromStringList(list []string) map[string]bool { + result := make(map[string]bool) + for _, item := range list { + result[item] = true + } + return result +}