github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocdexport/job.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 argocdexport 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 22 "sigs.k8s.io/controller-runtime/pkg/client" 23 24 batchv1 "k8s.io/api/batch/v1" 25 corev1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 28 29 argoproj "github.com/argoproj-labs/argocd-operator/api/v1alpha1" 30 "github.com/argoproj-labs/argocd-operator/common" 31 "github.com/argoproj-labs/argocd-operator/controllers/argocd" 32 "github.com/argoproj-labs/argocd-operator/controllers/argoutil" 33 ) 34 35 // getArgoExportCommand will return the command for the ArgoCD export process. 36 func getArgoExportCommand(cr *argoproj.ArgoCDExport) []string { 37 cmd := make([]string, 0) 38 cmd = append(cmd, "uid_entrypoint.sh") 39 cmd = append(cmd, "argocd-operator-util") 40 cmd = append(cmd, "export") 41 cmd = append(cmd, cr.Spec.Storage.Backend) 42 return cmd 43 } 44 45 func getArgoExportContainerEnv(cr *argoproj.ArgoCDExport) []corev1.EnvVar { 46 env := make([]corev1.EnvVar, 0) 47 48 switch cr.Spec.Storage.Backend { 49 case common.ArgoCDExportStorageBackendAWS: 50 env = append(env, corev1.EnvVar{ 51 Name: "AWS_ACCESS_KEY_ID", 52 ValueFrom: &corev1.EnvVarSource{ 53 SecretKeyRef: &corev1.SecretKeySelector{ 54 LocalObjectReference: corev1.LocalObjectReference{ 55 Name: argoutil.FetchStorageSecretName(cr), 56 }, 57 Key: "aws.access.key.id", 58 }, 59 }, 60 }) 61 62 env = append(env, corev1.EnvVar{ 63 Name: "AWS_SECRET_ACCESS_KEY", 64 ValueFrom: &corev1.EnvVarSource{ 65 SecretKeyRef: &corev1.SecretKeySelector{ 66 LocalObjectReference: corev1.LocalObjectReference{ 67 Name: argoutil.FetchStorageSecretName(cr), 68 }, 69 Key: "aws.secret.access.key", 70 }, 71 }, 72 }) 73 } 74 75 return env 76 } 77 78 // getArgoExportContainerImage will return the container image for ArgoCD. 79 func getArgoExportContainerImage(cr *argoproj.ArgoCDExport) string { 80 img := cr.Spec.Image 81 if len(img) <= 0 { 82 img = common.ArgoCDDefaultExportJobImage 83 } 84 85 tag := cr.Spec.Version 86 if len(tag) <= 0 { 87 tag = common.ArgoCDDefaultExportJobVersion 88 } 89 90 return argoutil.CombineImageTag(img, tag) 91 } 92 93 // getArgoExportVolumeMounts will return the VolumneMounts for the given ArgoCDExport. 94 func getArgoExportVolumeMounts() []corev1.VolumeMount { 95 mounts := make([]corev1.VolumeMount, 0) 96 97 mounts = append(mounts, corev1.VolumeMount{ 98 Name: "backup-storage", 99 MountPath: "/backups", 100 }) 101 102 mounts = append(mounts, corev1.VolumeMount{ 103 Name: "secret-storage", 104 MountPath: "/secrets", 105 }) 106 107 return mounts 108 } 109 110 // getArgoSecretVolume will return the Secret Volume for the export process. 111 func getArgoSecretVolume(name string, cr *argoproj.ArgoCDExport) corev1.Volume { 112 volume := corev1.Volume{ 113 Name: name, 114 } 115 116 volume.VolumeSource = corev1.VolumeSource{ 117 Secret: &corev1.SecretVolumeSource{ 118 SecretName: argoutil.FetchStorageSecretName(cr), 119 }, 120 } 121 122 return volume 123 } 124 125 // getArgoStorageVolume will return the storage Volume for the export process. 126 func getArgoStorageVolume(name string, cr *argoproj.ArgoCDExport) corev1.Volume { 127 volume := corev1.Volume{ 128 Name: name, 129 } 130 131 if cr.Spec.Storage == nil || strings.ToLower(cr.Spec.Storage.Backend) == common.ArgoCDExportStorageBackendLocal { 132 volume.VolumeSource = corev1.VolumeSource{ 133 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 134 ClaimName: cr.Name, 135 }, 136 } 137 } else { 138 volume.VolumeSource = corev1.VolumeSource{ 139 EmptyDir: &corev1.EmptyDirVolumeSource{}, 140 } 141 } 142 143 return volume 144 } 145 146 // newJob returns a new Job instance for the given ArgoCDExport. 147 func newJob(cr *argoproj.ArgoCDExport) *batchv1.Job { 148 return &batchv1.Job{ 149 ObjectMeta: metav1.ObjectMeta{ 150 Name: cr.Name, 151 Namespace: cr.Namespace, 152 Labels: common.DefaultLabels(cr.Name), 153 }, 154 } 155 } 156 157 // newCronJob returns a new CronJob instance for the given ArgoCDExport. 158 func newCronJob(cr *argoproj.ArgoCDExport) *batchv1.CronJob { 159 return &batchv1.CronJob{ 160 ObjectMeta: metav1.ObjectMeta{ 161 Name: cr.Name, 162 Namespace: cr.Namespace, 163 Labels: common.DefaultLabels(cr.Name), 164 }, 165 } 166 } 167 168 func newExportPodSpec(cr *argoproj.ArgoCDExport, argocdName string, client client.Client) corev1.PodSpec { 169 pod := corev1.PodSpec{} 170 171 boolPtr := func(value bool) *bool { 172 return &value 173 } 174 175 pod.Containers = []corev1.Container{{ 176 Command: getArgoExportCommand(cr), 177 Env: getArgoExportContainerEnv(cr), 178 Image: getArgoExportContainerImage(cr), 179 ImagePullPolicy: corev1.PullAlways, 180 Name: "argocd-export", 181 SecurityContext: &corev1.SecurityContext{ 182 AllowPrivilegeEscalation: boolPtr(false), 183 Capabilities: &corev1.Capabilities{ 184 Drop: []corev1.Capability{ 185 "ALL", 186 }, 187 }, 188 RunAsNonRoot: boolPtr(true), 189 }, 190 VolumeMounts: getArgoExportVolumeMounts(), 191 }} 192 193 pod.RestartPolicy = corev1.RestartPolicyOnFailure 194 pod.ServiceAccountName = fmt.Sprintf("%s-%s", argocdName, "argocd-application-controller") 195 pod.Volumes = []corev1.Volume{ 196 getArgoStorageVolume("backup-storage", cr), 197 getArgoSecretVolume("secret-storage", cr), 198 } 199 200 // Configure runAsUser, runAsGroup and fsGroup so that the job can write to the PV 201 // 999 is the uid/gid of the argocd user that the container runs as 202 id := int64(999) 203 pod.SecurityContext = &corev1.PodSecurityContext{ 204 RunAsUser: &id, 205 RunAsGroup: &id, 206 FSGroup: &id, 207 } 208 argocd.AddSeccompProfileForOpenShift(client, &pod) 209 210 return pod 211 } 212 213 func newPodTemplateSpec(cr *argoproj.ArgoCDExport, argocdName string, client client.Client) corev1.PodTemplateSpec { 214 return corev1.PodTemplateSpec{ 215 ObjectMeta: metav1.ObjectMeta{ 216 Name: cr.Name, 217 Namespace: cr.Namespace, 218 Labels: common.DefaultLabels(cr.Name), 219 }, 220 Spec: newExportPodSpec(cr, argocdName, client), 221 } 222 } 223 224 // reconcileCronJob will ensure that the CronJob for the ArgoCDExport is present. 225 func (r *ReconcileArgoCDExport) reconcileCronJob(cr *argoproj.ArgoCDExport) error { 226 if cr.Spec.Storage == nil { 227 return nil // Do nothing if storage options not set 228 } 229 230 cj := newCronJob(cr) 231 if argoutil.IsObjectFound(r.Client, cr.Namespace, cj.Name, cj) { 232 if *cr.Spec.Schedule != cj.Spec.Schedule { 233 cj.Spec.Schedule = *cr.Spec.Schedule 234 return r.Client.Update(context.TODO(), cj) 235 } 236 return nil 237 } 238 239 cj.Spec.Schedule = *cr.Spec.Schedule 240 241 // To create the job, we need the name of the argocd instance. Although the argocd export cr contains a field with 242 // the argocd instance name, it's never used anywhere, and so there may be existing argocd export resources with the 243 // wrong name. To avoid these breaking, we look up the name of the argocd instance in the namespace of the export cr. 244 argocdName, err := r.argocdName(cr.Namespace) 245 if err != nil { 246 return err 247 } 248 job := newJob(cr) 249 job.Spec.Template = newPodTemplateSpec(cr, argocdName, r.Client) 250 251 cj.Spec.JobTemplate.Spec = job.Spec 252 253 if err := controllerutil.SetControllerReference(cr, cj, r.Scheme); err != nil { 254 return err 255 } 256 return r.Client.Create(context.TODO(), cj) 257 } 258 259 // reconcileJob will ensure that the Job for the ArgoCDExport is present. 260 func (r *ReconcileArgoCDExport) reconcileJob(cr *argoproj.ArgoCDExport) error { 261 if cr.Spec.Storage == nil { 262 return nil // Do nothing if storage options not set 263 } 264 265 job := newJob(cr) 266 if argoutil.IsObjectFound(r.Client, cr.Namespace, job.Name, job) { 267 if job.Status.Succeeded > 0 && cr.Status.Phase != common.ArgoCDStatusCompleted { 268 // Mark status Phase as Complete 269 cr.Status.Phase = common.ArgoCDStatusCompleted 270 return r.Client.Status().Update(context.TODO(), cr) 271 } 272 return nil // Job not complete, move along... 273 } 274 275 // To create the job, we need the name of the argocd instance. Although the argocd export cr contains a field with 276 // the argocd instance name, it's never used anywhere, and so there may be existing argocd export resources with the 277 // wrong name. To avoid these breaking, we look up the name of the argocd instance in the namespace of the export cr. 278 argocdName, err := r.argocdName(cr.Namespace) 279 if err != nil { 280 return err 281 } 282 job.Spec.Template = newPodTemplateSpec(cr, argocdName, r.Client) 283 284 if err := controllerutil.SetControllerReference(cr, job, r.Scheme); err != nil { 285 return err 286 } 287 return r.Client.Create(context.TODO(), job) 288 } 289 290 func (r *ReconcileArgoCDExport) argocdName(namespace string) (string, error) { 291 argocds := &argoproj.ArgoCDList{} 292 if err := r.Client.List(context.TODO(), argocds, &client.ListOptions{Namespace: namespace}); err != nil { 293 return "", err 294 } 295 if len(argocds.Items) != 1 { 296 return "", fmt.Errorf("No Argo CD instance found in namespace %s", namespace) 297 } 298 argocd := argocds.Items[0] 299 return argocd.Name, nil 300 }