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  }