980 lines
31 KiB
Go
980 lines
31 KiB
Go
/*
|
|
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)
|
|
}
|