github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocd/applicationset.go (about) 1 // Copyright 2021 ArgoCD Operator Developers 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package argocd 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "os" 22 "reflect" 23 "strings" 24 25 corev1 "k8s.io/api/core/v1" 26 v1 "k8s.io/api/rbac/v1" 27 apierrors "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/types" 30 amerr "k8s.io/apimachinery/pkg/util/errors" 31 "k8s.io/apimachinery/pkg/util/intstr" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 34 35 argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" 36 "github.com/argoproj-labs/argocd-operator/common" 37 "github.com/argoproj-labs/argocd-operator/controllers/argoutil" 38 ) 39 40 const ( 41 ApplicationSetGitlabSCMTlsCertPath = "/app/tls/scm/cert" 42 ) 43 44 // getArgoApplicationSetCommand will return the command for the ArgoCD ApplicationSet component. 45 func (r *ReconcileArgoCD) getArgoApplicationSetCommand(cr *argoproj.ArgoCD) []string { 46 cmd := make([]string, 0) 47 48 cmd = append(cmd, "entrypoint.sh") 49 cmd = append(cmd, "argocd-applicationset-controller") 50 51 if cr.Spec.Repo.IsEnabled() { 52 cmd = append(cmd, "--argocd-repo-server", getRepoServerAddress(cr)) 53 } else { 54 log.Info("Repo Server is disabled. This would affect the functioning of ApplicationSet Controller.") 55 } 56 57 cmd = append(cmd, "--loglevel") 58 cmd = append(cmd, getLogLevel(cr.Spec.ApplicationSet.LogLevel)) 59 60 if cr.Spec.ApplicationSet.SCMRootCAConfigMap != "" { 61 cmd = append(cmd, "--scm-root-ca-path") 62 cmd = append(cmd, ApplicationSetGitlabSCMTlsCertPath) 63 } 64 65 // appset source namespaces should be subset of apps source namespaces 66 appsetsSourceNamespaces := []string{} 67 appsNamespaces, err := r.getSourceNamespaces(cr) 68 if err == nil { 69 for _, ns := range cr.Spec.ApplicationSet.SourceNamespaces { 70 if contains(appsNamespaces, ns) { 71 appsetsSourceNamespaces = append(appsetsSourceNamespaces, ns) 72 } else { 73 log.V(1).Info(fmt.Sprintf("Apps in target sourceNamespace %s is not enabled, thus skipping the namespace in deployment command.", ns)) 74 } 75 } 76 } 77 78 if len(appsetsSourceNamespaces) > 0 { 79 cmd = append(cmd, "--applicationset-namespaces", fmt.Sprint(strings.Join(appsetsSourceNamespaces, ","))) 80 } 81 82 if len(cr.Spec.ApplicationSet.SCMProviders) > 0 { 83 cmd = append(cmd, "--allowed-scm-providers", fmt.Sprint(strings.Join(cr.Spec.ApplicationSet.SCMProviders, ","))) 84 } 85 86 // appset in any ns is enabled and no scmProviders allow list is specified, 87 // disables scm & PR generators to prevent potential security issues 88 // https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Appset-Any-Namespace/#scm-providers-secrets-consideration 89 if len(appsetsSourceNamespaces) > 0 && !(len(cr.Spec.ApplicationSet.SCMProviders) > 0) { 90 cmd = append(cmd, "--enable-scm-providers=false") 91 } 92 93 // ApplicationSet command arguments provided by the user 94 extraArgs := cr.Spec.ApplicationSet.ExtraCommandArgs 95 err = isMergable(extraArgs, cmd) 96 if err != nil { 97 return cmd 98 } 99 100 cmd = append(cmd, extraArgs...) 101 102 return cmd 103 } 104 105 func (r *ReconcileArgoCD) reconcileApplicationSetController(cr *argoproj.ArgoCD) error { 106 107 log.Info("reconciling applicationset serviceaccounts") 108 sa, err := r.reconcileApplicationSetServiceAccount(cr) 109 if err != nil { 110 return err 111 } 112 113 log.Info("reconciling applicationset roles") 114 role, err := r.reconcileApplicationSetRole(cr) 115 if err != nil { 116 return err 117 } 118 119 log.Info("reconciling applicationset role bindings") 120 if err := r.reconcileApplicationSetRoleBinding(cr, role, sa); err != nil { 121 return err 122 } 123 124 log.Info("reconciling applicationset deployments") 125 if err := r.reconcileApplicationSetDeployment(cr, sa); err != nil { 126 return err 127 } 128 129 log.Info("reconciling applicationset service") 130 if err := r.reconcileApplicationSetService(cr); err != nil { 131 return err 132 } 133 134 // create clusterrole & clusterrolebinding if cluster-scoped ArgoCD 135 log.Info("reconciling applicationset clusterroles") 136 clusterrole, err := r.reconcileApplicationSetClusterRole(cr) 137 if err != nil { 138 return err 139 } 140 141 log.Info("reconciling applicationset clusterrolebindings") 142 if err := r.reconcileApplicationSetClusterRoleBinding(cr, clusterrole, sa); err != nil { 143 return err 144 } 145 146 // reconcile source namespace roles & rolebindings 147 log.Info("reconciling applicationset roles & rolebindings in source namespaces") 148 if err := r.reconcileApplicationSetSourceNamespacesResources(cr); err != nil { 149 return err 150 } 151 152 // remove resources for namespaces not part of SourceNamespaces 153 log.Info("performing cleanup for applicationset source namespaces") 154 if err := r.removeUnmanagedApplicationSetSourceNamespaceResources(cr); err != nil { 155 return err 156 } 157 158 return nil 159 } 160 161 // reconcileApplicationControllerDeployment will ensure the Deployment resource is present for the ArgoCD Application Controller component. 162 func (r *ReconcileArgoCD) reconcileApplicationSetDeployment(cr *argoproj.ArgoCD, sa *corev1.ServiceAccount) error { 163 164 exists := false 165 existing := newDeploymentWithSuffix("applicationset-controller", "controller", cr) 166 if argoutil.IsObjectFound(r.Client, cr.Namespace, existing.Name, existing) { 167 exists = true 168 } 169 if cr.Spec.ApplicationSet == nil || !cr.Spec.ApplicationSet.IsEnabled() { 170 if exists { 171 return r.Client.Delete(context.TODO(), existing) 172 } 173 return nil 174 } 175 176 deploy := newDeploymentWithSuffix("applicationset-controller", "controller", cr) 177 178 setAppSetLabels(&deploy.ObjectMeta) 179 180 podSpec := &deploy.Spec.Template.Spec 181 182 // sa would be nil when spec.applicationset.enabled = false 183 if sa != nil { 184 podSpec.ServiceAccountName = sa.ObjectMeta.Name 185 } 186 podSpec.Volumes = []corev1.Volume{ 187 { 188 Name: "ssh-known-hosts", 189 VolumeSource: corev1.VolumeSource{ 190 ConfigMap: &corev1.ConfigMapVolumeSource{ 191 LocalObjectReference: corev1.LocalObjectReference{ 192 Name: common.ArgoCDKnownHostsConfigMapName, 193 }, 194 }, 195 }, 196 }, 197 { 198 Name: "tls-certs", 199 VolumeSource: corev1.VolumeSource{ 200 ConfigMap: &corev1.ConfigMapVolumeSource{ 201 LocalObjectReference: corev1.LocalObjectReference{ 202 Name: common.ArgoCDTLSCertsConfigMapName, 203 }, 204 }, 205 }, 206 }, 207 { 208 Name: "gpg-keys", 209 VolumeSource: corev1.VolumeSource{ 210 ConfigMap: &corev1.ConfigMapVolumeSource{ 211 LocalObjectReference: corev1.LocalObjectReference{ 212 Name: common.ArgoCDGPGKeysConfigMapName, 213 }, 214 }, 215 }, 216 }, 217 { 218 Name: "gpg-keyring", 219 VolumeSource: corev1.VolumeSource{ 220 EmptyDir: &corev1.EmptyDirVolumeSource{}, 221 }, 222 }, 223 { 224 Name: "tmp", 225 VolumeSource: corev1.VolumeSource{ 226 EmptyDir: &corev1.EmptyDirVolumeSource{}, 227 }, 228 }, 229 } 230 addSCMGitlabVolumeMount := false 231 if scmRootCAConfigMapName := getSCMRootCAConfigMapName(cr); scmRootCAConfigMapName != "" { 232 cm := newConfigMapWithName(scmRootCAConfigMapName, cr) 233 if argoutil.IsObjectFound(r.Client, cr.Namespace, cr.Spec.ApplicationSet.SCMRootCAConfigMap, cm) { 234 addSCMGitlabVolumeMount = true 235 podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ 236 Name: "appset-gitlab-scm-tls-cert", 237 VolumeSource: corev1.VolumeSource{ 238 ConfigMap: &corev1.ConfigMapVolumeSource{ 239 LocalObjectReference: corev1.LocalObjectReference{ 240 Name: common.ArgoCDAppSetGitlabSCMTLSCertsConfigMapName, 241 }, 242 }, 243 }, 244 }) 245 } 246 } 247 248 podSpec.Containers = []corev1.Container{ 249 r.applicationSetContainer(cr, addSCMGitlabVolumeMount), 250 } 251 AddSeccompProfileForOpenShift(r.Client, podSpec) 252 253 if exists { 254 255 existingSpec := existing.Spec.Template.Spec 256 257 deploymentsDifferent := !reflect.DeepEqual(existingSpec.Containers[0], podSpec.Containers) || 258 !reflect.DeepEqual(existingSpec.Volumes, podSpec.Volumes) || 259 existingSpec.ServiceAccountName != podSpec.ServiceAccountName || 260 !reflect.DeepEqual(existing.Labels, deploy.Labels) || 261 !reflect.DeepEqual(existing.Spec.Template.Labels, deploy.Spec.Template.Labels) || 262 !reflect.DeepEqual(existing.Spec.Selector, deploy.Spec.Selector) || 263 !reflect.DeepEqual(existing.Spec.Template.Spec.NodeSelector, deploy.Spec.Template.Spec.NodeSelector) || 264 !reflect.DeepEqual(existing.Spec.Template.Spec.Tolerations, deploy.Spec.Template.Spec.Tolerations) 265 266 // If the Deployment already exists, make sure the values we care about are up-to-date 267 if deploymentsDifferent { 268 existing.Spec.Template.Spec.Containers = podSpec.Containers 269 existing.Spec.Template.Spec.Volumes = podSpec.Volumes 270 existing.Spec.Template.Spec.ServiceAccountName = podSpec.ServiceAccountName 271 existing.Labels = deploy.Labels 272 existing.Spec.Template.Labels = deploy.Spec.Template.Labels 273 existing.Spec.Selector = deploy.Spec.Selector 274 existing.Spec.Template.Spec.NodeSelector = deploy.Spec.Template.Spec.NodeSelector 275 existing.Spec.Template.Spec.Tolerations = deploy.Spec.Template.Spec.Tolerations 276 return r.Client.Update(context.TODO(), existing) 277 } 278 return nil // Deployment found with nothing to do, move along... 279 } 280 281 if !cr.Spec.ApplicationSet.IsEnabled() { 282 return nil 283 } 284 285 if err := controllerutil.SetControllerReference(cr, deploy, r.Scheme); err != nil { 286 return err 287 } 288 return r.Client.Create(context.TODO(), deploy) 289 290 } 291 292 func (r *ReconcileArgoCD) applicationSetContainer(cr *argoproj.ArgoCD, addSCMGitlabVolumeMount bool) corev1.Container { 293 // Global proxy env vars go first 294 appSetEnv := []corev1.EnvVar{{ 295 Name: "NAMESPACE", 296 ValueFrom: &corev1.EnvVarSource{ 297 FieldRef: &corev1.ObjectFieldSelector{ 298 FieldPath: "metadata.namespace", 299 }, 300 }, 301 }} 302 303 // Merge ApplicationSet env vars provided by the user 304 // User should be able to override the default NAMESPACE environmental variable 305 appSetEnv = argoutil.EnvMerge(cr.Spec.ApplicationSet.Env, appSetEnv, true) 306 // Environment specified in the CR take precedence over everything else 307 appSetEnv = argoutil.EnvMerge(appSetEnv, proxyEnvVars(), false) 308 309 container := corev1.Container{ 310 Command: r.getArgoApplicationSetCommand(cr), 311 Env: appSetEnv, 312 Image: getApplicationSetContainerImage(cr), 313 ImagePullPolicy: corev1.PullAlways, 314 Name: "argocd-applicationset-controller", 315 Resources: getApplicationSetResources(cr), 316 VolumeMounts: []corev1.VolumeMount{ 317 { 318 Name: "ssh-known-hosts", 319 MountPath: "/app/config/ssh", 320 }, 321 { 322 Name: "tls-certs", 323 MountPath: "/app/config/tls", 324 }, 325 { 326 Name: "gpg-keys", 327 MountPath: "/app/config/gpg/source", 328 }, 329 { 330 Name: "gpg-keyring", 331 MountPath: "/app/config/gpg/keys", 332 }, 333 { 334 Name: "tmp", 335 MountPath: "/tmp", 336 }, 337 }, 338 Ports: []corev1.ContainerPort{ 339 { 340 ContainerPort: 7000, 341 Name: "webhook", 342 }, 343 { 344 ContainerPort: 8080, 345 Name: "metrics", 346 }, 347 }, 348 SecurityContext: &corev1.SecurityContext{ 349 Capabilities: &corev1.Capabilities{ 350 Drop: []corev1.Capability{ 351 "ALL", 352 }, 353 }, 354 AllowPrivilegeEscalation: boolPtr(false), 355 ReadOnlyRootFilesystem: boolPtr(true), 356 RunAsNonRoot: boolPtr(true), 357 }, 358 } 359 if addSCMGitlabVolumeMount { 360 container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ 361 Name: "appset-gitlab-scm-tls-cert", 362 MountPath: ApplicationSetGitlabSCMTlsCertPath, 363 }) 364 } 365 return container 366 } 367 368 func (r *ReconcileArgoCD) reconcileApplicationSetServiceAccount(cr *argoproj.ArgoCD) (*corev1.ServiceAccount, error) { 369 370 sa := newServiceAccountWithName("applicationset-controller", cr) 371 setAppSetLabels(&sa.ObjectMeta) 372 373 exists := true 374 if err := argoutil.FetchObject(r.Client, cr.Namespace, sa.Name, sa); err != nil { 375 if !apierrors.IsNotFound(err) { 376 return sa, err 377 } 378 exists = false 379 } 380 381 if cr.Spec.ApplicationSet == nil || !cr.Spec.ApplicationSet.IsEnabled() { 382 if exists { 383 err := r.Client.Delete(context.TODO(), sa) 384 if err != nil { 385 if !apierrors.IsNotFound(err) { 386 return sa, err 387 } 388 } 389 } 390 return sa, nil 391 } 392 393 if !exists { 394 if err := controllerutil.SetControllerReference(cr, sa, r.Scheme); err != nil { 395 return sa, err 396 } 397 398 err := r.Client.Create(context.TODO(), sa) 399 if err != nil { 400 return sa, err 401 } 402 } 403 404 return sa, nil 405 } 406 407 // reconcileApplicationSetClusterRoleBinding reconciles required clusterrole for appset controller when ArgoCD is cluster-scoped 408 func (r *ReconcileArgoCD) reconcileApplicationSetClusterRole(cr *argoproj.ArgoCD) (*v1.ClusterRole, error) { 409 410 allowed := false 411 if allowedNamespace(cr.Namespace, os.Getenv("ARGOCD_CLUSTER_CONFIG_NAMESPACES")) { 412 allowed = true 413 } 414 415 // controller disabled, don't create resources 416 if cr.Spec.ApplicationSet == nil || !cr.Spec.ApplicationSet.IsEnabled() { 417 allowed = false 418 } 419 420 policyRules := []v1.PolicyRule{ 421 // ApplicationSet 422 { 423 APIGroups: []string{"argoproj.io"}, 424 Resources: []string{ 425 "applications", 426 "applicationsets", 427 }, 428 Verbs: []string{ 429 "list", 430 "watch", 431 }, 432 }, 433 // Secrets 434 { 435 APIGroups: []string{""}, 436 Resources: []string{ 437 "secrets", 438 }, 439 Verbs: []string{ 440 "list", 441 "watch", 442 }, 443 }, 444 } 445 446 clusterRole := newClusterRole(common.ArgoCDApplicationSetControllerComponent, policyRules, cr) 447 if err := applyReconcilerHook(cr, clusterRole, ""); err != nil { 448 return nil, err 449 } 450 451 existingClusterRole := &v1.ClusterRole{} 452 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: clusterRole.Name}, existingClusterRole) 453 if err != nil { 454 if !apierrors.IsNotFound(err) { 455 return nil, fmt.Errorf("failed to reconcile the cluster role for the service account associated with %s : %s", clusterRole.Name, err) 456 } 457 if !allowed { 458 // Do Nothing 459 return clusterRole, nil 460 } 461 return clusterRole, r.Client.Create(context.TODO(), clusterRole) 462 } 463 464 // ArgoCD not cluster scoped, cleanup any existing resource and exit 465 if !allowed { 466 err := r.Client.Delete(context.TODO(), existingClusterRole) 467 if err != nil { 468 if !apierrors.IsNotFound(err) { 469 return existingClusterRole, err 470 } 471 } 472 return existingClusterRole, nil 473 } 474 475 // if the Rules differ, update the Role 476 if !reflect.DeepEqual(existingClusterRole.Rules, clusterRole.Rules) { 477 existingClusterRole.Rules = clusterRole.Rules 478 if err := r.Client.Update(context.TODO(), existingClusterRole); err != nil { 479 return nil, err 480 } 481 } 482 return existingClusterRole, nil 483 } 484 485 // reconcileApplicationSetClusterRoleBinding reconciles required clusterrolebinding for appset controller when ArgoCD is cluster-scoped 486 func (r *ReconcileArgoCD) reconcileApplicationSetClusterRoleBinding(cr *argoproj.ArgoCD, role *v1.ClusterRole, sa *corev1.ServiceAccount) error { 487 488 allowed := false 489 if allowedNamespace(cr.Namespace, os.Getenv("ARGOCD_CLUSTER_CONFIG_NAMESPACES")) { 490 allowed = true 491 } 492 493 // controller disabled, don't create resources 494 if cr.Spec.ApplicationSet == nil || !cr.Spec.ApplicationSet.IsEnabled() { 495 allowed = false 496 } 497 498 clusterRB := newClusterRoleBindingWithname(common.ArgoCDApplicationSetControllerComponent, cr) 499 clusterRB.Subjects = []v1.Subject{ 500 { 501 Kind: v1.ServiceAccountKind, 502 Name: sa.Name, 503 Namespace: cr.Namespace, 504 }, 505 } 506 clusterRB.RoleRef = v1.RoleRef{ 507 APIGroup: v1.GroupName, 508 Kind: "ClusterRole", 509 Name: role.Name, 510 } 511 if err := applyReconcilerHook(cr, clusterRB, ""); err != nil { 512 return err 513 } 514 515 existingClusterRB := &v1.ClusterRoleBinding{} 516 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: clusterRB.Name}, existingClusterRB) 517 if err != nil { 518 if !apierrors.IsNotFound(err) { 519 return fmt.Errorf("failed to reconcile the cluster rolebinding for the service account associated with %s : %s", clusterRB.Name, err) 520 } 521 if !allowed { 522 // Do Nothing 523 return nil 524 } 525 return r.Client.Create(context.TODO(), clusterRB) 526 } 527 528 // ArgoCD not cluster scoped, cleanup any existing resource and exit 529 if !allowed { 530 err := r.Client.Delete(context.TODO(), existingClusterRB) 531 if err != nil { 532 if !apierrors.IsNotFound(err) { 533 return err 534 } 535 } 536 return nil 537 } 538 539 // if subj differ, update the rolebinding 540 if !reflect.DeepEqual(existingClusterRB.Subjects, clusterRB.Subjects) { 541 existingClusterRB.Subjects = clusterRB.Subjects 542 if err := r.Client.Update(context.TODO(), existingClusterRB); err != nil { 543 return err 544 } 545 } else if !reflect.DeepEqual(existingClusterRB.RoleRef, clusterRB.RoleRef) { 546 // RoleRef can't be updated, delete the rolebinding so that it gets recreated 547 _ = r.Client.Delete(context.TODO(), existingClusterRB) 548 return fmt.Errorf("change detected in roleRef for rolebinding %s of Argo CD instance %s in namespace %s", existingClusterRB.Name, cr.Name, existingClusterRB.Namespace) 549 } 550 return nil 551 } 552 553 // reconcileApplicationSetSourceNamespacesResources creates role & rolebinding in target source namespaces for appset controller 554 // Appset resources are only created if target source ns is subset of apps source namespaces 555 func (r *ReconcileArgoCD) reconcileApplicationSetSourceNamespacesResources(cr *argoproj.ArgoCD) error { 556 557 var reconciliationErrors []error 558 559 // controller disabled, nothing to do. cleanup handled by removeUnmanagedApplicationSetSourceNamespaceResources() 560 if cr.Spec.ApplicationSet == nil { 561 return nil 562 } 563 564 // create resources for each appset source namespace 565 for _, sourceNamespace := range cr.Spec.ApplicationSet.SourceNamespaces { 566 567 // source ns should be part of app-in-any-ns 568 appsNamespaces, err := r.getSourceNamespaces(cr) 569 if err != nil { 570 reconciliationErrors = append(reconciliationErrors, err) 571 continue 572 } 573 if !contains(appsNamespaces, sourceNamespace) { 574 log.Error(fmt.Errorf("skipping reconciliation of resources for sourceNamespace %s as Apps in target sourceNamespace is not enabled", sourceNamespace), "Warning") 575 continue 576 } 577 578 // skip source ns if doesn't exist 579 namespace := &corev1.Namespace{} 580 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: sourceNamespace}, namespace); err != nil { 581 errMsg := fmt.Errorf("failed to retrieve namespace %s", sourceNamespace) 582 reconciliationErrors = append(reconciliationErrors, errors.Join(errMsg, err)) 583 continue 584 } 585 586 // No namespace can be managed by multiple argo-cd instances (cluster scoped or namespace scoped) 587 // i.e, only one of either managed-by or applicationset-managed-by-cluster-argocd labels can be applied to a given namespace. 588 // Since appset-in-any-ns is in beta, we prioritize managed-by label in case of a conflict. 589 if value, ok := namespace.Labels[common.ArgoCDManagedByLabel]; ok && value != "" { 590 log.Info(fmt.Sprintf("Skipping reconciling resources for namespace %s as it is already managed-by namespace %s.", namespace.Name, value)) 591 // remove any source namespace resources 592 if val, ok1 := namespace.Labels[common.ArgoCDApplicationSetManagedByClusterArgoCDLabel]; ok1 && val != cr.Namespace { 593 delete(r.ManagedApplicationSetSourceNamespaces, namespace.Name) 594 if err := r.cleanupUnmanagedApplicationSetSourceNamespaceResources(cr, namespace.Name); err != nil { 595 log.Error(err, fmt.Sprintf("error cleaning up resources for namespace %s", namespace.Name)) 596 } 597 } 598 continue 599 } 600 601 log.Info(fmt.Sprintf("Reconciling applicationset resources for %s", namespace.Name)) 602 // add applicationset-managed-by-cluster-argocd label on namespace 603 if _, ok := namespace.Labels[common.ArgoCDApplicationSetManagedByClusterArgoCDLabel]; !ok { 604 // Get the latest value of namespace before updating it 605 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace.Name}, namespace); err != nil { 606 return err 607 } 608 // Update namespace with applicationset-managed-by-cluster-argocd label 609 if namespace.Labels == nil { 610 namespace.Labels = make(map[string]string) 611 } 612 namespace.Labels[common.ArgoCDApplicationSetManagedByClusterArgoCDLabel] = cr.Namespace 613 if err := r.Client.Update(context.TODO(), namespace); err != nil { 614 log.Error(err, fmt.Sprintf("failed to add label from namespace [%s]", namespace.Name)) 615 } 616 } 617 618 // role & rolebinding for applicationset controller in source namespace 619 role := v1.Role{ 620 ObjectMeta: metav1.ObjectMeta{ 621 Name: getResourceNameForApplicationSetSourceNamespaces(cr), 622 Namespace: sourceNamespace, 623 Labels: argoutil.LabelsForCluster(cr), 624 }, 625 Rules: policyRuleForApplicationSetController(), 626 } 627 err = r.reconcileSourceNamespaceRole(role, cr) 628 if err != nil { 629 reconciliationErrors = append(reconciliationErrors, err) 630 } 631 632 roleBinding := v1.RoleBinding{ 633 ObjectMeta: metav1.ObjectMeta{ 634 Name: getResourceNameForApplicationSetSourceNamespaces(cr), 635 Labels: argoutil.LabelsForCluster(cr), 636 Annotations: argoutil.AnnotationsForCluster(cr), 637 Namespace: sourceNamespace, 638 }, 639 RoleRef: v1.RoleRef{ 640 APIGroup: v1.GroupName, 641 Kind: "Role", 642 Name: getResourceNameForApplicationSetSourceNamespaces(cr), 643 }, 644 Subjects: []v1.Subject{ 645 { 646 Kind: v1.ServiceAccountKind, 647 Name: getServiceAccountName(cr.Name, "applicationset-controller"), 648 Namespace: cr.Namespace, 649 }, 650 }, 651 } 652 err = r.reconcileSourceNamespaceRoleBinding(roleBinding, cr) 653 if err != nil { 654 reconciliationErrors = append(reconciliationErrors, err) 655 } 656 657 // appset permissions for argocd server in source namespaces are handled by apps-in-any-ns code 658 659 if _, ok := r.ManagedApplicationSetSourceNamespaces[sourceNamespace]; !ok { 660 if r.ManagedApplicationSetSourceNamespaces == nil { 661 r.ManagedApplicationSetSourceNamespaces = make(map[string]string) 662 } 663 r.ManagedApplicationSetSourceNamespaces[sourceNamespace] = "" 664 } 665 } 666 667 return amerr.NewAggregate(reconciliationErrors) 668 } 669 670 func (r *ReconcileArgoCD) reconcileApplicationSetRole(cr *argoproj.ArgoCD) (*v1.Role, error) { 671 672 policyRules := policyRuleForApplicationSetController() 673 674 role := newRole("applicationset-controller", policyRules, cr) 675 setAppSetLabels(&role.ObjectMeta) 676 677 exists := true 678 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: role.Name, Namespace: cr.Namespace}, role) 679 if err != nil { 680 if !apierrors.IsNotFound(err) { 681 return role, err 682 } 683 exists = false 684 } 685 686 if cr.Spec.ApplicationSet == nil || !cr.Spec.ApplicationSet.IsEnabled() { 687 if exists { 688 if err := r.Client.Delete(context.TODO(), role); err != nil { 689 if !apierrors.IsNotFound(err) { 690 return role, err 691 } 692 } 693 } 694 return role, nil 695 } 696 697 role.Rules = policyRules 698 if err = controllerutil.SetControllerReference(cr, role, r.Scheme); err != nil { 699 return role, err 700 } 701 if exists { 702 return role, r.Client.Update(context.TODO(), role) 703 } else { 704 return role, r.Client.Create(context.TODO(), role) 705 } 706 707 } 708 709 func (r *ReconcileArgoCD) reconcileApplicationSetRoleBinding(cr *argoproj.ArgoCD, role *v1.Role, sa *corev1.ServiceAccount) error { 710 711 name := "applicationset-controller" 712 713 // get expected name 714 roleBinding := newRoleBindingWithname(name, cr) 715 716 // fetch existing rolebinding by name 717 roleBindingExists := true 718 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: roleBinding.Name, Namespace: cr.Namespace}, roleBinding); err != nil { 719 if !apierrors.IsNotFound(err) { 720 return fmt.Errorf("failed to get the rolebinding associated with %s : %s", name, err) 721 } 722 roleBindingExists = false 723 } 724 725 if cr.Spec.ApplicationSet == nil || !cr.Spec.ApplicationSet.IsEnabled() { 726 if roleBindingExists { 727 return r.Client.Delete(context.TODO(), roleBinding) 728 } 729 return nil 730 } 731 732 setAppSetLabels(&roleBinding.ObjectMeta) 733 734 roleBinding.RoleRef = v1.RoleRef{ 735 APIGroup: v1.GroupName, 736 Kind: "Role", 737 Name: role.Name, 738 } 739 740 roleBinding.Subjects = []v1.Subject{ 741 { 742 Kind: v1.ServiceAccountKind, 743 Name: sa.Name, 744 Namespace: sa.Namespace, 745 }, 746 } 747 748 if err := controllerutil.SetControllerReference(cr, roleBinding, r.Scheme); err != nil { 749 return err 750 } 751 752 if roleBindingExists { 753 return r.Client.Update(context.TODO(), roleBinding) 754 } 755 756 return r.Client.Create(context.TODO(), roleBinding) 757 } 758 759 func getApplicationSetContainerImage(cr *argoproj.ArgoCD) string { 760 defaultImg, defaultTag := false, false 761 762 img := "" 763 tag := "" 764 765 // First pull from spec, if it exists 766 if cr.Spec.ApplicationSet != nil { 767 img = cr.Spec.ApplicationSet.Image 768 tag = cr.Spec.ApplicationSet.Version 769 } 770 771 // If spec is empty, use the defaults 772 if img == "" { 773 img = common.ArgoCDDefaultArgoImage 774 defaultImg = true 775 } 776 if tag == "" { 777 tag = common.ArgoCDDefaultArgoVersion 778 defaultTag = true 779 } 780 781 // If an env var is specified then use that, but don't override the spec values (if they are present) 782 if e := os.Getenv(common.ArgoCDImageEnvName); e != "" && (defaultTag && defaultImg) { 783 return e 784 } 785 return argoutil.CombineImageTag(img, tag) 786 } 787 788 // getApplicationSetResources will return the ResourceRequirements for the Application Sets container. 789 func getApplicationSetResources(cr *argoproj.ArgoCD) corev1.ResourceRequirements { 790 resources := corev1.ResourceRequirements{} 791 792 // Allow override of resource requirements from CR 793 if cr.Spec.ApplicationSet.Resources != nil { 794 resources = *cr.Spec.ApplicationSet.Resources 795 } 796 797 return resources 798 } 799 800 func setAppSetLabels(obj *metav1.ObjectMeta) { 801 obj.Labels["app.kubernetes.io/name"] = "argocd-applicationset-controller" 802 obj.Labels["app.kubernetes.io/part-of"] = "argocd-applicationset" 803 obj.Labels["app.kubernetes.io/component"] = "controller" 804 } 805 806 // reconcileApplicationSetService will ensure that the Service is present for the ApplicationSet webhook and metrics component. 807 func (r *ReconcileArgoCD) reconcileApplicationSetService(cr *argoproj.ArgoCD) error { 808 log.Info("reconciling applicationset service") 809 810 svc := newServiceWithSuffix(common.ApplicationSetServiceNameSuffix, common.ApplicationSetServiceNameSuffix, cr) 811 if cr.Spec.ApplicationSet == nil || !cr.Spec.ApplicationSet.IsEnabled() { 812 813 if argoutil.IsObjectFound(r.Client, cr.Namespace, svc.Name, svc) { 814 err := argoutil.FetchObject(r.Client, cr.Namespace, svc.Name, svc) 815 if err != nil { 816 return err 817 } 818 log.Info(fmt.Sprintf("Deleting applicationset controller service %s as applicationset is disabled", svc.Name)) 819 err = r.Delete(context.TODO(), svc) 820 if err != nil { 821 return err 822 } 823 } 824 return nil 825 } else { 826 if argoutil.IsObjectFound(r.Client, cr.Namespace, svc.Name, svc) { 827 return nil // Service found, do nothing 828 } 829 } 830 svc.Spec.Ports = []corev1.ServicePort{ 831 { 832 Name: "webhook", 833 Port: 7000, 834 Protocol: corev1.ProtocolTCP, 835 TargetPort: intstr.FromInt(7000), 836 }, { 837 Name: "metrics", 838 Port: 8080, 839 Protocol: corev1.ProtocolTCP, 840 TargetPort: intstr.FromInt(8080), 841 }, 842 } 843 844 svc.Spec.Selector = map[string]string{ 845 common.ArgoCDKeyName: nameWithSuffix(common.ApplicationSetServiceNameSuffix, cr), 846 } 847 848 if err := controllerutil.SetControllerReference(cr, svc, r.Scheme); err != nil { 849 return err 850 } 851 return r.Client.Create(context.TODO(), svc) 852 } 853 854 // Returns the name of the role/rolebinding for the source namespaces for applicationset-controller in the format of "argocdName-argocdNamespace-applicationset" 855 func getResourceNameForApplicationSetSourceNamespaces(cr *argoproj.ArgoCD) string { 856 return fmt.Sprintf("%s-%s-applicationset", cr.Name, cr.Namespace) 857 } 858 859 // removeUnmanagedApplicationSetSourceNamespaceResources cleansup resources from ApplicationSetSourceNamespaces if namespace is not managed by argocd instance. 860 // ManagedApplicationSetSourceNamespaces var keeps track of namespaces with appset resources. 861 func (r *ReconcileArgoCD) removeUnmanagedApplicationSetSourceNamespaceResources(cr *argoproj.ArgoCD) error { 862 863 for ns := range r.ManagedApplicationSetSourceNamespaces { 864 managedNamespace := false 865 if cr.Spec.ApplicationSet != nil && cr.GetDeletionTimestamp() == nil { 866 appsNamespaces, err := r.getSourceNamespaces(cr) 867 if err != nil { 868 return err 869 } 870 for _, namespace := range cr.Spec.ApplicationSet.SourceNamespaces { 871 // appset ns should be part of apps ns 872 if namespace == ns && contains(appsNamespaces, namespace) { 873 managedNamespace = true 874 break 875 } 876 } 877 } 878 879 if !managedNamespace { 880 if err := r.cleanupUnmanagedApplicationSetSourceNamespaceResources(cr, ns); err != nil { 881 log.Error(err, fmt.Sprintf("error cleaning up applicationset resources for namespace %s", ns)) 882 continue 883 } 884 delete(r.ManagedApplicationSetSourceNamespaces, ns) 885 } 886 } 887 return nil 888 } 889 890 // cleanupUnmanagedApplicationSetSourceNamespaceResources removes the application set resources from target namespace 891 func (r *ReconcileArgoCD) cleanupUnmanagedApplicationSetSourceNamespaceResources(cr *argoproj.ArgoCD, ns string) error { 892 namespace := corev1.Namespace{} 893 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: ns}, &namespace); err != nil { 894 if !apierrors.IsNotFound(err) { 895 return err 896 } 897 return nil 898 } 899 900 // Delete applicationset role & rolebinding 901 existingRole := v1.Role{} 902 roleName := getResourceNameForApplicationSetSourceNamespaces(cr) 903 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: roleName, Namespace: namespace.Name}, &existingRole); err != nil { 904 if !apierrors.IsNotFound(err) { 905 return fmt.Errorf("failed to fetch the role for the service account associated with %s : %s", common.ArgoCDApplicationSetControllerComponent, err) 906 } 907 } 908 if existingRole.Name != "" { 909 err := r.Client.Delete(context.TODO(), &existingRole) 910 if err != nil { 911 return err 912 } 913 } 914 915 existingRoleBinding := &v1.RoleBinding{} 916 roleBindingName := getResourceNameForApplicationSetSourceNamespaces(cr) 917 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: roleBindingName, Namespace: namespace.Name}, existingRoleBinding); err != nil { 918 if !apierrors.IsNotFound(err) { 919 return fmt.Errorf("failed to get the rolebinding associated with %s : %s", common.ArgoCDApplicationSetControllerComponent, err) 920 } 921 } 922 if existingRoleBinding.Name != "" { 923 if err := r.Client.Delete(context.TODO(), existingRoleBinding); err != nil { 924 return err 925 } 926 } 927 928 // app-in-any-ns code will handle removal of appsets permissions for argocd-server in target namespace 929 930 // Remove applicationset-managed-by-cluster-argocd label from the namespace 931 delete(namespace.Labels, common.ArgoCDApplicationSetManagedByClusterArgoCDLabel) 932 if err := r.Client.Update(context.TODO(), &namespace); err != nil { 933 return fmt.Errorf("failed to remove applicationset label from namespace %s : %s", namespace.Name, err) 934 } 935 936 return nil 937 } 938 939 // setManagedApplicationSetSourceNamespaces populates ManagedApplicationSetSourceNamespaces var with namespaces 940 // with "argocd.argoproj.io/applicationset-managed-by-cluster-argocd" label. 941 func (r *ReconcileArgoCD) setManagedApplicationSetSourceNamespaces(cr *argoproj.ArgoCD) error { 942 if r.ManagedApplicationSetSourceNamespaces == nil { 943 r.ManagedApplicationSetSourceNamespaces = make(map[string]string) 944 } 945 namespaces := &corev1.NamespaceList{} 946 listOption := client.MatchingLabels{ 947 common.ArgoCDApplicationSetManagedByClusterArgoCDLabel: cr.Namespace, 948 } 949 950 // get the list of namespaces managed with "argocd.argoproj.io/applicationset-managed-by-cluster-argocd" label 951 if err := r.Client.List(context.TODO(), namespaces, listOption); err != nil { 952 return err 953 } 954 955 for _, namespace := range namespaces.Items { 956 r.ManagedApplicationSetSourceNamespaces[namespace.Name] = "" 957 } 958 959 return nil 960 } 961 962 // reconcileSourceNamespaceRole creates/updates role 963 func (r *ReconcileArgoCD) reconcileSourceNamespaceRole(role v1.Role, cr *argoproj.ArgoCD) error { 964 965 if err := applyReconcilerHook(cr, role, ""); err != nil { 966 return err 967 } 968 969 existingRole := v1.Role{} 970 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: role.Name, Namespace: role.Namespace}, &existingRole) 971 if err != nil { 972 if !apierrors.IsNotFound(err) { 973 errMsg := fmt.Errorf("failed to retrieve role %s in namespace %s", role.Name, role.Namespace) 974 return errors.Join(errMsg, err) 975 } 976 977 if err := r.Client.Create(context.TODO(), &role); err != nil { 978 errMsg := fmt.Errorf("failed to create role %s in namespace %s", role.Name, role.Namespace) 979 return errors.Join(errMsg, err) 980 } 981 982 log.Info(fmt.Sprintf("role %s created successfully for Argo CD instance %s in namespace %s", role.Name, cr.Name, role.Namespace)) 983 return nil 984 } 985 986 // if the Rules differ, update the Role, ignore if role is just created. 987 if !reflect.DeepEqual(existingRole.Rules, role.Rules) { 988 existingRole.Rules = role.Rules 989 if err := r.Client.Update(context.TODO(), &existingRole); err != nil { 990 errMsg := fmt.Errorf("failed to update role %s in namespace %s", role.Name, role.Namespace) 991 return errors.Join(errMsg, err) 992 } 993 log.Info(fmt.Sprintf("role %s update successfully for Argo CD instance %s in namespace %s", role.Name, cr.Name, role.Namespace)) 994 } 995 996 return nil 997 } 998 999 // reconcileSourceNamespaceRole creates/updates rolebinding 1000 func (r *ReconcileArgoCD) reconcileSourceNamespaceRoleBinding(roleBinding v1.RoleBinding, cr *argoproj.ArgoCD) error { 1001 1002 if err := applyReconcilerHook(cr, roleBinding, ""); err != nil { 1003 return err 1004 } 1005 1006 existingRoleBinding := v1.RoleBinding{} 1007 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: roleBinding.Name, Namespace: roleBinding.Namespace}, &existingRoleBinding) 1008 if err != nil { 1009 if !apierrors.IsNotFound(err) { 1010 errMsg := fmt.Errorf("failed to retrieve rolebinding %s in namespace %s", roleBinding.Name, roleBinding.Namespace) 1011 return errors.Join(errMsg, err) 1012 } 1013 1014 if err := r.Client.Create(context.TODO(), &roleBinding); err != nil { 1015 errMsg := fmt.Errorf("failed to create rolebinding %s in namespace %s", roleBinding.Name, roleBinding.Namespace) 1016 return errors.Join(errMsg, err) 1017 } 1018 1019 log.Info(fmt.Sprintf("rolebinding %s created successfully for Argo CD instance %s in namespace %s", roleBinding.Name, cr.Name, roleBinding.Namespace)) 1020 return nil 1021 } 1022 1023 // if the RoleRef changes, delete the existing role binding and create a new one 1024 if !reflect.DeepEqual(roleBinding.RoleRef, existingRoleBinding.RoleRef) { 1025 if err = r.Client.Delete(context.TODO(), &existingRoleBinding); err != nil { 1026 return err 1027 } 1028 } else { 1029 // if the Subjects differ, update the role bindings 1030 if !reflect.DeepEqual(roleBinding.Subjects, existingRoleBinding.Subjects) { 1031 existingRoleBinding.Subjects = roleBinding.Subjects 1032 if err = r.Client.Update(context.TODO(), &existingRoleBinding); err != nil { 1033 return err 1034 } 1035 log.Info(fmt.Sprintf("rolebinding %s update successfully for Argo CD instance %s in namespace %s", roleBinding.Name, cr.Name, roleBinding.Namespace)) 1036 } 1037 } 1038 1039 return nil 1040 } 1041 1042 // getApplicationSetSourceNamespaces return list of namespaces from .spec.ApplicationSet.SourceNamespaces 1043 func (r *ReconcileArgoCD) getApplicationSetSourceNamespaces(cr *argoproj.ArgoCD) []string { 1044 if cr.Spec.ApplicationSet != nil { 1045 return cr.Spec.ApplicationSet.SourceNamespaces 1046 } 1047 return []string(nil) 1048 }