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  }