From 201f1e0400794eee39c397251b85e25605d55e52 Mon Sep 17 00:00:00 2001 From: Saman Mahdanian Date: Tue, 28 May 2024 15:21:09 +0330 Subject: [PATCH 1/2] changed cache structure and added support for comma separation --- controllers/namespace_controller.go | 233 ++++++++++++++++++---------- go.mod | 6 +- go.sum | 2 + 3 files changed, 161 insertions(+), 80 deletions(-) diff --git a/controllers/namespace_controller.go b/controllers/namespace_controller.go index a84b080..2e4250d 100644 --- a/controllers/namespace_controller.go +++ b/controllers/namespace_controller.go @@ -26,6 +26,7 @@ import ( argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/go-logr/logr" + "github.com/hashicorp/go-multierror" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -40,34 +41,90 @@ import ( type NamespaceReconciler struct { client.Client Scheme *runtime.Scheme + mu sync.Mutex } const ( - teamLabel = "argocd.snappcloud.io/appproj" - baseNs = "user-argocd" + projectsLabel = "argocd.snappcloud.io/appproj" + baseNs = "user-argocd" ) -var safeNsCache = &SafeNsCache{m: map[string]string{}} +var safeNsCache = &SafeNsCache{initialized: false} +type Nameset map[string]struct{} + +type AppProjectNameset Nameset +type NamespaceNameset Nameset type SafeNsCache struct { - mu sync.Mutex - m map[string]string + mu sync.Mutex + projects map[string]AppProjectNameset + namespaces map[string]NamespaceNameset + initialized bool } -// Inc increments the counter for the given key. -func (c *SafeNsCache) Set(k, v string) { +// LeaveProject will add given namespace from given project in SafeNsCache entries +// It will update both upward and downward edges in AppProjectNameset and NamespaceNameset +func (c *SafeNsCache) JoinProject(ns, proj string) { c.mu.Lock() defer c.mu.Unlock() - // Lock so only one goroutine at a time can access the map c.m. - c.m[k] = v + c.projects[ns][proj] = struct{}{} + c.namespaces[proj][ns] = struct{}{} +} +// LeaveProject will remove given namespace from given project in SafeNsCache entries +// It will update both upward and downward edges in AppProjectNameset and NamespaceNameset +func (c *SafeNsCache) LeaveProject(ns, proj string) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.projects[ns], proj) + delete(c.namespaces[proj], ns) } -func (c *SafeNsCache) Load(k string) (v string, ok bool) { +// GetProjects will return name-set for given Namespace name +func (c *SafeNsCache) GetProjects(ns string) AppProjectNameset { c.mu.Lock() defer c.mu.Unlock() - v, ok = c.m[k] - return v, ok + r := make(AppProjectNameset) + for k := range c.projects[ns] { + r[k] = struct{}{} + } + return r +} + +// GetNamespaces will return name-set for given AppProject name +func (c *SafeNsCache) GetNamespaces(proj string) NamespaceNameset { + c.mu.Lock() + defer c.mu.Unlock() + r := make(NamespaceNameset) + for k := range c.namespaces[proj] { + r[k] = struct{}{} + } + return r +} + +func (c *SafeNsCache) InitOrPass(r *NamespaceReconciler, ctx context.Context) error { + if c.initialized { + return nil + } + + nsList := &corev1.NamespaceList{} + err := r.List(ctx, nsList) + if err != nil { + return err + } + + c.namespaces = make(map[string]NamespaceNameset) + c.projects = make(map[string]AppProjectNameset) + + for _, nsItem := range nsList.Items { + projects := convertLabelToAppProjectNameset( + nsItem.GetLabels()[projectsLabel], + ) + for project := range projects { + c.JoinProject(nsItem.Name, project) + } + } + return nil } //+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch;delete @@ -85,83 +142,93 @@ func (c *SafeNsCache) Load(k string) (v string, ok bool) { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.mu.Lock() + defer r.mu.Unlock() + logger := log.FromContext(ctx) - logger.Info(fmt.Sprint(req.NamespacedName)) + logger.Info("Reconciling Namespace: ", fmt.Sprint(req.NamespacedName)) + + err := safeNsCache.InitOrPass(r, ctx) + if err != nil { + return ctrl.Result{}, err + } ns := &corev1.Namespace{} - err := r.Get(ctx, req.NamespacedName, ns) + // First Fetch Phase + err = r.Get(ctx, req.NamespacedName, ns) if err != nil { if errors.IsNotFound(err) { - - oldTeam, _ := safeNsCache.Load(req.Name) - fmt.Println("oldteam", oldTeam) - if oldTeam != "" { - err = r.reconcileAppProject(ctx, logger, oldTeam) - if err != nil { - return ctrl.Result{}, err + logger.Info("Namespace not found. Ignoring since object must be deleted") + + oldTeams := safeNsCache.GetProjects(req.Name) + if len(oldTeams) > 0 { + for t := range oldTeams { + err := r.reconcileAppProject(ctx, logger, t) + if err != nil { + logger.Info("Failed to reconcile AppProject [", t, "] for not found resource error recovery: ", err.Error()) + } else { + logger.Info("Successfully reconciled AppProject [", t, "] for not found resource error recovery") + } } } // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue - logger.Info("Resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. - logger.Error(err, "Failed to get Namespace") + logger.Error(err, "Failed to get Namespace Resource, Requeuing the request") return ctrl.Result{}, err } - team := ns.GetLabels()[teamLabel] - oldTeam, _ := safeNsCache.Load(req.Name) - fmt.Println("oldteam", oldTeam) - safeNsCache.Set(req.Name, team) - - if team == "snappcloud" { - return ctrl.Result{}, nil - - } - - if team == "" { - - // newly created ns, without any team - if oldTeam == "" { - return ctrl.Result{}, nil + projectsToAdd := convertLabelToAppProjectNameset( + ns.GetLabels()[projectsLabel], + ) + projectsToRemove := make([]string, 0) + oldProjects := safeNsCache.GetProjects(req.Name) + + for t := range oldProjects { + if _, ok := projectsToAdd[t]; !ok { + projectsToRemove = append(projectsToRemove, t) + logger.Info("Updating Cache: Removing NS:", req.Name, " to AppProject:", t) + safeNsCache.LeaveProject(req.Name, t) + } else { + delete(projectsToAdd, t) + logger.Info("Updating Cache: Adding NS:", req.Name, " to AppProject:", t) + safeNsCache.JoinProject(req.Name, t) } - - // unlabeled ns, but it had an old team - if oldTeam != "" { - err = r.reconcileAppProject(ctx, logger, oldTeam) - if err != nil { - return ctrl.Result{}, err - } - } - - } - - // add ns to new team - err = r.reconcileAppProject(ctx, logger, team) - if err != nil { - return ctrl.Result{}, err } - if oldTeam == "" || oldTeam == team { - return ctrl.Result{}, nil + var reconciliationErrors *multierror.Error + // add ns to new app-projects + logger.Info("Reconciling New Teams") + for t := range projectsToAdd { + logger.Info("Reconciling AppProject: ", t) + err = r.reconcileAppProject(ctx, logger, t) + if err != nil { + logger.Info("Error while Reconciling AppProject ", t, " : ", err.Error()) + reconciliationErrors = multierror.Append(reconciliationErrors, err) + } } - // also reconicle oldTeam to remove ns from oldTeam - err = r.reconcileAppProject(ctx, logger, oldTeam) - if err != nil { - return ctrl.Result{}, err + // removing ns from old projects + logger.Info("Reconciling Old Teams") + for _, t := range projectsToRemove { + logger.Info("Reconciling AppProject:", t) + err = r.reconcileAppProject(ctx, logger, t) + if err != nil { + logger.Info("Error while Reconciling AppProject ", t, " : ", err.Error()) + reconciliationErrors = multierror.Append(reconciliationErrors, err) + } } - return ctrl.Result{}, nil + return ctrl.Result{}, reconciliationErrors.ErrorOrNil() } func (r *NamespaceReconciler) reconcileAppProject(ctx context.Context, logger logr.Logger, team string) error { - appProj, err := r.createAppProj(ctx, team) + appProj, err := r.createAppProj(team) if err != nil { - return fmt.Errorf("Error generating AppProj manifest: %v", err) + return fmt.Errorf("error generating AppProj manifest: %v", err) } // Check if AppProj does not exist and create a new one @@ -171,11 +238,11 @@ func (r *NamespaceReconciler) reconcileAppProject(ctx context.Context, logger lo logger.Info("Creating AppProj", "AppProj.Name", team) err = r.Create(ctx, appProj) if err != nil { - return fmt.Errorf("Error creating AppProj: %v", err) + return fmt.Errorf("error creating AppProj: %v", err) } return nil } else if err != nil { - return fmt.Errorf("Error getting AppProj: %v", err) + return fmt.Errorf("error getting AppProj: %v", err) } // If AppProj already exist, check if it is deeply equal with desrired state @@ -185,30 +252,31 @@ func (r *NamespaceReconciler) reconcileAppProject(ctx context.Context, logger lo found.Spec = appProj.Spec err := r.Update(ctx, found) if err != nil { - return fmt.Errorf("Error updating AppProj: %v", err) + return fmt.Errorf("error updating AppProj: %v", err) } } return nil } -func (r *NamespaceReconciler) createAppProj(ctx context.Context, team string) (*argov1alpha1.AppProject, error) { +func (r *NamespaceReconciler) createAppProj(team string) (*argov1alpha1.AppProject, error) { fmt.Println("run reconcile on ", team) - listOpts := []client.ListOption{ - client.MatchingLabels(map[string]string{ - teamLabel: team, - }), - } - nsList := &corev1.NamespaceList{} - err := r.List(ctx, nsList, listOpts...) - if err != nil { - return nil, err - } + // listOpts := []client.ListOption{ + // client.MatchingLabels(map[string]string{ + // teamLabel: team, + // }), + // } + // nsList := &corev1.NamespaceList{} + // err := r.List(ctx, nsList, listOpts...) + // if err != nil { + // return nil, err + // } + desiredNamespaces := safeNsCache.GetNamespaces(team) destList := []argov1alpha1.ApplicationDestination{} - for _, nsItem := range nsList.Items { + for nsItem := range desiredNamespaces { destList = append(destList, argov1alpha1.ApplicationDestination{ - Namespace: nsItem.Name, + Namespace: nsItem, Server: "https://kubernetes.default.svc", }) @@ -283,3 +351,12 @@ func appendRepos(repo_list []string, found_repos []string) []string { return res } + +// ConvertLabelToAppProjectNameset will convert comma separated label value to actual nameset +func convertLabelToAppProjectNameset(l string) AppProjectNameset { + result := make(AppProjectNameset) + for _, s := range strings.Split(l, ",") { + result[s] = struct{}{} + } + return result +} diff --git a/go.mod b/go.mod index af64041..7eb80c4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.17 require ( github.com/argoproj/argo-cd/v2 v2.0.5 github.com/go-logr/logr v0.4.0 + github.com/hashicorp/go-multierror v1.0.0 github.com/onsi/ginkgo v1.16.4 github.com/onsi/gomega v1.13.0 github.com/openshift/api v3.9.0+incompatible @@ -13,7 +14,6 @@ require ( k8s.io/apimachinery v0.21.4 k8s.io/client-go v11.0.1-0.20190816222228-6d55c1b1f1ca+incompatible sigs.k8s.io/controller-runtime v0.9.2 - sigs.k8s.io/yaml v1.2.0 ) require ( @@ -69,6 +69,7 @@ require ( github.com/google/uuid v1.1.2 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -151,6 +152,7 @@ require ( sigs.k8s.io/kustomize/api v0.8.8 // indirect sigs.k8s.io/kustomize/kyaml v0.10.17 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect ) replace ( @@ -181,4 +183,4 @@ replace ( k8s.io/mount-utils => k8s.io/mount-utils v0.21.4 k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.22.0 k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.21.4 -) \ No newline at end of file +) diff --git a/go.sum b/go.sum index d513348..7366a8f 100644 --- a/go.sum +++ b/go.sum @@ -409,10 +409,12 @@ github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4G github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= From 52eba7a1f250f2515355b8087732957fb85a8bcd Mon Sep 17 00:00:00 2001 From: Saman Mahdanian Date: Wed, 29 May 2024 12:14:02 +0330 Subject: [PATCH 2/2] fixed typo in comment --- controllers/namespace_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/namespace_controller.go b/controllers/namespace_controller.go index 2e4250d..f0cd672 100644 --- a/controllers/namespace_controller.go +++ b/controllers/namespace_controller.go @@ -62,7 +62,7 @@ type SafeNsCache struct { initialized bool } -// LeaveProject will add given namespace from given project in SafeNsCache entries +// JoinProject will remove given namespace from given project in SafeNsCache entries // It will update both upward and downward edges in AppProjectNameset and NamespaceNameset func (c *SafeNsCache) JoinProject(ns, proj string) { c.mu.Lock()