/* Copyright © 2022 - 2025 SUSE LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controllers import ( "bytes" "context" "encoding/base64" "fmt" "path/filepath" "strings" "time" "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" errorutils "k8s.io/apimachinery/pkg/util/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" managementv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" "github.com/rancher/wrangler/v2/pkg/randomtoken" "github.com/rancher/yip/pkg/schema" elementalv1 "github.com/rancher/elemental-operator/api/v1beta1" "github.com/rancher/elemental-operator/pkg/util" ) type SeedImageReconciler struct { client.Client SeedImageImage string SeedImageImagePullPolicy corev1.PullPolicy } const ( configMapKeyRegistration = "registration" configMapKeyCloudConfig = "cloud-config" configMapKeyResetConfig = "reset-config" configMapKeyDevice = "device" configMapKeyRegistrationURL = "registration-url" configMapKeyBaseImage = "base-image" configMapKeyOutputName = "output-name" ) // +kubebuilder:rbac:groups=elemental.cattle.io,resources=seedimages,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=elemental.cattle.io,resources=seedimages/status,verbs=get;update;patch // +kubebuilder:rbac:groups=elemental.cattle.io,resources=machineregistrations,verbs=get;watch;list // TODO: restrict access to resources to the required namespace only: // https://github.com/rancher/elemental-operator/issues/457 // +kubebuilder:rbac:groups="",namespace=fleet-default,resources=pods,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="",namespace=fleet-default,resources=pods/status,verbs=get // +kubebuilder:rbac:groups="",namespace=fleet-default,resources=services,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="",namespace=fleet-default,resources=services/status,verbs=get // +kubebuilder:rbac:groups="",namespace=fleet-default,resources=configmaps,verbs=get;list;watch;create;update;patch;delete // TODO: extend SetupWithManager with "Watches" and "WithEventFilter" func (r *SeedImageReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&elementalv1.SeedImage{}). Owns(&corev1.Pod{}). Complete(r) } func (r *SeedImageReconciler) Reconcile(ctx context.Context, req reconcile.Request) (ctrl.Result, error) { logger := ctrl.LoggerFrom(ctx) seedImg := &elementalv1.SeedImage{} if err := r.Get(ctx, req.NamespacedName, seedImg); err != nil { if apierrors.IsNotFound(err) { logger.V(5).Info("Object was not found, not an error") return reconcile.Result{}, nil } return reconcile.Result{}, fmt.Errorf("failed to get seedimage object: %w", err) } patchBase := client.MergeFrom(seedImg.DeepCopy()) // Collect errors as an aggregate to return together after all patches have been performed. var errs []error result, err := r.reconcile(ctx, seedImg) if err != nil { errs = append(errs, fmt.Errorf("error reconciling seedimage object: %w", err)) } seedImgStatusCopy := seedImg.Status.DeepCopy() // Patch call will erase the status if err := r.Patch(ctx, seedImg, patchBase); err != nil && !apierrors.IsNotFound(err) { errs = append(errs, fmt.Errorf("failed to patch seedimage object: %w", err)) } seedImg.Status = *seedImgStatusCopy if err := r.Status().Patch(ctx, seedImg, patchBase); err != nil && !apierrors.IsNotFound(err) { errs = append(errs, fmt.Errorf("failed to patch status for seedimage object: %w", err)) } if len(errs) > 0 { return ctrl.Result{}, errorutils.NewAggregate(errs) } return result, nil } func (r *SeedImageReconciler) reconcile(ctx context.Context, seedImg *elementalv1.SeedImage) (ctrl.Result, error) { logger := ctrl.LoggerFrom(ctx) logger.Info("Reconciling seedimage object") // RetriggerBuild resets the conditions and the buider pod: the controller // will reconcile anew all the resources in the following loop if seedImg.Spec.RetriggerBuild { seedImg.Status.Conditions = []metav1.Condition{} seedImg.Spec.RetriggerBuild = false return ctrl.Result{}, r.deleteChildResources(ctx, seedImg) } // Init the Ready condition as we want it to be the first one displayed if readyCond := meta.FindStatusCondition(seedImg.Status.Conditions, elementalv1.ReadyCondition); readyCond == nil { meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.ReadyCondition, Reason: elementalv1.ResourcesNotCreatedYet, Status: metav1.ConditionUnknown, }) } mRegistration := &elementalv1.MachineRegistration{} if err := r.reconcileSeedImageOwner(ctx, seedImg, mRegistration); err != nil { errMsg := fmt.Errorf("failed to set seedimage owner: %w", err) meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.ReadyCondition, Status: metav1.ConditionFalse, Reason: elementalv1.SetOwnerFailureReason, Message: errMsg.Error(), }) return ctrl.Result{}, errMsg } if err := r.reconcileBuildImagePod(ctx, seedImg, mRegistration); err != nil { meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.ReadyCondition, Status: metav1.ConditionFalse, Reason: elementalv1.PodCreationFailureReason, Message: err.Error(), }) return ctrl.Result{}, fmt.Errorf("failed to reconcile pod: %w", err) } if err := r.createBuildImageService(ctx, seedImg); err != nil { meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.ReadyCondition, Status: metav1.ConditionFalse, Reason: elementalv1.ServiceCreationFailureReason, Message: err.Error(), }) return ctrl.Result{}, fmt.Errorf("failed to create service: %w", err) } meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.ReadyCondition, Reason: elementalv1.ResourcesSuccessfullyCreatedReason, Status: metav1.ConditionTrue, Message: "resources created successfully", }) return ctrl.Result{}, nil } func (r *SeedImageReconciler) reconcileSeedImageOwner(ctx context.Context, seedImg *elementalv1.SeedImage, mRegistration *elementalv1.MachineRegistration) error { if err := r.Get(ctx, types.NamespacedName{ Name: seedImg.Spec.MachineRegistrationRef.Name, Namespace: seedImg.Spec.MachineRegistrationRef.Namespace, }, mRegistration); err != nil { return err } for _, o := range seedImg.OwnerReferences { if o.UID == mRegistration.UID { return nil } } return controllerutil.SetOwnerReference(mRegistration, seedImg, r.Scheme()) } func (r *SeedImageReconciler) reconcileBuildImagePod(ctx context.Context, seedImg *elementalv1.SeedImage, mRegistration *elementalv1.MachineRegistration) error { logger := ctrl.LoggerFrom(ctx) logger.Info("Reconciling Pod resources") seedImgCondition := meta.FindStatusCondition(seedImg.Status.Conditions, elementalv1.SeedImageConditionReady) seedImgConditionMet := false if seedImgCondition != nil && seedImgCondition.Status == metav1.ConditionTrue { seedImgConditionMet = true } if seedImgConditionMet && seedImgCondition.Reason == elementalv1.SeedImageBuildDeadline { logger.V(5).Info("Seed image deadline passed, skip") return nil } if err := r.reconcileConfigMapObject(ctx, seedImg, mRegistration); err != nil { return err } podName := seedImg.Name podNamespace := seedImg.Namespace podBaseImg := seedImg.Spec.BaseImage foundPod := &corev1.Pod{} err := r.Get(ctx, types.NamespacedName{Name: podName, Namespace: podNamespace}, foundPod) if err != nil && !apierrors.IsNotFound(err) { return err } if err == nil { logger.V(5).Info("Pod already there") // ensure the pod was created by us if !util.IsObjectOwned(&foundPod.ObjectMeta, seedImg.UID) { return fmt.Errorf("pod already exists and was not created by this controller") } // pod is already there and with the right configuration if foundPod.Annotations["elemental.cattle.io/base-image"] == podBaseImg { return r.updateStatusFromPod(ctx, seedImg, foundPod) } // Pod is not up-to-date: delete and recreate it if err := r.Delete(ctx, foundPod); err != nil { return fmt.Errorf("failed to delete old pod: %w", err) } } if seedImgConditionMet { logger.V(5).Info("SeedImageReady condition is met: skip Pod creation") return nil } // apierrors.IsNotFound(err) OR we just deleted the old Pod logger.V(5).Info("Creating pod") pod := fillBuildImagePod(seedImg, r.SeedImageImage, r.SeedImageImagePullPolicy) if err := controllerutil.SetControllerReference(seedImg, pod, r.Scheme()); err != nil { meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageBuildNotStartedReason, Message: err.Error(), }) return err } if err := r.Create(ctx, pod); err != nil && !apierrors.IsAlreadyExists(err) { meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageBuildNotStartedReason, Message: err.Error(), }) return err } meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageBuildOngoingReason, Message: "seed image build started", }) return nil } func (r *SeedImageReconciler) reconcileConfigMapObject(ctx context.Context, seedImg *elementalv1.SeedImage, mRegistration *elementalv1.MachineRegistration) error { logger := ctrl.LoggerFrom(ctx) logger.Info("Reconciling ConfigMap resources") podConfigMap := &corev1.ConfigMap{} regClientConf, err := mRegistration.GetClientRegistrationConfig(util.GetRancherCACert(ctx, r)) if err != nil { return fmt.Errorf("failed processing registration config: %w", err) } regData, err := yaml.Marshal(regClientConf) if err != nil { return fmt.Errorf("failed marshalling registration config: %w", err) } resetConfig, err := serializeResetRegistrationYaml(regClientConf) if err != nil { return fmt.Errorf("failed marshalling reset machine registration: %w", err) } cloudConfig, err := util.MarshalCloudConfig(seedImg.Spec.CloudConfig) if err != nil { return fmt.Errorf("failed marshalling cloud-config: %w", err) } outputName := fmt.Sprintf("%s-%s.%s", mRegistration.Name, time.Now().Format(time.RFC3339), seedImg.Spec.Type) if err := r.Get(ctx, types.NamespacedName{ Name: seedImg.Name, Namespace: seedImg.Namespace, }, podConfigMap); err != nil { if !apierrors.IsNotFound(err) { return fmt.Errorf("failed fetching config map: %w", err) } } else { // ConfigMap is already there... if bytes.Equal(regData, podConfigMap.BinaryData["registration"]) && bytes.Equal(resetConfig, podConfigMap.BinaryData["reset-config"]) && bytes.Equal(cloudConfig, podConfigMap.BinaryData["cloud-config"]) { logger.V(5).Info("ConfigMap is up-to-date") return nil } logger.V(5).Info("ConfigMap is out of date", "configmap", podConfigMap.Namespace+"/"+podConfigMap.Name) outputName = podConfigMap.Data[configMapKeyOutputName] // ...but values are not up-to-date if err := r.Delete(ctx, podConfigMap); err != nil { return fmt.Errorf("failed to delete old config map: %w", err) } } // (re)create the configmap logger.V(5).Info("Create ConfigMap", "seedimage", seedImg.Namespace+"/"+seedImg.Name) binaryData := map[string][]byte{ configMapKeyRegistration: regData, configMapKeyCloudConfig: cloudConfig, configMapKeyResetConfig: resetConfig, } data := map[string]string{ configMapKeyRegistrationURL: mRegistration.Status.RegistrationURL, configMapKeyDevice: mRegistration.Spec.Config.Elemental.Install.Device, configMapKeyBaseImage: seedImg.Spec.BaseImage, configMapKeyOutputName: outputName, } conf := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: seedImg.Name, Namespace: seedImg.Namespace, }, BinaryData: binaryData, Data: data, } if err := controllerutil.SetControllerReference(seedImg, conf, r.Scheme()); err != nil { return fmt.Errorf("failed setting configmap ownership: %w", err) } if err := r.Create(ctx, conf); err != nil { return fmt.Errorf("failed to create registration config map: %w", err) } return nil } func (r *SeedImageReconciler) updateStatusFromPod(ctx context.Context, seedImg *elementalv1.SeedImage, foundPod *corev1.Pod) error { podName := seedImg.Name podNamespace := seedImg.Namespace logger := ctrl.LoggerFrom(ctx) if !foundPod.DeletionTimestamp.IsZero() { logger.V(5).Info("Wait the builder Pod to terminate") meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageBuildNotStartedReason, Message: "wait old builder Pod termination", }) return nil } // no need to reconcile if meta.IsStatusConditionTrue(seedImg.Status.Conditions, elementalv1.SeedImageConditionReady) && foundPod.Status.Phase != corev1.PodSucceeded { return nil } logger.V(5).Info("Sync SeedImage Status from builder Pod", "pod-phase", foundPod.Status.Phase) switch foundPod.Status.Phase { case corev1.PodPending: meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageBuildOngoingReason, Message: "seed image build ongoing", }) return nil case corev1.PodRunning: rancherURL, err := r.getRancherServerAddress(ctx) if err != nil { errMsg := fmt.Errorf("failed to get Rancher Server Address: %w", err) meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageExposeFailureReason, Message: errMsg.Error(), }) return errMsg } // Let's check here we have an associated Service, so the iso could be downloaded if err := r.Get(ctx, types.NamespacedName{Name: podName, Namespace: podNamespace}, &corev1.Service{}); err != nil { errMsg := fmt.Errorf("failed to get associated service: %w", err) meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageExposeFailureReason, Message: errMsg.Error(), }) return errMsg } token, err := randomtoken.Generate() if err != nil { errMsg := fmt.Errorf("failed to generate registration token: %s", err.Error()) meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageExposeFailureReason, Message: errMsg.Error(), }) return errMsg } podConfigMap := &corev1.ConfigMap{} if err := r.Get(ctx, types.NamespacedName{ Name: seedImg.Name, Namespace: seedImg.Namespace, }, podConfigMap); err != nil { return fmt.Errorf("getting ConfigMap '%s': %w", seedImg.Name, err) } outputName, found := podConfigMap.Data[configMapKeyOutputName] if !found { return fmt.Errorf("Could not find '%s' value in ConfigMap '%s'", configMapKeyOutputName, seedImg.Name) } seedImg.Status.DownloadToken = token seedImg.Status.DownloadURL = fmt.Sprintf("https://%s/elemental/seedimage/%s/%s", rancherURL, token, outputName) seedImg.Status.ChecksumURL = fmt.Sprintf("%s.sha256", seedImg.Status.DownloadURL) meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionTrue, Reason: elementalv1.SeedImageBuildSuccessReason, Message: "seed image iso available", }) return nil case corev1.PodFailed: meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageBuildFailureReason, Message: "pod failed", }) return nil case corev1.PodSucceeded: if err := r.Delete(ctx, foundPod); err != nil { errMsg := fmt.Errorf("failed to delete builder pod: %w", err) meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionFalse, Reason: elementalv1.SeedImageBuildDeadline, Message: errMsg.Error(), }) return errMsg } seedImg.Status.DownloadURL = "" seedImg.Status.ChecksumURL = "" meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionTrue, Reason: elementalv1.SeedImageBuildDeadline, Message: "seed image deadline elapsed", }) return nil default: meta.SetStatusCondition(&seedImg.Status.Conditions, metav1.Condition{ Type: elementalv1.SeedImageConditionReady, Status: metav1.ConditionUnknown, Reason: elementalv1.SeedImageBuildUnknown, Message: fmt.Sprintf("pod phase %s", foundPod.Status.Phase), }) return nil } } func (r *SeedImageReconciler) deleteChildResources(ctx context.Context, seedImg *elementalv1.SeedImage) error { foundPod := &corev1.Pod{} podName := seedImg.Name podNamespace := seedImg.Namespace if err := r.Get(ctx, types.NamespacedName{Name: podName, Namespace: podNamespace}, foundPod); err != nil { if !apierrors.IsNotFound(err) { return err } } else { // Pod found, delete it if !util.IsObjectOwned(&foundPod.ObjectMeta, seedImg.UID) { return fmt.Errorf("pod %s/%s doesn't belong to seedimage %s/%s", podNamespace, podName, seedImg.Namespace, seedImg.Name) } if err := r.Delete(ctx, foundPod); err != nil { return err } } seedImg.Status.DownloadURL = "" seedImg.Status.ChecksumURL = "" foundSvc := &corev1.Service{} svcName := seedImg.Name svcNamespace := seedImg.Namespace if err := r.Get(ctx, types.NamespacedName{Name: svcName, Namespace: svcNamespace}, foundSvc); err != nil { if !apierrors.IsNotFound(err) { return err } } else { if !util.IsObjectOwned(&foundSvc.ObjectMeta, seedImg.UID) { return fmt.Errorf("service %s/%s doesn't belong to seedimage %s/%s", svcNamespace, svcName, seedImg.Namespace, seedImg.Name) } if err := r.Delete(ctx, foundSvc); err != nil { return err } } return nil } func (r *SeedImageReconciler) getRancherServerAddress(ctx context.Context) (string, error) { logger := ctrl.LoggerFrom(ctx) setting := &managementv3.Setting{} if err := r.Get(ctx, types.NamespacedName{Name: "server-url"}, setting); err != nil { return "", fmt.Errorf("failed to get server url setting: %w", err) } if setting.Value == "" { err := fmt.Errorf("server-url is not set") logger.Error(err, "can't get server-url") return "", err } return strings.TrimPrefix(setting.Value, "https://"), nil } func fillBuildImagePod(seedImg *elementalv1.SeedImage, buildImg string, pullPolicy corev1.PullPolicy) *corev1.Pod { name := seedImg.Name namespace := seedImg.Namespace baseImg := seedImg.Spec.BaseImage deadline := seedImg.Spec.LifetimeMinutes configMap := name var initContainers []corev1.Container if seedImg.Spec.BuildContainer == nil { initContainers = defaultInitContainers(seedImg, buildImg, pullPolicy) } else { initContainers = userDefinedInitContainers(seedImg) } pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, Labels: map[string]string{"app.kubernetes.io/name": name}, Annotations: map[string]string{ "elemental.cattle.io/base-image": baseImg, }, }, Spec: corev1.PodSpec{ InitContainers: initContainers, Containers: []corev1.Container{ { Name: "serve", Image: buildImg, ImagePullPolicy: pullPolicy, Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: 80, }, }, Args: []string{"-d", "/srv", "-t", fmt.Sprintf("%d", deadline*60)}, VolumeMounts: []corev1.VolumeMount{ { Name: "iso-storage", MountPath: "/srv", }, }, Env: []corev1.EnvVar{ { Name: "ELEMENTAL_OUTPUT_NAME", ValueFrom: &corev1.EnvVarSource{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: seedImg.Name, }, Key: configMapKeyOutputName, }, }, }, }, }, }, RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { Name: "iso-storage", VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ SizeLimit: &seedImg.Spec.Size, }, }, }, { Name: "config-map", VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{Name: configMap}, Items: []corev1.KeyToPath{ {Key: configMapKeyRegistration, Path: "reg/livecd-cloud-config.yaml"}, {Key: configMapKeyCloudConfig, Path: "iso-config/cloud-config.yaml"}, {Key: configMapKeyResetConfig, Path: "reg/reset-config.yaml"}, }, }, }, }, }, }, } return pod } func defaultInitContainers(seedImg *elementalv1.SeedImage, buildImg string, pullPolicy corev1.PullPolicy) []corev1.Container { if seedImg.Spec.Type == elementalv1.TypeRaw { return defaultRawInitContainers(seedImg, buildImg, pullPolicy) } return defaultIsoInitContainers(seedImg, buildImg, pullPolicy) } func defaultRawInitContainers(seedImg *elementalv1.SeedImage, buildImg string, pullPolicy corev1.PullPolicy) []corev1.Container { platformArg := "" if seedImg.Spec.TargetPlatform != "" { platformArg = fmt.Sprintf("--platform %s", seedImg.Spec.TargetPlatform) } buildCommands := []string{ fmt.Sprintf(`/usr/bin/elemental \ --debug \ build-disk \ %s \ --expandable \ --deploy-command systemctl,start,elemental-register-reset.service \ --squash-no-compression \ --cloud-init /overlay/reg/reset-config.yaml,/overlay/iso-config/cloud-config.yaml \ -n elemental \ -o /iso \ --system $(ELEMENTAL_BASE_IMAGE)`, platformArg), "mv /iso/elemental.raw /iso/$(ELEMENTAL_OUTPUT_NAME)", "cd /iso && sha256sum $(ELEMENTAL_OUTPUT_NAME) > $(ELEMENTAL_OUTPUT_NAME).sha256 && cd ../", } return []corev1.Container{ { Name: "build", Image: buildImg, ImagePullPolicy: pullPolicy, Command: []string{"/bin/bash", "-c"}, Args: []string{strings.Join(buildCommands, " && ")}, VolumeMounts: []corev1.VolumeMount{ { Name: "iso-storage", MountPath: "/iso", }, { Name: "config-map", MountPath: "/overlay", }, }, Env: defaultEnvVars(seedImg.Name), }, } } func defaultIsoInitContainers(seedImg *elementalv1.SeedImage, buildImg string, pullPolicy corev1.PullPolicy) []corev1.Container { const baseIsoPath = "/iso/base.iso" containers := []corev1.Container{} buildCommands := []string{ fmt.Sprintf("xorriso -indev %s -outdev /iso/$(ELEMENTAL_OUTPUT_NAME) -map /overlay/reg/livecd-cloud-config.yaml /livecd-cloud-config.yaml -map /overlay/iso-config/cloud-config.yaml /iso-config/cloud-config.yaml -boot_image any replay", baseIsoPath), fmt.Sprintf("rm -rf %s", baseIsoPath), } if util.IsHTTP(seedImg.Spec.BaseImage) { buildCommands = append([]string{fmt.Sprintf("curl -Lo %s %s", baseIsoPath, seedImg.Spec.BaseImage)}, buildCommands...) } else { command := []string{"busybox", "sh", "-c"} args := []string{fmt.Sprintf("busybox cp /elemental-iso/*.iso %s", baseIsoPath)} image := seedImg.Spec.BaseImage if seedImg.Spec.TargetPlatform != "" { image = buildImg command = []string{"/bin/bash", "-c"} args = []string{fmt.Sprintf("mkdir /work && elemental pull-image --platform=%s %s /work && cp /work/elemental-iso/*.iso %s", seedImg.Spec.TargetPlatform, seedImg.Spec.BaseImage, baseIsoPath)} } // If baseImg is not an HTTP url assume it is an image reference containers = append( containers, corev1.Container{ Name: "baseiso", Image: image, ImagePullPolicy: corev1.PullIfNotPresent, Command: command, Args: args, VolumeMounts: []corev1.VolumeMount{ { Name: "iso-storage", MountPath: "/iso", }, }, }, ) } buildCommands = append(buildCommands, "cd /iso && sha256sum $(ELEMENTAL_OUTPUT_NAME) > $(ELEMENTAL_OUTPUT_NAME).sha256 && cd ../") containers = append( containers, corev1.Container{ Name: "build", Image: buildImg, ImagePullPolicy: pullPolicy, Command: []string{"/bin/bash", "-c"}, Args: []string{strings.Join(buildCommands, " && ")}, VolumeMounts: []corev1.VolumeMount{ { Name: "iso-storage", MountPath: "/iso", }, { Name: "config-map", MountPath: "/overlay", }, }, Env: defaultEnvVars(seedImg.Name), }, ) // Make sure the pod starts starts having enough disk for the maximum volume size // volume size just represents a maximum it is not computed to schedule pods. // To ensure there is enough local disk space we add a storage requirement to the first // init container of the pod. containers[0].Resources = corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceEphemeralStorage: seedImg.Spec.Size, }, } return containers } func userDefinedInitContainers(seedImg *elementalv1.SeedImage) []corev1.Container { c := seedImg.Spec.BuildContainer name := "build" if c.Name != "" { name = c.Name } return []corev1.Container{ { Name: name, Image: c.Image, ImagePullPolicy: c.ImagePullPolicy, Command: c.Command, Args: c.Args, VolumeMounts: []corev1.VolumeMount{ { Name: "iso-storage", MountPath: "/iso", }, { Name: "config-map", MountPath: "/overlay", }, }, Env: defaultEnvVars(seedImg.Name), Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceEphemeralStorage: seedImg.Spec.Size, }, }, }, } } func defaultEnvVars(seedImgName string) []corev1.EnvVar { return []corev1.EnvVar{ { Name: "ELEMENTAL_DEVICE", ValueFrom: &corev1.EnvVarSource{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: seedImgName, }, Key: configMapKeyDevice, }, }, }, { Name: "ELEMENTAL_REGISTRATION_URL", ValueFrom: &corev1.EnvVarSource{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: seedImgName, }, Key: configMapKeyRegistrationURL, }, }, }, { Name: "ELEMENTAL_BASE_IMAGE", ValueFrom: &corev1.EnvVarSource{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: seedImgName, }, Key: configMapKeyBaseImage, }, }, }, { Name: "ELEMENTAL_OUTPUT_NAME", ValueFrom: &corev1.EnvVarSource{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: seedImgName, }, Key: configMapKeyOutputName, }, }, }, } } func (r *SeedImageReconciler) createBuildImageService(ctx context.Context, seedImg *elementalv1.SeedImage) error { logger := ctrl.LoggerFrom(ctx) logger.Info("Reconciling Service resources") svcName := seedImg.Name svcNamespace := seedImg.Namespace foundSvc := &corev1.Service{} err := r.Get(ctx, types.NamespacedName{Name: svcName, Namespace: svcNamespace}, foundSvc) if err != nil && !apierrors.IsNotFound(err) { return err } seedImageDeadlinePassed := false if seedImgCondition := meta.FindStatusCondition(seedImg.Status.Conditions, elementalv1.SeedImageConditionReady); seedImgCondition != nil && seedImgCondition.Status == metav1.ConditionTrue && seedImgCondition.Reason == elementalv1.SeedImageBuildDeadline { seedImageDeadlinePassed = true } if err == nil { logger.V(5).Info("Service already there") // ensure the service was created by us for _, owner := range foundSvc.GetOwnerReferences() { if owner.UID == seedImg.UID { if seedImageDeadlinePassed { logger.V(5).Info("Seed image deadline passed, delete associated service", "service", foundSvc.Name) if err := r.Delete(ctx, foundSvc); err != nil { return fmt.Errorf("failed to delete service %s: %w", foundSvc.Name, err) } } return nil } } return fmt.Errorf("service already exists and was not created by this controller") } if seedImageDeadlinePassed { return nil } logger.V(5).Info("Creating service") service := fillBuildImageService(seedImg.Name, seedImg.Namespace) if err := controllerutil.SetControllerReference(seedImg, service, r.Scheme()); err != nil { return err } if err := r.Create(ctx, service); err != nil && !apierrors.IsAlreadyExists(err) { return err } return nil } func fillBuildImageService(name, namespace string) *corev1.Service { service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app.kubernetes.io/name": name}, Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolTCP, Port: 80, }, }, }, } return service } func serializeResetRegistrationYaml(config *elementalv1.Config) ([]byte, error) { regConf, err := yaml.Marshal(config) if err != nil { return nil, err } conf := &schema.YipConfig{ Name: "Write registration config", Stages: map[string][]schema.Stage{ "initramfs": { schema.Stage{ If: `[ -f "/run/cos/recovery_mode" ] || [ -f "/run/elemental/recovery_mode" ]`, Name: "Save registration config", Directories: []schema.Directory{ { Path: filepath.Dir("/oem/registration"), Permissions: 0700, }, }, Files: []schema.File{ { Path: "/oem/registration/config.yaml", Content: base64.StdEncoding.EncodeToString(regConf), Encoding: "b64", Permissions: 0666, }, }, }, }, }, } return yaml.Marshal(conf) }