851 lines
25 KiB
Go
851 lines
25 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 (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
managementv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
|
|
elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
|
|
"github.com/rancher/elemental-operator/pkg/test"
|
|
)
|
|
|
|
var _ = Describe("reconcile seed image", func() {
|
|
var r *SeedImageReconciler
|
|
var mRegistration *elementalv1.MachineRegistration
|
|
var seedImg *elementalv1.SeedImage
|
|
var setting *managementv3.Setting
|
|
var pod *corev1.Pod
|
|
var service *corev1.Service
|
|
|
|
BeforeEach(func() {
|
|
r = &SeedImageReconciler{
|
|
Client: cl,
|
|
SeedImageImage: "registry.suse.com/rancher/seedimage-builder:latest",
|
|
SeedImageImagePullPolicy: corev1.PullIfNotPresent,
|
|
}
|
|
|
|
mRegistration = &elementalv1.MachineRegistration{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Status: elementalv1.MachineRegistrationStatus{
|
|
RegistrationURL: "https://example.com/token",
|
|
RegistrationToken: "token",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: elementalv1.ReadyCondition,
|
|
Reason: elementalv1.SuccessfullyCreatedReason,
|
|
Status: metav1.ConditionTrue,
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
Spec: elementalv1.MachineRegistrationSpec{
|
|
Config: elementalv1.Config{
|
|
Elemental: elementalv1.Elemental{
|
|
Install: elementalv1.Install{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
seedImg = &elementalv1.SeedImage{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Spec: elementalv1.SeedImageSpec{
|
|
BaseImage: "missing-image",
|
|
MachineRegistrationRef: &corev1.ObjectReference{
|
|
Name: mRegistration.Name,
|
|
Namespace: mRegistration.Namespace,
|
|
},
|
|
Type: elementalv1.TypeIso,
|
|
},
|
|
}
|
|
|
|
setting = &managementv3.Setting{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "server-url",
|
|
},
|
|
Value: "https://example.com",
|
|
}
|
|
|
|
pod = &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
}
|
|
|
|
service = &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
}
|
|
|
|
statusCopy := mRegistration.Status.DeepCopy()
|
|
|
|
Expect(cl.Create(ctx, mRegistration)).To(Succeed())
|
|
Expect(cl.Create(ctx, seedImg)).To(Succeed())
|
|
Expect(cl.Create(ctx, setting)).To(Succeed())
|
|
|
|
patchBase := client.MergeFrom(mRegistration.DeepCopy())
|
|
mRegistration.Status = *statusCopy
|
|
Expect(cl.Status().Patch(ctx, mRegistration, patchBase)).To(Succeed())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
Expect(test.CleanupAndWait(ctx, cl, mRegistration, seedImg, setting, pod, service)).To(Succeed())
|
|
})
|
|
|
|
It("should reconcile seed image object", func() {
|
|
_, err := r.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, seedImg)).To(Succeed())
|
|
|
|
Expect(seedImg.Status.Conditions).To(HaveLen(2))
|
|
Expect(seedImg.Status.Conditions[0].Type).To(Equal(elementalv1.ReadyCondition))
|
|
Expect(seedImg.Status.Conditions[0].Reason).To(Equal(elementalv1.ResourcesSuccessfullyCreatedReason))
|
|
Expect(seedImg.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
|
|
Expect(seedImg.Status.Conditions[1].Type).To(Equal(elementalv1.SeedImageConditionReady))
|
|
Expect(seedImg.Status.Conditions[1].Status).To(Equal(metav1.ConditionFalse))
|
|
Expect(seedImg.OwnerReferences[0].UID).To(Equal(mRegistration.UID))
|
|
|
|
objKey := types.NamespacedName{Namespace: seedImg.Namespace, Name: seedImg.Name}
|
|
Expect(r.Get(ctx, objKey, &corev1.Pod{})).To(Succeed())
|
|
Expect(r.Get(ctx, objKey, &corev1.Service{})).To(Succeed())
|
|
|
|
})
|
|
|
|
It("should create the pod pulling the ISO from a container", func() {
|
|
seedImg.Spec.BaseImage = "domain.org/some/repo:and-a-tag"
|
|
|
|
_, err := r.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, seedImg)).To(Succeed())
|
|
|
|
foundPod := &corev1.Pod{}
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, foundPod)).To(Succeed())
|
|
Expect(foundPod.Annotations["elemental.cattle.io/base-image"]).To(Equal(seedImg.Spec.BaseImage))
|
|
Expect(len(foundPod.Spec.InitContainers)).To(Equal(2))
|
|
Expect(foundPod.Spec.InitContainers[0].Image).To(Equal(seedImg.Spec.BaseImage))
|
|
})
|
|
|
|
})
|
|
|
|
var _ = Describe("validate seed image", func() {
|
|
var mRegistration *elementalv1.MachineRegistration
|
|
var seedImg *elementalv1.SeedImage
|
|
|
|
BeforeEach(func() {
|
|
mRegistration = &elementalv1.MachineRegistration{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name2",
|
|
Namespace: "default",
|
|
},
|
|
Status: elementalv1.MachineRegistrationStatus{
|
|
RegistrationURL: "https://example.com/token",
|
|
RegistrationToken: "token",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: elementalv1.ReadyCondition,
|
|
Reason: elementalv1.SuccessfullyCreatedReason,
|
|
Status: metav1.ConditionTrue,
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
Spec: elementalv1.MachineRegistrationSpec{
|
|
Config: elementalv1.Config{
|
|
Elemental: elementalv1.Elemental{
|
|
Install: elementalv1.Install{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
})
|
|
|
|
It("should fail validation on unknown platform format", func() {
|
|
seedImg = &elementalv1.SeedImage{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Spec: elementalv1.SeedImageSpec{
|
|
TargetPlatform: "test-platform",
|
|
BaseImage: "missing-image",
|
|
MachineRegistrationRef: &corev1.ObjectReference{
|
|
Name: mRegistration.Name,
|
|
Namespace: mRegistration.Namespace,
|
|
},
|
|
Type: elementalv1.TypeIso,
|
|
},
|
|
}
|
|
|
|
Expect(cl.Create(ctx, mRegistration)).To(Succeed())
|
|
err := cl.Create(ctx, seedImg)
|
|
Expect(err).ToNot(Succeed())
|
|
Expect(err.Error()).To(ContainSubstring("Invalid value: \"test-platform\": spec.targetPlatform in body should match '^$|^\\S+\\/\\S+$'"))
|
|
|
|
})
|
|
|
|
It("should pass validation on linux/amd64", func() {
|
|
seedImg = &elementalv1.SeedImage{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Spec: elementalv1.SeedImageSpec{
|
|
TargetPlatform: "linux/amd64",
|
|
BaseImage: "missing-image",
|
|
MachineRegistrationRef: &corev1.ObjectReference{
|
|
Name: mRegistration.Name,
|
|
Namespace: mRegistration.Namespace,
|
|
},
|
|
Type: elementalv1.TypeIso,
|
|
},
|
|
}
|
|
|
|
Expect(cl.Create(ctx, mRegistration)).To(Succeed())
|
|
Expect(cl.Create(ctx, seedImg)).To(Succeed())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
Expect(test.CleanupAndWait(ctx, cl, mRegistration, seedImg)).To(Succeed())
|
|
})
|
|
|
|
})
|
|
|
|
var _ = Describe("reconcile seed image build container", func() {
|
|
var r *SeedImageReconciler
|
|
var mRegistration *elementalv1.MachineRegistration
|
|
var seedImg *elementalv1.SeedImage
|
|
var setting *managementv3.Setting
|
|
var pod *corev1.Pod
|
|
var service *corev1.Service
|
|
|
|
BeforeEach(func() {
|
|
r = &SeedImageReconciler{
|
|
Client: cl,
|
|
SeedImageImage: "registry.suse.com/rancher/seedimage-builder:latest",
|
|
SeedImageImagePullPolicy: corev1.PullIfNotPresent,
|
|
}
|
|
|
|
mRegistration = &elementalv1.MachineRegistration{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Status: elementalv1.MachineRegistrationStatus{
|
|
RegistrationURL: "https://example.com/token",
|
|
RegistrationToken: "token",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: elementalv1.ReadyCondition,
|
|
Reason: elementalv1.SuccessfullyCreatedReason,
|
|
Status: metav1.ConditionTrue,
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
Spec: elementalv1.MachineRegistrationSpec{
|
|
Config: elementalv1.Config{
|
|
Elemental: elementalv1.Elemental{
|
|
Install: elementalv1.Install{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
seedImg = &elementalv1.SeedImage{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Spec: elementalv1.SeedImageSpec{
|
|
TargetPlatform: "linux/amd64",
|
|
BaseImage: "seed-image:latest",
|
|
MachineRegistrationRef: &corev1.ObjectReference{
|
|
Name: mRegistration.Name,
|
|
Namespace: mRegistration.Namespace,
|
|
},
|
|
BuildContainer: &elementalv1.BuildContainer{
|
|
Image: "test-seedimage-builder:latest",
|
|
ImagePullPolicy: corev1.PullNever,
|
|
},
|
|
Type: elementalv1.TypeIso,
|
|
},
|
|
}
|
|
|
|
setting = &managementv3.Setting{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "server-url",
|
|
},
|
|
Value: "https://example.com",
|
|
}
|
|
|
|
pod = &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
}
|
|
|
|
service = &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
}
|
|
|
|
statusCopy := mRegistration.Status.DeepCopy()
|
|
|
|
Expect(cl.Create(ctx, mRegistration)).To(Succeed())
|
|
Expect(cl.Create(ctx, seedImg)).To(Succeed())
|
|
Expect(cl.Create(ctx, setting)).To(Succeed())
|
|
|
|
patchBase := client.MergeFrom(mRegistration.DeepCopy())
|
|
mRegistration.Status = *statusCopy
|
|
Expect(cl.Status().Patch(ctx, mRegistration, patchBase)).To(Succeed())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
Expect(test.CleanupAndWait(ctx, cl, mRegistration, seedImg, setting, pod, service)).To(Succeed())
|
|
})
|
|
|
|
It("should create the initContainer from a user-specified BuildContainer", func() {
|
|
_, err := r.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, seedImg)).To(Succeed())
|
|
|
|
foundPod := &corev1.Pod{}
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, foundPod)).To(Succeed())
|
|
Expect(foundPod.Annotations["elemental.cattle.io/base-image"]).To(Equal(seedImg.Spec.BaseImage))
|
|
Expect(len(foundPod.Spec.InitContainers)).To(Equal(1))
|
|
Expect(foundPod.Spec.InitContainers[0].Image).To(Equal(seedImg.Spec.BuildContainer.Image))
|
|
Expect(foundPod.Spec.InitContainers[0].ImagePullPolicy).To(Equal(seedImg.Spec.BuildContainer.ImagePullPolicy))
|
|
Expect(len(foundPod.Spec.InitContainers[0].Env)).To(Equal(4))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("reconcileBuildImagePod", func() {
|
|
var r *SeedImageReconciler
|
|
var setting *managementv3.Setting
|
|
var mRegistration *elementalv1.MachineRegistration
|
|
var seedImg *elementalv1.SeedImage
|
|
var pod *corev1.Pod
|
|
var svc *corev1.Service
|
|
|
|
BeforeEach(func() {
|
|
r = &SeedImageReconciler{
|
|
Client: cl,
|
|
SeedImageImage: "registry.suse.com/rancher/seedimage-builder:latest",
|
|
SeedImageImagePullPolicy: corev1.PullIfNotPresent,
|
|
}
|
|
|
|
mRegistration = &elementalv1.MachineRegistration{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Status: elementalv1.MachineRegistrationStatus{
|
|
RegistrationURL: "https://example.com/token",
|
|
RegistrationToken: "token",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: elementalv1.ReadyCondition,
|
|
Reason: elementalv1.SuccessfullyCreatedReason,
|
|
Status: metav1.ConditionTrue,
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
Spec: elementalv1.MachineRegistrationSpec{
|
|
Config: elementalv1.Config{
|
|
Elemental: elementalv1.Elemental{
|
|
Install: elementalv1.Install{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
seedImg = &elementalv1.SeedImage{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Spec: elementalv1.SeedImageSpec{
|
|
BaseImage: "https://example.com/base.iso",
|
|
MachineRegistrationRef: &corev1.ObjectReference{
|
|
Name: mRegistration.Name,
|
|
Namespace: mRegistration.Namespace,
|
|
},
|
|
Type: elementalv1.TypeIso,
|
|
},
|
|
}
|
|
|
|
pod = &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
},
|
|
}
|
|
|
|
svc = &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
}
|
|
|
|
setting = &managementv3.Setting{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "server-url",
|
|
},
|
|
Value: "https://example.com",
|
|
}
|
|
|
|
statusCopy := mRegistration.Status.DeepCopy()
|
|
|
|
Expect(cl.Create(ctx, mRegistration)).To(Succeed())
|
|
Expect(cl.Create(ctx, seedImg)).To(Succeed())
|
|
Expect(cl.Create(ctx, setting)).To(Succeed())
|
|
|
|
patchBase := client.MergeFrom(mRegistration.DeepCopy())
|
|
mRegistration.Status = *statusCopy
|
|
Expect(cl.Status().Patch(ctx, mRegistration, patchBase)).To(Succeed())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
Expect(test.CleanupAndWait(ctx, cl, mRegistration, setting, seedImg, pod, svc)).To(Succeed())
|
|
})
|
|
|
|
It("should return error when a pod with the same name but different owner is there", func() {
|
|
pod.Spec.Containers = []corev1.Container{
|
|
{
|
|
Name: "nginx",
|
|
Image: "nginx:latest",
|
|
},
|
|
}
|
|
|
|
Expect(cl.Create(ctx, pod)).To(Succeed())
|
|
|
|
err := r.reconcileBuildImagePod(ctx, seedImg, mRegistration)
|
|
|
|
// Pod already there and not owned by the SeedImage obj
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
|
|
It("should skip the reconcile loop when the SeedImageReady condition is met and the Image lifetime has expired", func() {
|
|
seedImg.Status.Conditions = []metav1.Condition{
|
|
{
|
|
Type: elementalv1.SeedImageConditionReady,
|
|
Status: metav1.ConditionTrue,
|
|
Reason: elementalv1.SeedImageBuildDeadline,
|
|
},
|
|
}
|
|
|
|
err := r.reconcileBuildImagePod(ctx, seedImg, mRegistration)
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
foundPod := &corev1.Pod{}
|
|
err = cl.Get(ctx, types.NamespacedName{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
}, foundPod)
|
|
|
|
Expect(apierrors.IsNotFound(err)).To(BeTrue())
|
|
|
|
})
|
|
|
|
It("should recreate the pod if the pod is owned but the base iso is different", func() {
|
|
|
|
_, err := r.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, seedImg)).To(Succeed())
|
|
|
|
foundPod := &corev1.Pod{}
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, foundPod)).To(Succeed())
|
|
Expect(foundPod.Annotations["elemental.cattle.io/base-image"]).To(Equal(seedImg.Spec.BaseImage))
|
|
|
|
seedImg.Spec.BaseImage = "https://example.com/new-base.iso"
|
|
|
|
err = r.reconcileBuildImagePod(ctx, seedImg, mRegistration)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, foundPod)).To(Succeed())
|
|
Expect(foundPod.Annotations["elemental.cattle.io/base-image"]).To(Equal(seedImg.Spec.BaseImage))
|
|
})
|
|
|
|
It("should recreate the pod if the pod is owned but the cloud-config is different", func() {
|
|
_, err := r.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, seedImg)).To(Succeed())
|
|
|
|
foundPod := &corev1.Pod{}
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, foundPod)).To(Succeed())
|
|
|
|
seedImg.Spec.CloudConfig = map[string]runtime.RawExtension{
|
|
"write_files": {},
|
|
}
|
|
|
|
err = r.reconcileBuildImagePod(ctx, seedImg, mRegistration)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, foundPod)).To(Succeed())
|
|
})
|
|
})
|
|
|
|
var _ = Describe("createConfigMapObject", func() {
|
|
var r *SeedImageReconciler
|
|
var setting *managementv3.Setting
|
|
var mRegistration *elementalv1.MachineRegistration
|
|
var seedImg *elementalv1.SeedImage
|
|
var configMap *corev1.ConfigMap
|
|
var pod *corev1.Pod
|
|
var svc *corev1.Service
|
|
|
|
BeforeEach(func() {
|
|
r = &SeedImageReconciler{
|
|
Client: cl,
|
|
SeedImageImage: "registry.suse.com/rancher/seedimage-builder:latest",
|
|
SeedImageImagePullPolicy: corev1.PullIfNotPresent,
|
|
}
|
|
|
|
mRegistration = &elementalv1.MachineRegistration{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Status: elementalv1.MachineRegistrationStatus{
|
|
RegistrationURL: "https://example.com/token",
|
|
RegistrationToken: "token",
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: elementalv1.ReadyCondition,
|
|
Reason: elementalv1.SuccessfullyCreatedReason,
|
|
Status: metav1.ConditionTrue,
|
|
LastTransitionTime: metav1.Now(),
|
|
},
|
|
},
|
|
},
|
|
Spec: elementalv1.MachineRegistrationSpec{
|
|
Config: elementalv1.Config{
|
|
Elemental: elementalv1.Elemental{
|
|
Install: elementalv1.Install{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
seedImg = &elementalv1.SeedImage{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
Namespace: "default",
|
|
},
|
|
Spec: elementalv1.SeedImageSpec{
|
|
BaseImage: "https://example.com/base.iso",
|
|
MachineRegistrationRef: &corev1.ObjectReference{
|
|
Name: mRegistration.Name,
|
|
Namespace: mRegistration.Namespace,
|
|
},
|
|
Type: elementalv1.TypeIso,
|
|
},
|
|
}
|
|
pod = &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
},
|
|
}
|
|
|
|
svc = &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
}
|
|
configMap = &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: seedImg.Namespace,
|
|
Name: seedImg.Name,
|
|
},
|
|
}
|
|
|
|
setting = &managementv3.Setting{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "server-url",
|
|
},
|
|
Value: "https://example.com",
|
|
}
|
|
|
|
statusCopy := mRegistration.Status.DeepCopy()
|
|
|
|
Expect(cl.Create(ctx, mRegistration)).To(Succeed())
|
|
Expect(cl.Create(ctx, seedImg)).To(Succeed())
|
|
Expect(cl.Create(ctx, setting)).To(Succeed())
|
|
|
|
patchBase := client.MergeFrom(mRegistration.DeepCopy())
|
|
mRegistration.Status = *statusCopy
|
|
Expect(cl.Status().Patch(ctx, mRegistration, patchBase)).To(Succeed())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
Expect(test.CleanupAndWait(ctx, cl, mRegistration, seedImg, setting, configMap, pod, svc)).To(Succeed())
|
|
})
|
|
|
|
It("should create a configmap with empty cloud-config data", func() {
|
|
Expect(r.reconcileConfigMapObject(ctx, seedImg, mRegistration)).To(Succeed())
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, configMap)).To(Succeed())
|
|
Expect(len(configMap.BinaryData["cloud-config"])).To(Equal(0))
|
|
})
|
|
|
|
It("should create a configmap with non-empty cloud-config data", func() {
|
|
config := []map[string]string{{
|
|
"path": "/some/path",
|
|
"content": "file contents",
|
|
}}
|
|
rawConf, err := json.Marshal(config)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
seedImg.Spec.CloudConfig = map[string]runtime.RawExtension{}
|
|
seedImg.Spec.CloudConfig["write_files"] = runtime.RawExtension{Raw: rawConf}
|
|
Expect(r.reconcileConfigMapObject(ctx, seedImg, mRegistration)).To(Succeed())
|
|
Expect(cl.Get(ctx, client.ObjectKey{
|
|
Name: seedImg.Name,
|
|
Namespace: seedImg.Namespace,
|
|
}, configMap)).To(Succeed())
|
|
Expect(string(configMap.BinaryData["cloud-config"])).
|
|
To(ContainSubstring("#cloud-config\nwrite_files:"))
|
|
})
|
|
|
|
It("should fail if cloud-config can't be marshalled", func() {
|
|
seedImg.Spec.CloudConfig = map[string]runtime.RawExtension{}
|
|
seedImg.Spec.CloudConfig["write_files"] = runtime.RawExtension{Raw: []byte("invalid data")}
|
|
Expect(r.reconcileConfigMapObject(ctx, seedImg, mRegistration)).ToNot(Succeed())
|
|
})
|
|
})
|
|
|
|
var _ = Describe("updateStatusFromPod", func() {
|
|
var r *SeedImageReconciler
|
|
var seedImg *elementalv1.SeedImage
|
|
var pod *corev1.Pod
|
|
|
|
BeforeEach(func() {
|
|
r = &SeedImageReconciler{
|
|
Client: cl,
|
|
SeedImageImage: "registry.suse.com/rancher/seedimage-builder:latest",
|
|
}
|
|
seedImg = &elementalv1.SeedImage{
|
|
Status: elementalv1.SeedImageStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: elementalv1.SeedImageConditionReady,
|
|
Status: metav1.ConditionUnknown,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
pod = &corev1.Pod{}
|
|
})
|
|
|
|
AfterEach(func() {
|
|
Expect(test.CleanupAndWait(ctx, cl, seedImg, pod)).To(Succeed())
|
|
})
|
|
|
|
It("should sync SeedImageReady condition to Pod phase", func() {
|
|
expectedStates := []struct {
|
|
podPhase corev1.PodPhase
|
|
errorExpected bool
|
|
imgConditionState metav1.ConditionStatus
|
|
imgConditionReason string
|
|
}{
|
|
{corev1.PodPending, false, metav1.ConditionFalse, elementalv1.SeedImageBuildOngoingReason},
|
|
{corev1.PodRunning, true, metav1.ConditionFalse, elementalv1.SeedImageExposeFailureReason},
|
|
{corev1.PodFailed, false, metav1.ConditionFalse, elementalv1.SeedImageBuildFailureReason},
|
|
{corev1.PodSucceeded, true, metav1.ConditionFalse, elementalv1.SeedImageBuildDeadline},
|
|
{corev1.PodUnknown, false, metav1.ConditionUnknown, elementalv1.SeedImageBuildUnknown},
|
|
}
|
|
|
|
for _, expectedState := range expectedStates {
|
|
pod.Status.Phase = expectedState.podPhase
|
|
err := r.updateStatusFromPod(ctx, seedImg, pod)
|
|
if expectedState.errorExpected {
|
|
Expect(err).To(HaveOccurred())
|
|
} else {
|
|
Expect(err).ToNot(HaveOccurred())
|
|
}
|
|
annotation := fmt.Sprintf("pod phase: %s", expectedState.podPhase)
|
|
Expect(seedImg.Status.Conditions[0].Status).To(Equal(expectedState.imgConditionState), annotation)
|
|
Expect(seedImg.Status.Conditions[0].Reason).To(Equal(expectedState.imgConditionReason), annotation)
|
|
}
|
|
})
|
|
|
|
It("should skip syncing when SeedImageReady condition is True", func() {
|
|
seedImg.Status.Conditions = []metav1.Condition{
|
|
{
|
|
Type: elementalv1.SeedImageConditionReady,
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "should stay untouched",
|
|
},
|
|
}
|
|
|
|
err := r.updateStatusFromPod(ctx, seedImg, pod)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(seedImg.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
|
|
Expect(seedImg.Status.Conditions[0].Reason).To(Equal("should stay untouched"))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("fillBuildImagePod", func() {
|
|
It("should provide default build container", func() {
|
|
defaultBuildImg := "default-builder:latest"
|
|
seedImg := &elementalv1.SeedImage{
|
|
Spec: elementalv1.SeedImageSpec{
|
|
BaseImage: "http://localhost/default-image.iso",
|
|
},
|
|
}
|
|
|
|
pod := fillBuildImagePod(seedImg, defaultBuildImg, corev1.PullNever)
|
|
|
|
Expect(len(pod.Spec.InitContainers)).To(Equal(1))
|
|
Expect(pod.Spec.InitContainers[0].Image).To(Equal(defaultBuildImg))
|
|
Expect(pod.Spec.InitContainers[0].ImagePullPolicy).To(Equal(corev1.PullNever))
|
|
})
|
|
|
|
It("should use user-provided build container", func() {
|
|
buildImg := "custom-image:latest"
|
|
|
|
quantity10M := *resource.NewQuantity(10*1024*1024, resource.BinarySI)
|
|
|
|
seedImg := &elementalv1.SeedImage{
|
|
Spec: elementalv1.SeedImageSpec{
|
|
BuildContainer: &elementalv1.BuildContainer{
|
|
Image: buildImg,
|
|
ImagePullPolicy: corev1.PullNever,
|
|
},
|
|
Size: quantity10M,
|
|
},
|
|
}
|
|
|
|
pod := fillBuildImagePod(seedImg, "", corev1.PullNever)
|
|
|
|
Expect(len(pod.Spec.InitContainers)).To(Equal(1))
|
|
Expect(pod.Spec.InitContainers[0].Image).To(Equal(buildImg))
|
|
Expect(pod.Spec.InitContainers[0].ImagePullPolicy).To(Equal(corev1.PullNever))
|
|
})
|
|
|
|
It("should use targetPlatform when provided", func() {
|
|
defaultBuildImg := "default-builder:latest"
|
|
seedImg := &elementalv1.SeedImage{
|
|
Spec: elementalv1.SeedImageSpec{
|
|
BaseImage: "elemental/default-iso:latest",
|
|
TargetPlatform: "linux/riscv64",
|
|
},
|
|
}
|
|
|
|
pod := fillBuildImagePod(seedImg, defaultBuildImg, corev1.PullNever)
|
|
|
|
Expect(len(pod.Spec.InitContainers)).To(Equal(2))
|
|
Expect(pod.Spec.InitContainers[0].Image).To(Equal(defaultBuildImg))
|
|
Expect(pod.Spec.InitContainers[0].Args[0]).To(ContainSubstring("elemental pull-image --platform=linux/riscv64"))
|
|
|
|
})
|
|
})
|