github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocd/statefulset.go (about) 1 // Copyright 2019 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 "fmt" 20 "reflect" 21 "strconv" 22 "time" 23 24 appsv1 "k8s.io/api/apps/v1" 25 corev1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/util/intstr" 28 "sigs.k8s.io/controller-runtime/pkg/client" 29 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 30 31 argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" 32 "github.com/argoproj-labs/argocd-operator/common" 33 "github.com/argoproj-labs/argocd-operator/controllers/argoutil" 34 ) 35 36 func getRedisHAReplicas(cr *argoproj.ArgoCD) *int32 { 37 replicas := common.ArgoCDDefaultRedisHAReplicas 38 // TODO: Allow override of this value through CR? 39 return &replicas 40 } 41 42 // newStatefulSet returns a new StatefulSet instance for the given ArgoCD instance. 43 func newStatefulSet(cr *argoproj.ArgoCD) *appsv1.StatefulSet { 44 return &appsv1.StatefulSet{ 45 ObjectMeta: metav1.ObjectMeta{ 46 Name: cr.Name, 47 Namespace: cr.Namespace, 48 Labels: argoutil.LabelsForCluster(cr), 49 }, 50 } 51 } 52 53 // newStatefulSetWithName returns a new StatefulSet instance for the given ArgoCD using the given name. 54 func newStatefulSetWithName(name string, component string, cr *argoproj.ArgoCD) *appsv1.StatefulSet { 55 ss := newStatefulSet(cr) 56 ss.ObjectMeta.Name = name 57 58 lbls := ss.ObjectMeta.Labels 59 lbls[common.ArgoCDKeyName] = name 60 lbls[common.ArgoCDKeyComponent] = component 61 ss.ObjectMeta.Labels = lbls 62 63 ss.Spec = appsv1.StatefulSetSpec{ 64 Selector: &metav1.LabelSelector{ 65 MatchLabels: map[string]string{ 66 common.ArgoCDKeyName: name, 67 }, 68 }, 69 Template: corev1.PodTemplateSpec{ 70 ObjectMeta: metav1.ObjectMeta{ 71 Labels: map[string]string{ 72 common.ArgoCDKeyName: name, 73 }, 74 }, 75 Spec: corev1.PodSpec{ 76 NodeSelector: common.DefaultNodeSelector(), 77 }, 78 }, 79 } 80 if cr.Spec.NodePlacement != nil { 81 ss.Spec.Template.Spec.NodeSelector = argoutil.AppendStringMap(ss.Spec.Template.Spec.NodeSelector, cr.Spec.NodePlacement.NodeSelector) 82 ss.Spec.Template.Spec.Tolerations = cr.Spec.NodePlacement.Tolerations 83 } 84 ss.Spec.ServiceName = name 85 86 return ss 87 } 88 89 // newStatefulSetWithSuffix returns a new StatefulSet instance for the given ArgoCD using the given suffix. 90 func newStatefulSetWithSuffix(suffix string, component string, cr *argoproj.ArgoCD) *appsv1.StatefulSet { 91 return newStatefulSetWithName(fmt.Sprintf("%s-%s", cr.Name, suffix), component, cr) 92 } 93 94 func (r *ReconcileArgoCD) reconcileRedisStatefulSet(cr *argoproj.ArgoCD) error { 95 ss := newStatefulSetWithSuffix("redis-ha-server", "redis", cr) 96 97 ss.Spec.PodManagementPolicy = appsv1.OrderedReadyPodManagement 98 ss.Spec.Replicas = getRedisHAReplicas(cr) 99 ss.Spec.Selector = &metav1.LabelSelector{ 100 MatchLabels: map[string]string{ 101 common.ArgoCDKeyName: nameWithSuffix("redis-ha", cr), 102 }, 103 } 104 105 ss.Spec.ServiceName = nameWithSuffix("redis-ha", cr) 106 107 ss.Spec.Template.ObjectMeta = metav1.ObjectMeta{ 108 Annotations: map[string]string{ 109 "checksum/init-config": "7128bfbb51eafaffe3c33b1b463e15f0cf6514cec570f9d9c4f2396f28c724ac", // TODO: Should this be hard-coded? 110 }, 111 Labels: map[string]string{ 112 common.ArgoCDKeyName: nameWithSuffix("redis-ha", cr), 113 }, 114 } 115 116 ss.Spec.Template.Spec.Affinity = &corev1.Affinity{ 117 PodAntiAffinity: &corev1.PodAntiAffinity{ 118 RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{{ 119 LabelSelector: &metav1.LabelSelector{ 120 MatchLabels: map[string]string{ 121 common.ArgoCDKeyName: nameWithSuffix("redis-ha", cr), 122 }, 123 }, 124 TopologyKey: common.ArgoCDKeyHostname, 125 }}, 126 }, 127 } 128 129 f := false 130 ss.Spec.Template.Spec.AutomountServiceAccountToken = &f 131 132 ss.Spec.Template.Spec.Containers = []corev1.Container{ 133 { 134 Args: []string{ 135 "/data/conf/redis.conf", 136 }, 137 Command: []string{ 138 "redis-server", 139 }, 140 Image: getRedisHAContainerImage(cr), 141 ImagePullPolicy: corev1.PullIfNotPresent, 142 LivenessProbe: &corev1.Probe{ 143 ProbeHandler: corev1.ProbeHandler{ 144 Exec: &corev1.ExecAction{ 145 Command: []string{ 146 "sh", 147 "-c", 148 "/health/redis_liveness.sh", 149 }, 150 }, 151 }, 152 FailureThreshold: int32(5), 153 InitialDelaySeconds: int32(30), 154 PeriodSeconds: int32(15), 155 SuccessThreshold: int32(1), 156 TimeoutSeconds: int32(15), 157 }, 158 Name: "redis", 159 Ports: []corev1.ContainerPort{{ 160 ContainerPort: common.ArgoCDDefaultRedisPort, 161 Name: "redis", 162 }}, 163 ReadinessProbe: &corev1.Probe{ 164 ProbeHandler: corev1.ProbeHandler{ 165 Exec: &corev1.ExecAction{ 166 Command: []string{ 167 "sh", 168 "-c", 169 "/health/redis_readiness.sh", 170 }, 171 }, 172 }, 173 FailureThreshold: int32(5), 174 InitialDelaySeconds: int32(30), 175 PeriodSeconds: int32(15), 176 SuccessThreshold: int32(1), 177 TimeoutSeconds: int32(15), 178 }, 179 Resources: getRedisHAResources(cr), 180 SecurityContext: &corev1.SecurityContext{ 181 AllowPrivilegeEscalation: boolPtr(false), 182 Capabilities: &corev1.Capabilities{ 183 Drop: []corev1.Capability{ 184 "ALL", 185 }, 186 }, 187 RunAsNonRoot: boolPtr(true), 188 }, 189 VolumeMounts: []corev1.VolumeMount{ 190 { 191 MountPath: "/data", 192 Name: "data", 193 }, 194 { 195 MountPath: "/health", 196 Name: "health", 197 }, 198 { 199 Name: common.ArgoCDRedisServerTLSSecretName, 200 MountPath: "/app/config/redis/tls", 201 }, 202 }, 203 }, 204 { 205 Args: []string{ 206 "/data/conf/sentinel.conf", 207 }, 208 Command: []string{ 209 "redis-sentinel", 210 }, 211 Image: getRedisHAContainerImage(cr), 212 ImagePullPolicy: corev1.PullIfNotPresent, 213 LivenessProbe: &corev1.Probe{ 214 ProbeHandler: corev1.ProbeHandler{ 215 Exec: &corev1.ExecAction{ 216 Command: []string{ 217 "sh", 218 "-c", 219 "/health/sentinel_liveness.sh", 220 }, 221 }, 222 }, 223 FailureThreshold: int32(5), 224 InitialDelaySeconds: int32(30), 225 PeriodSeconds: int32(15), 226 SuccessThreshold: int32(1), 227 TimeoutSeconds: int32(15), 228 }, 229 Name: "sentinel", 230 Ports: []corev1.ContainerPort{{ 231 ContainerPort: common.ArgoCDDefaultRedisSentinelPort, 232 Name: "sentinel", 233 }}, 234 ReadinessProbe: &corev1.Probe{ 235 ProbeHandler: corev1.ProbeHandler{ 236 Exec: &corev1.ExecAction{ 237 Command: []string{ 238 "sh", 239 "-c", 240 "/health/sentinel_liveness.sh", 241 }, 242 }, 243 }, 244 FailureThreshold: int32(5), 245 InitialDelaySeconds: int32(30), 246 PeriodSeconds: int32(15), 247 SuccessThreshold: int32(1), 248 TimeoutSeconds: int32(15), 249 }, 250 Resources: getRedisHAResources(cr), 251 SecurityContext: &corev1.SecurityContext{ 252 AllowPrivilegeEscalation: boolPtr(false), 253 Capabilities: &corev1.Capabilities{ 254 Drop: []corev1.Capability{ 255 "ALL", 256 }, 257 }, 258 RunAsNonRoot: boolPtr(true), 259 }, 260 VolumeMounts: []corev1.VolumeMount{ 261 { 262 MountPath: "/data", 263 Name: "data", 264 }, 265 { 266 MountPath: "/health", 267 Name: "health", 268 }, 269 { 270 Name: common.ArgoCDRedisServerTLSSecretName, 271 MountPath: "/app/config/redis/tls", 272 }, 273 }, 274 }, 275 } 276 277 ss.Spec.Template.Spec.InitContainers = []corev1.Container{{ 278 Args: []string{ 279 "/readonly-config/init.sh", 280 }, 281 Command: []string{ 282 "sh", 283 }, 284 Env: []corev1.EnvVar{ 285 { 286 Name: "SENTINEL_ID_0", 287 Value: "3c0d9c0320bb34888c2df5757c718ce6ca992ce6", // TODO: Should this be hard-coded? 288 }, 289 { 290 Name: "SENTINEL_ID_1", 291 Value: "40000915ab58c3fa8fd888fb8b24711944e6cbb4", // TODO: Should this be hard-coded? 292 }, 293 { 294 Name: "SENTINEL_ID_2", 295 Value: "2bbec7894d954a8af3bb54d13eaec53cb024e2ca", // TODO: Should this be hard-coded? 296 }, 297 }, 298 Image: getRedisHAContainerImage(cr), 299 ImagePullPolicy: corev1.PullIfNotPresent, 300 Name: "config-init", 301 Resources: getRedisHAResources(cr), 302 SecurityContext: &corev1.SecurityContext{ 303 AllowPrivilegeEscalation: boolPtr(false), 304 Capabilities: &corev1.Capabilities{ 305 Drop: []corev1.Capability{ 306 "ALL", 307 }, 308 }, 309 RunAsNonRoot: boolPtr(true), 310 }, 311 VolumeMounts: []corev1.VolumeMount{ 312 { 313 MountPath: "/readonly-config", 314 Name: "config", 315 ReadOnly: true, 316 }, 317 { 318 MountPath: "/data", 319 Name: "data", 320 }, 321 { 322 Name: common.ArgoCDRedisServerTLSSecretName, 323 MountPath: "/app/config/redis/tls", 324 }, 325 }, 326 }} 327 328 var fsGroup int64 = 1000 329 var runAsNonRoot bool = true 330 var runAsUser int64 = 1000 331 332 ss.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{ 333 FSGroup: &fsGroup, 334 RunAsNonRoot: &runAsNonRoot, 335 RunAsUser: &runAsUser, 336 } 337 AddSeccompProfileForOpenShift(r.Client, &ss.Spec.Template.Spec) 338 339 ss.Spec.Template.Spec.ServiceAccountName = nameWithSuffix("argocd-redis-ha", cr) 340 341 var terminationGracePeriodSeconds int64 = 60 342 ss.Spec.Template.Spec.TerminationGracePeriodSeconds = &terminationGracePeriodSeconds 343 344 var defaultMode int32 = 493 345 ss.Spec.Template.Spec.Volumes = []corev1.Volume{ 346 { 347 Name: "config", 348 VolumeSource: corev1.VolumeSource{ 349 ConfigMap: &corev1.ConfigMapVolumeSource{ 350 LocalObjectReference: corev1.LocalObjectReference{ 351 Name: common.ArgoCDRedisHAConfigMapName, 352 }, 353 }, 354 }, 355 }, 356 { 357 Name: "health", 358 VolumeSource: corev1.VolumeSource{ 359 ConfigMap: &corev1.ConfigMapVolumeSource{ 360 DefaultMode: &defaultMode, 361 LocalObjectReference: corev1.LocalObjectReference{ 362 Name: common.ArgoCDRedisHAHealthConfigMapName, 363 }, 364 }, 365 }, 366 }, 367 { 368 Name: "data", 369 VolumeSource: corev1.VolumeSource{ 370 EmptyDir: &corev1.EmptyDirVolumeSource{}, 371 }, 372 }, 373 { 374 Name: common.ArgoCDRedisServerTLSSecretName, 375 VolumeSource: corev1.VolumeSource{ 376 Secret: &corev1.SecretVolumeSource{ 377 SecretName: common.ArgoCDRedisServerTLSSecretName, 378 Optional: boolPtr(true), 379 }, 380 }, 381 }, 382 } 383 384 ss.Spec.UpdateStrategy = appsv1.StatefulSetUpdateStrategy{ 385 Type: appsv1.RollingUpdateStatefulSetStrategyType, 386 } 387 388 if err := applyReconcilerHook(cr, ss, ""); err != nil { 389 return err 390 } 391 392 existing := newStatefulSetWithSuffix("redis-ha-server", "redis", cr) 393 if argoutil.IsObjectFound(r.Client, cr.Namespace, existing.Name, existing) { 394 if !(cr.Spec.HA.Enabled && cr.Spec.Redis.IsEnabled()) { 395 // StatefulSet exists but either HA or component enabled flag has been set to false, delete the StatefulSet 396 return r.Client.Delete(context.TODO(), existing) 397 } 398 399 desiredImage := getRedisHAContainerImage(cr) 400 changed := false 401 updateNodePlacementStateful(existing, ss, &changed) 402 for i, container := range existing.Spec.Template.Spec.Containers { 403 if container.Image != desiredImage { 404 existing.Spec.Template.Spec.Containers[i].Image = getRedisHAContainerImage(cr) 405 existing.Spec.Template.ObjectMeta.Labels["image.upgraded"] = time.Now().UTC().Format("01022006-150406-MST") 406 changed = true 407 } 408 409 if !reflect.DeepEqual(ss.Spec.Template.Spec.Containers[i].Resources, existing.Spec.Template.Spec.Containers[i].Resources) { 410 existing.Spec.Template.Spec.Containers[i].Resources = ss.Spec.Template.Spec.Containers[i].Resources 411 changed = true 412 } 413 } 414 415 if !reflect.DeepEqual(ss.Spec.Template.Spec.InitContainers[0].Resources, existing.Spec.Template.Spec.InitContainers[0].Resources) { 416 existing.Spec.Template.Spec.InitContainers[0].Resources = ss.Spec.Template.Spec.InitContainers[0].Resources 417 changed = true 418 } 419 420 if changed { 421 return r.Client.Update(context.TODO(), existing) 422 } 423 424 return nil // StatefulSet found, do nothing 425 } 426 427 if !cr.Spec.Redis.IsEnabled() { 428 log.Info("Redis disabled. Skipping starting Redis.") // Redis not enabled, do nothing. 429 return nil 430 } 431 432 if !cr.Spec.HA.Enabled { 433 return nil // HA not enabled, do nothing. 434 } 435 436 if err := controllerutil.SetControllerReference(cr, ss, r.Scheme); err != nil { 437 return err 438 } 439 return r.Client.Create(context.TODO(), ss) 440 } 441 442 func getArgoControllerContainerEnv(cr *argoproj.ArgoCD) []corev1.EnvVar { 443 env := make([]corev1.EnvVar, 0) 444 445 env = append(env, corev1.EnvVar{ 446 Name: "HOME", 447 Value: "/home/argocd", 448 }) 449 450 if cr.Spec.Controller.Sharding.Enabled { 451 env = append(env, corev1.EnvVar{ 452 Name: "ARGOCD_CONTROLLER_REPLICAS", 453 Value: fmt.Sprint(cr.Spec.Controller.Sharding.Replicas), 454 }) 455 } 456 457 if cr.Spec.Controller.AppSync != nil { 458 env = append(env, corev1.EnvVar{ 459 Name: "ARGOCD_RECONCILIATION_TIMEOUT", 460 Value: strconv.FormatInt(int64(cr.Spec.Controller.AppSync.Seconds()), 10) + "s", 461 }) 462 } 463 464 return env 465 } 466 467 func (r *ReconcileArgoCD) getApplicationControllerReplicaCount(cr *argoproj.ArgoCD) int32 { 468 var replicas int32 = common.ArgocdApplicationControllerDefaultReplicas 469 var minShards int32 = cr.Spec.Controller.Sharding.MinShards 470 var maxShards int32 = cr.Spec.Controller.Sharding.MaxShards 471 472 if cr.Spec.Controller.Sharding.DynamicScalingEnabled != nil && *cr.Spec.Controller.Sharding.DynamicScalingEnabled { 473 474 // TODO: add the same validations to Validation Webhook once webhook has been introduced 475 if minShards < 1 { 476 log.Info("Minimum number of shards cannot be less than 1. Setting default value to 1") 477 minShards = 1 478 } 479 480 if maxShards < minShards { 481 log.Info("Maximum number of shards cannot be less than minimum number of shards. Setting maximum shards same as minimum shards") 482 maxShards = minShards 483 } 484 485 clustersPerShard := cr.Spec.Controller.Sharding.ClustersPerShard 486 if clustersPerShard < 1 { 487 log.Info("clustersPerShard cannot be less than 1. Defaulting to 1.") 488 clustersPerShard = 1 489 } 490 491 clusterSecrets, err := r.getClusterSecrets(cr) 492 if err != nil { 493 // If we were not able to query cluster secrets, return the default count of replicas (ArgocdApplicationControllerDefaultReplicas) 494 log.Error(err, "Error retreiving cluster secrets for ArgoCD instance %s", cr.Name) 495 return replicas 496 } 497 498 replicas = int32(len(clusterSecrets.Items)) / clustersPerShard 499 500 if replicas < minShards { 501 replicas = minShards 502 } 503 504 if replicas > maxShards { 505 replicas = maxShards 506 } 507 508 return replicas 509 510 } else if cr.Spec.Controller.Sharding.Replicas != 0 && cr.Spec.Controller.Sharding.Enabled { 511 return cr.Spec.Controller.Sharding.Replicas 512 } 513 514 return replicas 515 } 516 517 func (r *ReconcileArgoCD) reconcileApplicationControllerStatefulSet(cr *argoproj.ArgoCD, useTLSForRedis bool) error { 518 519 replicas := r.getApplicationControllerReplicaCount(cr) 520 521 ss := newStatefulSetWithSuffix("application-controller", "application-controller", cr) 522 ss.Spec.Replicas = &replicas 523 controllerEnv := cr.Spec.Controller.Env 524 // Sharding setting explicitly overrides a value set in the env 525 controllerEnv = argoutil.EnvMerge(controllerEnv, getArgoControllerContainerEnv(cr), true) 526 // Let user specify their own environment first 527 controllerEnv = argoutil.EnvMerge(controllerEnv, proxyEnvVars(), false) 528 podSpec := &ss.Spec.Template.Spec 529 podSpec.Containers = []corev1.Container{{ 530 Command: getArgoApplicationControllerCommand(cr, useTLSForRedis), 531 Image: getArgoContainerImage(cr), 532 ImagePullPolicy: corev1.PullAlways, 533 Name: "argocd-application-controller", 534 Env: controllerEnv, 535 Ports: []corev1.ContainerPort{ 536 { 537 ContainerPort: 8082, 538 }, 539 }, 540 ReadinessProbe: &corev1.Probe{ 541 ProbeHandler: corev1.ProbeHandler{ 542 HTTPGet: &corev1.HTTPGetAction{ 543 Path: "/healthz", 544 Port: intstr.FromInt(8082), 545 }, 546 }, 547 InitialDelaySeconds: 5, 548 PeriodSeconds: 10, 549 }, 550 Resources: getArgoApplicationControllerResources(cr), 551 SecurityContext: &corev1.SecurityContext{ 552 AllowPrivilegeEscalation: boolPtr(false), 553 Capabilities: &corev1.Capabilities{ 554 Drop: []corev1.Capability{ 555 "ALL", 556 }, 557 }, 558 RunAsNonRoot: boolPtr(true), 559 }, 560 VolumeMounts: []corev1.VolumeMount{ 561 { 562 Name: "argocd-repo-server-tls", 563 MountPath: "/app/config/controller/tls", 564 }, 565 { 566 Name: common.ArgoCDRedisServerTLSSecretName, 567 MountPath: "/app/config/controller/tls/redis", 568 }, 569 }, 570 }} 571 AddSeccompProfileForOpenShift(r.Client, podSpec) 572 podSpec.ServiceAccountName = nameWithSuffix("argocd-application-controller", cr) 573 podSpec.Volumes = []corev1.Volume{ 574 { 575 Name: "argocd-repo-server-tls", 576 VolumeSource: corev1.VolumeSource{ 577 Secret: &corev1.SecretVolumeSource{ 578 SecretName: common.ArgoCDRepoServerTLSSecretName, 579 Optional: boolPtr(true), 580 }, 581 }, 582 }, 583 { 584 Name: common.ArgoCDRedisServerTLSSecretName, 585 VolumeSource: corev1.VolumeSource{ 586 Secret: &corev1.SecretVolumeSource{ 587 SecretName: common.ArgoCDRedisServerTLSSecretName, 588 Optional: boolPtr(true), 589 }, 590 }, 591 }, 592 } 593 594 ss.Spec.Template.Spec.Affinity = &corev1.Affinity{ 595 PodAntiAffinity: &corev1.PodAntiAffinity{ 596 PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{{ 597 PodAffinityTerm: corev1.PodAffinityTerm{ 598 LabelSelector: &metav1.LabelSelector{ 599 MatchLabels: map[string]string{ 600 common.ArgoCDKeyName: nameWithSuffix("argocd-application-controller", cr), 601 }, 602 }, 603 TopologyKey: common.ArgoCDKeyHostname, 604 }, 605 Weight: int32(100), 606 }, 607 { 608 PodAffinityTerm: corev1.PodAffinityTerm{ 609 LabelSelector: &metav1.LabelSelector{ 610 MatchLabels: map[string]string{ 611 common.ArgoCDKeyPartOf: common.ArgoCDAppName, 612 }, 613 }, 614 TopologyKey: common.ArgoCDKeyHostname, 615 }, 616 Weight: int32(5), 617 }}, 618 }, 619 } 620 621 // Handle import/restore from ArgoCDExport 622 export := r.getArgoCDExport(cr) 623 if export == nil { 624 log.Info("existing argocd export not found, skipping import") 625 } else { 626 podSpec.InitContainers = []corev1.Container{{ 627 Command: getArgoImportCommand(r.Client, cr), 628 Env: proxyEnvVars(getArgoImportContainerEnv(export)...), 629 Resources: getArgoApplicationControllerResources(cr), 630 Image: getArgoImportContainerImage(export), 631 ImagePullPolicy: corev1.PullAlways, 632 Name: "argocd-import", 633 SecurityContext: &corev1.SecurityContext{ 634 AllowPrivilegeEscalation: boolPtr(false), 635 Capabilities: &corev1.Capabilities{ 636 Drop: []corev1.Capability{ 637 "ALL", 638 }, 639 }, 640 RunAsNonRoot: boolPtr(true), 641 }, 642 VolumeMounts: getArgoImportVolumeMounts(), 643 }} 644 645 podSpec.Volumes = getArgoImportVolumes(export) 646 } 647 648 invalidImagePod := containsInvalidImage(cr, r) 649 if invalidImagePod { 650 if err := r.Client.Delete(context.TODO(), ss); err != nil { 651 return err 652 } 653 } 654 655 existing := newStatefulSetWithSuffix("application-controller", "application-controller", cr) 656 if argoutil.IsObjectFound(r.Client, cr.Namespace, existing.Name, existing) { 657 if !cr.Spec.Controller.IsEnabled() { 658 log.Info("Existing application controller found but should be disabled. Deleting Application Controller") 659 // Delete existing deployment for Application Controller, if any .. 660 return r.Client.Delete(context.TODO(), existing) 661 } 662 actualImage := existing.Spec.Template.Spec.Containers[0].Image 663 desiredImage := getArgoContainerImage(cr) 664 changed := false 665 if actualImage != desiredImage { 666 existing.Spec.Template.Spec.Containers[0].Image = desiredImage 667 existing.Spec.Template.ObjectMeta.Labels["image.upgraded"] = time.Now().UTC().Format("01022006-150406-MST") 668 changed = true 669 } 670 desiredCommand := getArgoApplicationControllerCommand(cr, useTLSForRedis) 671 if isRepoServerTLSVerificationRequested(cr) { 672 desiredCommand = append(desiredCommand, "--repo-server-strict-tls") 673 } 674 updateNodePlacementStateful(existing, ss, &changed) 675 if !reflect.DeepEqual(desiredCommand, existing.Spec.Template.Spec.Containers[0].Command) { 676 existing.Spec.Template.Spec.Containers[0].Command = desiredCommand 677 changed = true 678 } 679 680 if !reflect.DeepEqual(existing.Spec.Template.Spec.Containers[0].Env, 681 ss.Spec.Template.Spec.Containers[0].Env) { 682 existing.Spec.Template.Spec.Containers[0].Env = ss.Spec.Template.Spec.Containers[0].Env 683 changed = true 684 } 685 if !reflect.DeepEqual(ss.Spec.Template.Spec.Volumes, existing.Spec.Template.Spec.Volumes) { 686 existing.Spec.Template.Spec.Volumes = ss.Spec.Template.Spec.Volumes 687 changed = true 688 } 689 if !reflect.DeepEqual(ss.Spec.Template.Spec.Containers[0].VolumeMounts, 690 existing.Spec.Template.Spec.Containers[0].VolumeMounts) { 691 existing.Spec.Template.Spec.Containers[0].VolumeMounts = ss.Spec.Template.Spec.Containers[0].VolumeMounts 692 changed = true 693 } 694 if !reflect.DeepEqual(ss.Spec.Template.Spec.Containers[0].Resources, existing.Spec.Template.Spec.Containers[0].Resources) { 695 existing.Spec.Template.Spec.Containers[0].Resources = ss.Spec.Template.Spec.Containers[0].Resources 696 changed = true 697 } 698 if !reflect.DeepEqual(ss.Spec.Replicas, existing.Spec.Replicas) { 699 existing.Spec.Replicas = ss.Spec.Replicas 700 changed = true 701 } 702 703 if changed { 704 return r.Client.Update(context.TODO(), existing) 705 } 706 return nil // StatefulSet found with nothing to do, move along... 707 } 708 709 if !cr.Spec.Controller.IsEnabled() { 710 log.Info("Application Controller disabled. Skipping starting application controller.") 711 return nil 712 } 713 714 // Delete existing deployment for Application Controller, if any .. 715 deploy := newDeploymentWithSuffix("application-controller", "application-controller", cr) 716 if argoutil.IsObjectFound(r.Client, deploy.Namespace, deploy.Name, deploy) { 717 if err := r.Client.Delete(context.TODO(), deploy); err != nil { 718 return err 719 } 720 } 721 722 if err := controllerutil.SetControllerReference(cr, ss, r.Scheme); err != nil { 723 return err 724 } 725 return r.Client.Create(context.TODO(), ss) 726 } 727 728 // reconcileStatefulSets will ensure that all StatefulSets are present for the given ArgoCD. 729 func (r *ReconcileArgoCD) reconcileStatefulSets(cr *argoproj.ArgoCD, useTLSForRedis bool) error { 730 if err := r.reconcileApplicationControllerStatefulSet(cr, useTLSForRedis); err != nil { 731 return err 732 } 733 if err := r.reconcileRedisStatefulSet(cr); err != nil { 734 return err 735 } 736 return nil 737 } 738 739 // triggerStatefulSetRollout will update the label with the given key to trigger a new rollout of the StatefulSet. 740 func (r *ReconcileArgoCD) triggerStatefulSetRollout(sts *appsv1.StatefulSet, key string) error { 741 if !argoutil.IsObjectFound(r.Client, sts.Namespace, sts.Name, sts) { 742 log.Info(fmt.Sprintf("unable to locate deployment with name: %s", sts.Name)) 743 return nil 744 } 745 746 sts.Spec.Template.ObjectMeta.Labels[key] = nowNano() 747 return r.Client.Update(context.TODO(), sts) 748 } 749 750 // to update nodeSelector and tolerations in reconciler 751 func updateNodePlacementStateful(existing *appsv1.StatefulSet, ss *appsv1.StatefulSet, changed *bool) { 752 if !reflect.DeepEqual(existing.Spec.Template.Spec.NodeSelector, ss.Spec.Template.Spec.NodeSelector) { 753 existing.Spec.Template.Spec.NodeSelector = ss.Spec.Template.Spec.NodeSelector 754 *changed = true 755 } 756 if !reflect.DeepEqual(existing.Spec.Template.Spec.Tolerations, ss.Spec.Template.Spec.Tolerations) { 757 existing.Spec.Template.Spec.Tolerations = ss.Spec.Template.Spec.Tolerations 758 *changed = true 759 } 760 } 761 762 // Returns true if a StatefulSet has pods in ErrImagePull or ImagePullBackoff state. 763 // These pods cannot be restarted automatially due to known kubernetes issue https://github.com/kubernetes/kubernetes/issues/67250 764 func containsInvalidImage(cr *argoproj.ArgoCD, r *ReconcileArgoCD) bool { 765 766 brokenPod := false 767 768 podList := &corev1.PodList{} 769 listOption := client.MatchingLabels{common.ArgoCDKeyName: fmt.Sprintf("%s-%s", cr.Name, "application-controller")} 770 771 if err := r.Client.List(context.TODO(), podList, listOption); err != nil { 772 log.Error(err, "Failed to list Pods") 773 } 774 if len(podList.Items) > 0 { 775 if len(podList.Items[0].Status.ContainerStatuses) > 0 { 776 if podList.Items[0].Status.ContainerStatuses[0].State.Waiting != nil && (podList.Items[0].Status.ContainerStatuses[0].State.Waiting.Reason == "ImagePullBackOff" || podList.Items[0].Status.ContainerStatuses[0].State.Waiting.Reason == "ErrImagePull") { 777 brokenPod = true 778 } 779 } 780 } 781 return brokenPod 782 }