From 34eb426db7e606c65bb2172ad040386e37970fab Mon Sep 17 00:00:00 2001 From: Jan Lauber Date: Wed, 22 May 2024 17:46:13 +0200 Subject: [PATCH 1/2] feat: Add CronJobSpec to RolloutSpec Signed-off-by: Jan Lauber --- README.md | 23 ++++ api/v1alpha1/rollout_types.go | 14 ++ api/v1alpha1/zz_generated.deepcopy.go | 39 ++++++ config/crd/bases/one-click.dev_rollouts.yaml | 86 ++++++++++++ controllers/cronjobs.go | 130 +++++++++++++++++++ controllers/rollout_controller.go | 20 +++ 6 files changed, 312 insertions(+) create mode 100644 controllers/cronjobs.go diff --git a/README.md b/README.md index 2fd5a00..1a8b6fb 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,29 @@ spec: - host: "reflex.oneclickapps.dev" path: "/test" tls: false + cronjobs: + - name: some-bash-job + suspend: false + image: + password: "" + registry: docker.io + repository: library/busybox + tag: latest + username: "" + schedule: "*/1 * * * *" + command: ["echo", "hello"] + maxRetries: 3 + backoffLimit: 2 + env: + - name: SOME_ENV + value: "some-value" + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi serviceAccountName: "nginx" ``` diff --git a/api/v1alpha1/rollout_types.go b/api/v1alpha1/rollout_types.go index 6b35cea..7c8eec6 100644 --- a/api/v1alpha1/rollout_types.go +++ b/api/v1alpha1/rollout_types.go @@ -99,6 +99,19 @@ type IngressRule struct { TlsSecretName string `json:"tlsSecretName,omitempty"` } +type CronJobSpec struct { + Name string `json:"name"` + Suspend bool `json:"suspend"` + Image ImageSpec `json:"image"` + Schedule string `json:"schedule"` + Command []string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + MaxRetries int32 `json:"maxRetries,omitempty"` + BackoffLimit int32 `json:"backoffLimit,omitempty"` + Env []EnvVar `json:"env,omitempty"` + Resources ResourceRequirements `json:"resources"` +} + // RolloutSpec defines the desired state of Rollout type RolloutSpec struct { Args []string `json:"args,omitempty"` @@ -112,6 +125,7 @@ type RolloutSpec struct { Volumes []VolumeSpec `json:"volumes,omitempty"` Interfaces []InterfaceSpec `json:"interfaces,omitempty"` ServiceAccountName string `json:"serviceAccountName"` + CronJobs []CronJobSpec `json:"cronjobs,omitempty"` } type Resources struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index fa8e636..0303257 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -50,6 +50,38 @@ func (in *CapabilitiesSpec) DeepCopy() *CapabilitiesSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CronJobSpec) DeepCopyInto(out *CronJobSpec) { + *out = *in + out.Image = in.Image + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]EnvVar, len(*in)) + copy(*out, *in) + } + out.Resources = in.Resources +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobSpec. +func (in *CronJobSpec) DeepCopy() *CronJobSpec { + if in == nil { + return nil + } + out := new(CronJobSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentResources) DeepCopyInto(out *DeploymentResources) { *out = *in @@ -356,6 +388,13 @@ func (in *RolloutSpec) DeepCopyInto(out *RolloutSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.CronJobs != nil { + in, out := &in.CronJobs, &out.CronJobs + *out = make([]CronJobSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutSpec. diff --git a/config/crd/bases/one-click.dev_rollouts.yaml b/config/crd/bases/one-click.dev_rollouts.yaml index 9fee3a6..569c954 100644 --- a/config/crd/bases/one-click.dev_rollouts.yaml +++ b/config/crd/bases/one-click.dev_rollouts.yaml @@ -58,6 +58,92 @@ spec: items: type: string type: array + cronjobs: + items: + properties: + args: + items: + type: string + type: array + backoffLimit: + format: int32 + type: integer + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + image: + properties: + password: + type: string + registry: + type: string + repository: + type: string + tag: + type: string + username: + type: string + required: + - registry + - repository + - tag + type: object + maxRetries: + format: int32 + type: integer + name: + type: string + resources: + properties: + limits: + properties: + cpu: + type: string + memory: + type: string + required: + - cpu + - memory + type: object + requests: + properties: + cpu: + type: string + memory: + type: string + required: + - cpu + - memory + type: object + required: + - limits + - requests + type: object + schedule: + type: string + suspend: + type: boolean + required: + - image + - name + - resources + - schedule + - suspend + type: object + type: array env: items: properties: diff --git a/controllers/cronjobs.go b/controllers/cronjobs.go new file mode 100644 index 0000000..021144d --- /dev/null +++ b/controllers/cronjobs.go @@ -0,0 +1,130 @@ +package controllers + +import ( + "context" + "fmt" + "reflect" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + oneclickiov1alpha1 "github.com/janlauber/one-click-operator/api/v1alpha1" +) + +func (r *RolloutReconciler) reconcileCronJobs(ctx context.Context, instance *oneclickiov1alpha1.Rollout) error { + log := log.FromContext(ctx) + + // Track the CronJobs defined in the Rollout spec + definedCronJobs := make(map[string]oneclickiov1alpha1.CronJobSpec) + for _, cronJobSpec := range instance.Spec.CronJobs { + definedCronJobs[cronJobSpec.Name] = cronJobSpec + cronJob := &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: cronJobSpec.Name, + Namespace: instance.Namespace, + }, + Spec: batchv1.CronJobSpec{ + Suspend: &cronJobSpec.Suspend, + Schedule: cronJobSpec.Schedule, + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: cronJobSpec.Name, + Image: fmt.Sprintf("%s/%s:%s", cronJobSpec.Image.Registry, cronJobSpec.Image.Repository, cronJobSpec.Image.Tag), + Command: cronJobSpec.Command, + Args: cronJobSpec.Args, + Env: getEnvVars(cronJobSpec.Env), + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(cronJobSpec.Resources.Limits.CPU), + corev1.ResourceMemory: resource.MustParse(cronJobSpec.Resources.Limits.Memory), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(cronJobSpec.Resources.Requests.CPU), + corev1.ResourceMemory: resource.MustParse(cronJobSpec.Resources.Requests.Memory), + }, + }, + }, + }, + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + }, + BackoffLimit: &cronJobSpec.BackoffLimit, + }, + }, + }, + } + + // Set Rollout instance as the owner and controller + if err := ctrl.SetControllerReference(instance, cronJob, r.Scheme); err != nil { + return err + } + + // Check if this CronJob already exists + found := &batchv1.CronJob{} + err := r.Get(ctx, types.NamespacedName{Name: cronJob.Name, Namespace: cronJob.Namespace}, found) + if err != nil && errors.IsNotFound(err) { + log.Info("Creating a new CronJob", "CronJob.Namespace", cronJob.Namespace, "CronJob.Name", cronJob.Name) + err = r.Create(ctx, cronJob) + if err != nil { + return err + } + } else if err != nil { + return err + } else { + // Update existing CronJob if needed + if needsUpdateCronJob(found, cronJob) { + found.Spec = cronJob.Spec + log.Info("Updating existing CronJob", "CronJob.Namespace", cronJob.Namespace, "CronJob.Name", cronJob.Name) + err = r.Update(ctx, found) + if err != nil { + return err + } + } + } + } + + // List all CronJobs in the namespace to find any that are not defined in the Rollout spec + var existingCronJobs batchv1.CronJobList + if err := r.List(ctx, &existingCronJobs, client.InNamespace(instance.Namespace), client.MatchingFields{"metadata.ownerReferences.uid": string(instance.UID)}); err != nil { + return err + } + + for _, existingCronJob := range existingCronJobs.Items { + if _, exists := definedCronJobs[existingCronJob.Name]; !exists { + // CronJob is not defined in the Rollout spec, so delete it + log.Info("Deleting CronJob not defined in Rollout spec", "CronJob.Namespace", existingCronJob.Namespace, "CronJob.Name", existingCronJob.Name) + if err := r.Delete(ctx, &existingCronJob); err != nil { + return err + } + } + } + + return nil +} + +// Helper function to check if the CronJob needs to be updated +func needsUpdateCronJob(current *batchv1.CronJob, desired *batchv1.CronJob) bool { + return !reflect.DeepEqual(current.Spec, desired.Spec) +} + +// func getEnvVars(envVars []oneclickiov1alpha1.EnvVar) []corev1.EnvVar { +// var envs []corev1.EnvVar +// for _, env := range envVars { +// envs = append(envs, corev1.EnvVar{ +// Name: env.Name, +// Value: env.Value, +// }) +// } +// return envs +// } diff --git a/controllers/rollout_controller.go b/controllers/rollout_controller.go index 648ee16..999b589 100644 --- a/controllers/rollout_controller.go +++ b/controllers/rollout_controller.go @@ -21,6 +21,7 @@ import ( appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -123,6 +124,12 @@ func (r *RolloutReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, err } + // Reconcile CronJobs + if err := r.reconcileCronJobs(ctx, &rollout); err != nil { + log.Error(err, "Failed to reconcile CronJobs.") + return ctrl.Result{}, err + } + // Update status if err := r.updateStatus(ctx, &rollout); err != nil { if errors.IsConflict(err) { @@ -138,6 +145,18 @@ func (r *RolloutReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // SetupWithManager sets up the controller with the Manager. func (r *RolloutReconciler) SetupWithManager(mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &batchv1.CronJob{}, "metadata.ownerReferences.uid", func(rawObj client.Object) []string { + cronJob := rawObj.(*batchv1.CronJob) + ownerRefs := cronJob.GetOwnerReferences() + ownerUIDs := make([]string, len(ownerRefs)) + for i, ownerRef := range ownerRefs { + ownerUIDs[i] = string(ownerRef.UID) + } + return ownerUIDs + }); err != nil { + return err + } + return ctrl.NewControllerManagedBy(mgr). For(&oneclickiov1alpha1.Rollout{}). Owns(&appsv1.Deployment{}). @@ -147,5 +166,6 @@ func (r *RolloutReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.PersistentVolumeClaim{}). Owns(&autoscalingv2.HorizontalPodAutoscaler{}). Owns(&corev1.ServiceAccount{}). + Owns(&batchv1.CronJob{}). Complete(r) } From b88b913a7b330bb8f339532132914c21e5bfe1a9 Mon Sep 17 00:00:00 2001 From: Jan Lauber Date: Wed, 22 May 2024 19:26:55 +0200 Subject: [PATCH 2/2] feat: Add helper functions for creating resource requirements and reconciling image pull secrets Signed-off-by: Jan Lauber --- controllers/cronjobs.go | 47 +++++------ controllers/deployment.go | 163 ++++---------------------------------- controllers/helpers.go | 95 ++++++++++++++++++++++ 3 files changed, 130 insertions(+), 175 deletions(-) create mode 100644 controllers/helpers.go diff --git a/controllers/cronjobs.go b/controllers/cronjobs.go index 021144d..5c58c7d 100644 --- a/controllers/cronjobs.go +++ b/controllers/cronjobs.go @@ -8,7 +8,6 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -25,6 +24,17 @@ func (r *RolloutReconciler) reconcileCronJobs(ctx context.Context, instance *one definedCronJobs := make(map[string]oneclickiov1alpha1.CronJobSpec) for _, cronJobSpec := range instance.Spec.CronJobs { definedCronJobs[cronJobSpec.Name] = cronJobSpec + + // Handle image pull secret if username and password are provided + var imagePullSecrets []corev1.LocalObjectReference + if cronJobSpec.Image.Username != "" && cronJobSpec.Image.Password != "" { + secretName := cronJobSpec.Name + "-imagepullsecret" + if err := reconcileImagePullSecret(ctx, r.Client, instance, cronJobSpec.Image, secretName, instance.Namespace); err != nil { + return err + } + imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{Name: secretName}) + } + cronJob := &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobSpec.Name, @@ -39,24 +49,16 @@ func (r *RolloutReconciler) reconcileCronJobs(ctx context.Context, instance *one Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: cronJobSpec.Name, - Image: fmt.Sprintf("%s/%s:%s", cronJobSpec.Image.Registry, cronJobSpec.Image.Repository, cronJobSpec.Image.Tag), - Command: cronJobSpec.Command, - Args: cronJobSpec.Args, - Env: getEnvVars(cronJobSpec.Env), - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(cronJobSpec.Resources.Limits.CPU), - corev1.ResourceMemory: resource.MustParse(cronJobSpec.Resources.Limits.Memory), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(cronJobSpec.Resources.Requests.CPU), - corev1.ResourceMemory: resource.MustParse(cronJobSpec.Resources.Requests.Memory), - }, - }, + Name: cronJobSpec.Name, + Image: fmt.Sprintf("%s/%s:%s", cronJobSpec.Image.Registry, cronJobSpec.Image.Repository, cronJobSpec.Image.Tag), + Command: cronJobSpec.Command, + Args: cronJobSpec.Args, + Env: getEnvVars(cronJobSpec.Env), + Resources: createResourceRequirements(cronJobSpec.Resources), }, }, - RestartPolicy: corev1.RestartPolicyOnFailure, + RestartPolicy: corev1.RestartPolicyOnFailure, + ImagePullSecrets: imagePullSecrets, }, }, BackoffLimit: &cronJobSpec.BackoffLimit, @@ -117,14 +119,3 @@ func (r *RolloutReconciler) reconcileCronJobs(ctx context.Context, instance *one func needsUpdateCronJob(current *batchv1.CronJob, desired *batchv1.CronJob) bool { return !reflect.DeepEqual(current.Spec, desired.Spec) } - -// func getEnvVars(envVars []oneclickiov1alpha1.EnvVar) []corev1.EnvVar { -// var envs []corev1.EnvVar -// for _, env := range envVars { -// envs = append(envs, corev1.EnvVar{ -// Name: env.Name, -// Value: env.Value, -// }) -// } -// return envs -// } diff --git a/controllers/deployment.go b/controllers/deployment.go index b079654..ec261ad 100644 --- a/controllers/deployment.go +++ b/controllers/deployment.go @@ -2,15 +2,12 @@ package controllers import ( "context" - "encoding/base64" - "encoding/json" "fmt" "reflect" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" @@ -69,6 +66,17 @@ func (r *RolloutReconciler) deploymentForRollout(ctx context.Context, f *oneclic "one-click.dev/deploymentId": f.Name, } + // Handle image pull secret if username and password are provided + var imagePullSecrets []corev1.LocalObjectReference + if f.Spec.Image.Username != "" && f.Spec.Image.Password != "" { + secretName := f.Name + "-imagepullsecret" + if err := reconcileImagePullSecret(ctx, r.Client, f, f.Spec.Image, secretName, f.Namespace); err != nil { + r.Recorder.Eventf(f, corev1.EventTypeWarning, "ImagePullSecretFailed", "Failed to reconcile Image Pull Secret %s", secretName) + } else { + imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{Name: secretName}) + } + } + dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: f.Name, @@ -85,21 +93,13 @@ func (r *RolloutReconciler) deploymentForRollout(ctx context.Context, f *oneclic }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ - Name: f.Name, - Image: fmt.Sprintf("%s/%s:%s", f.Spec.Image.Registry, f.Spec.Image.Repository, f.Spec.Image.Tag), - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(f.Spec.Resources.Requests.CPU), - corev1.ResourceMemory: resource.MustParse(f.Spec.Resources.Requests.Memory), - }, - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(f.Spec.Resources.Limits.CPU), - corev1.ResourceMemory: resource.MustParse(f.Spec.Resources.Limits.Memory), - }, - }, - Env: getEnvVars(f.Spec.Env), + Name: f.Name, + Image: fmt.Sprintf("%s/%s:%s", f.Spec.Image.Registry, f.Spec.Image.Repository, f.Spec.Image.Tag), + Resources: createResourceRequirements(f.Spec.Resources), + Env: getEnvVars(f.Spec.Env), }}, ServiceAccountName: f.Spec.ServiceAccountName, + ImagePullSecrets: imagePullSecrets, }, }, }, @@ -158,30 +158,6 @@ func (r *RolloutReconciler) deploymentForRollout(ctx context.Context, f *oneclic } } - // Check if Username and Password are provided - if f.Spec.Image.Username != "" && f.Spec.Image.Password != "" { - // Logic to create or get existing secret - secretName := f.Name + "-imagepullsecret" - if err := r.reconcileImagePullSecret(ctx, f, secretName); err != nil { - r.Recorder.Eventf(f, corev1.EventTypeWarning, "ImagePullSecretFailed", "Failed to reconcile Image Pull Secret %s", secretName) - } - - // Attach the image pull secret to the deployment - dep.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{ - { - Name: secretName, - }, - } - } else { - // Remove image pull secret from deployment - dep.Spec.Template.Spec.ImagePullSecrets = nil - // Logic to create or get existing secret - secretName := f.Name + "-imagepullsecret" - if err := r.reconcileImagePullSecret(ctx, f, secretName); err != nil { - r.Recorder.Eventf(f, corev1.EventTypeWarning, "ImagePullSecretFailed", "Failed to reconcile Image Pull Secret %s", secretName) - } - } - // Update volumes and volume mounts if len(f.Spec.Volumes) > 0 { var volumes []corev1.Volume @@ -219,23 +195,7 @@ func (r *RolloutReconciler) deploymentForRollout(ctx context.Context, f *oneclic return dep } -func getEnvVars(envVars []oneclickiov1alpha1.EnvVar) []corev1.EnvVar { - var envs []corev1.EnvVar - for _, env := range envVars { - envs = append(envs, corev1.EnvVar{ - Name: env.Name, - Value: env.Value, - }) - } - return envs -} - func needsUpdate(current *appsv1.Deployment, f *oneclickiov1alpha1.Rollout) bool { - // Check replicas - // if *current.Spec.Replicas != int32(f.Spec.HorizontalScale.MinReplicas) { - // return true - // } - // Check security context if !reflect.DeepEqual(current.Spec.Template.Spec.SecurityContext, &corev1.PodSecurityContext{ FSGroup: &f.Spec.SecurityContext.FsGroup, @@ -357,94 +317,3 @@ func portsMatch(currentPorts []corev1.ContainerPort, interfaces []oneclickiov1al return true } - -func createResourceRequirements(resources oneclickiov1alpha1.ResourceRequirements) corev1.ResourceRequirements { - return corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(resources.Requests.CPU), - corev1.ResourceMemory: resource.MustParse(resources.Requests.Memory), - }, - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(resources.Limits.CPU), - corev1.ResourceMemory: resource.MustParse(resources.Limits.Memory), - }, - } -} - -type dockerConfigEntry struct { - Username string `json:"username"` - Password string `json:"password"` - Auth string `json:"auth"` -} - -type dockerConfigJSON struct { - Auths map[string]dockerConfigEntry `json:"auths"` -} - -func (r *RolloutReconciler) reconcileImagePullSecret(ctx context.Context, rollout *oneclickiov1alpha1.Rollout, secretName string) error { - registry := rollout.Spec.Image.Registry - username := rollout.Spec.Image.Username - password := rollout.Spec.Image.Password - - // Check if username and password are empty, delete the secret if it exists - if username == "" && password == "" { - foundSecret := &corev1.Secret{} - err := r.Get(ctx, types.NamespacedName{Name: secretName, Namespace: rollout.Namespace}, foundSecret) - if err == nil { - if metav1.IsControlledBy(foundSecret, rollout) { - return r.Delete(ctx, foundSecret) - } - } - return nil - } - - auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) - dockerConfigEntryData := dockerConfigEntry{ - Username: username, - Password: password, - Auth: auth, - } - dockerConfigJSON := dockerConfigJSON{ - Auths: map[string]dockerConfigEntry{ - registry: dockerConfigEntryData, - }, - } - - dockerConfigJSONBytes, err := json.Marshal(dockerConfigJSON) - if err != nil { - return err - } - - secretData := map[string][]byte{ - ".dockerconfigjson": dockerConfigJSONBytes, - } - - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: rollout.Namespace, - OwnerReferences: []metav1.OwnerReference{ - *metav1.NewControllerRef(rollout, rollout.GroupVersionKind()), - }, - }, - Type: corev1.SecretTypeDockerConfigJson, - Data: secretData, - } - - // Check if secret already exists, if not, create it - foundSecret := &corev1.Secret{} - err = r.Get(ctx, types.NamespacedName{Name: secretName, Namespace: rollout.Namespace}, foundSecret) - if err != nil && errors.IsNotFound(err) { - // Secret not found, create it - return r.Create(ctx, secret) - } else if err != nil { - // Error occurred while checking for secret - return err - } - - // Secret found, update it - return retry.RetryOnConflict(retry.DefaultRetry, func() error { - foundSecret.Data = secretData - return r.Update(ctx, foundSecret) - }) -} diff --git a/controllers/helpers.go b/controllers/helpers.go new file mode 100644 index 0000000..1f38b9e --- /dev/null +++ b/controllers/helpers.go @@ -0,0 +1,95 @@ +package controllers + +import ( + "context" + "encoding/base64" + "encoding/json" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + oneclickiov1alpha1 "github.com/janlauber/one-click-operator/api/v1alpha1" +) + +func getEnvVars(envVars []oneclickiov1alpha1.EnvVar) []corev1.EnvVar { + var envs []corev1.EnvVar + for _, env := range envVars { + envs = append(envs, corev1.EnvVar{ + Name: env.Name, + Value: env.Value, + }) + } + return envs +} + +func createResourceRequirements(resources oneclickiov1alpha1.ResourceRequirements) corev1.ResourceRequirements { + return corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(resources.Requests.CPU), + corev1.ResourceMemory: resource.MustParse(resources.Requests.Memory), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(resources.Limits.CPU), + corev1.ResourceMemory: resource.MustParse(resources.Limits.Memory), + }, + } +} + +func reconcileImagePullSecret(ctx context.Context, client client.Client, instance *oneclickiov1alpha1.Rollout, imageSpec oneclickiov1alpha1.ImageSpec, secretName string, namespace string) error { + auth := base64.StdEncoding.EncodeToString([]byte(imageSpec.Username + ":" + imageSpec.Password)) + dockerConfigEntry := map[string]interface{}{ + "username": imageSpec.Username, + "password": imageSpec.Password, + "auth": auth, + } + dockerConfigJSON := map[string]interface{}{ + "auths": map[string]interface{}{ + imageSpec.Registry: dockerConfigEntry, + }, + } + dockerConfigJSONBytes, err := json.Marshal(dockerConfigJSON) + if err != nil { + return err + } + + secretData := map[string][]byte{ + ".dockerconfigjson": dockerConfigJSONBytes, + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: secretData, + } + + // Set the owner reference + if err := controllerutil.SetControllerReference(instance, secret, client.Scheme()); err != nil { + return err + } + + // Check if secret already exists, if not, create it + foundSecret := &corev1.Secret{} + err = client.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, foundSecret) + if err != nil && errors.IsNotFound(err) { + // Secret not found, create it + return client.Create(ctx, secret) + } else if err != nil { + // Error occurred while checking for secret + return err + } + + // Secret found, update it + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + foundSecret.Data = secretData + return client.Update(ctx, foundSecret) + }) +}