Compare commits

...

3 Commits

Author SHA1 Message Date
Dimitris Karakasilis
d4ce271c3a Migrate legacy (already removed) scripts to auroraboot commands (#201)
* Migrate legacy (already removed) scripts to auroraboot commands

Since we switched to using auroraboot as the tool-image, these scripts
no longer exist. The functionality is provided by auroraboot itself.

Fixes https://github.com/kairos-io/kairos/issues/3779

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Add e2e tests for various cloud formats

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Fix "PVC already exists" error in tests

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Fix auroraboot commands

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Make build-iso an init container

so that the netboot container finds the ISO in place when it needs it

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Fix tests

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Increase timeout for namespace deletion

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Refactor commands

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Refactor auroraboot command strings to use strings.Builder (#203)

* Initial plan

* Refactor command string to use strings.Builder for better readability

Co-authored-by: jimmykarily <2794419+jimmykarily@users.noreply.github.com>

* Complete refactoring of command string

Co-authored-by: jimmykarily <2794419+jimmykarily@users.noreply.github.com>

* Refactor azureCmd and gceCmd to use strings.Builder

Co-authored-by: jimmykarily <2794419+jimmykarily@users.noreply.github.com>

* Revert unintended changes to generated files

Co-authored-by: jimmykarily <2794419+jimmykarily@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jimmykarily <2794419+jimmykarily@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Apply suggestion from @jimmidyson

Co-authored-by: Jimmi Dyson <jimmi.dyson@nutanix.com>
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Refactor auroraboot invocation for consistenty with the rest

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Use string builder everywhere (consistency)

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Make sure we don't match multiple files (PR suggestion)

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

---------

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jimmykarily <2794419+jimmykarily@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jimmi Dyson <jimmi.dyson@nutanix.com>
2025-12-16 20:57:42 +02:00
Dimitris Karakasilis
19b3a57878 The flag --name has been renamed to --override-name in newer auroraboot (#200)
* The flag --name has been renamed to --override-name in newer auroraboot

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Set the default tool-image to a compatible one

We'd better set this to the compatible auroraboot version each time
because changing the flags of auroraboot in the future means it may not always
work with `latest`.

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Use newer image to fix tests

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

---------

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2025-11-20 13:36:57 +02:00
dependabot[bot]
f3ee5c3dc1 Bump golang.org/x/crypto in the go_modules group across 1 directory (#193)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 08:36:00 +02:00
11 changed files with 865 additions and 163 deletions

View File

@@ -183,7 +183,9 @@ KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/k
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN)
@if [ ! -f $(KUSTOMIZE) ]; then \
curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); \
fi
.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.

View File

@@ -18,6 +18,7 @@ package controllers
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -156,10 +157,10 @@ func (r *OSArtifactReconciler) newArtifactPVC(artifact *osbuilder.OSArtifact) *c
}
func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder.OSArtifact) *corev1.Pod {
cmd := fmt.Sprintf(
"auroraboot --debug build-iso --name %s --date=false --output /artifacts dir:/rootfs",
artifact.Name,
)
var cmd strings.Builder
cmd.WriteString("auroraboot --debug build-iso")
cmd.WriteString(fmt.Sprintf(" --override-name %s", artifact.Name))
cmd.WriteString(" --date=false")
volumeMounts := []corev1.VolumeMount{
{
@@ -180,27 +181,29 @@ func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder
})
}
cloudImgCmd := fmt.Sprintf(
"/raw-images.sh /rootfs /artifacts/%s.raw",
artifact.Name,
)
var cloudImgCmd strings.Builder
cloudImgCmd.WriteString("auroraboot --debug")
cloudImgCmd.WriteString(" --set 'disk.raw=true'")
cloudImgCmd.WriteString(" --set 'disable_netboot=true'")
cloudImgCmd.WriteString(" --set 'disable_http_server=true'")
cloudImgCmd.WriteString(" --set 'state_dir=/artifacts'")
cloudImgCmd.WriteString(" --set 'container_image=dir:/rootfs'")
if artifact.Spec.CloudConfigRef != nil {
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: "cloudconfig",
MountPath: "/iso/iso-overlay/cloud_config.yaml",
MountPath: "/cloud-config.yaml",
SubPath: artifact.Spec.CloudConfigRef.Key,
})
cloudImgCmd += " /iso/iso-overlay/cloud_config.yaml"
cloudImgCmd.WriteString(" --cloud-config /cloud-config.yaml")
}
cloudImgCmd.WriteString(fmt.Sprintf(" && file=$(ls /artifacts/*.raw 2>/dev/null | head -n1) && [ -n \"$file\" ] && mv \"$file\" /artifacts/%s.raw", artifact.Name))
if artifact.Spec.CloudConfigRef != nil || artifact.Spec.GRUBConfig != "" {
cmd = fmt.Sprintf(
"auroraboot --debug build-iso --name %s --date=false --overlay-iso /iso/iso-overlay --output /artifacts dir:/rootfs",
artifact.Name,
)
cmd.WriteString(" --cloud-config /cloud-config.yaml")
}
cmd.WriteString(" --output /artifacts dir:/rootfs")
buildIsoContainer := corev1.Container{
ImagePullPolicy: corev1.PullAlways,
@@ -209,7 +212,7 @@ func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
cmd,
cmd.String(),
},
VolumeMounts: volumeMounts,
}
@@ -222,7 +225,7 @@ func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
cloudImgCmd,
cloudImgCmd.String(),
},
VolumeMounts: volumeMounts,
}
@@ -234,6 +237,12 @@ func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder
}}
}
var netbootCmd strings.Builder
netbootCmd.WriteString("auroraboot --debug netboot")
netbootCmd.WriteString(fmt.Sprintf(" /artifacts/%s.iso", artifact.Name))
netbootCmd.WriteString(" /artifacts")
netbootCmd.WriteString(fmt.Sprintf(" %s", artifact.Name))
extractNetboot := corev1.Container{
ImagePullPolicy: corev1.PullAlways,
SecurityContext: &corev1.SecurityContext{Privileged: ptr(true)},
@@ -245,15 +254,24 @@ func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder
Value: artifact.Spec.NetbootURL,
}},
Args: []string{
fmt.Sprintf(
"/netboot.sh /artifacts/%s.iso /artifacts/%s",
artifact.Name,
artifact.Name,
),
netbootCmd.String(),
},
VolumeMounts: volumeMounts,
}
var azureCmd strings.Builder
azureCmd.WriteString("auroraboot --debug")
azureCmd.WriteString(" --set 'disk.vhd=true'")
azureCmd.WriteString(" --set 'disable_netboot=true'")
azureCmd.WriteString(" --set 'disable_http_server=true'")
azureCmd.WriteString(" --set 'state_dir=/artifacts'")
azureCmd.WriteString(" --set 'container_image=dir:/rootfs'")
if artifact.Spec.CloudConfigRef != nil {
azureCmd.WriteString(" --cloud-config /cloud-config.yaml")
}
azureCmd.WriteString(fmt.Sprintf(" && file=$(ls /artifacts/*.vhd 2>/dev/null | head -n1) && [ -n \"$file\" ] && mv \"$file\" /artifacts/%s.vhd", artifact.Name))
buildAzureCloudImageContainer := corev1.Container{
ImagePullPolicy: corev1.PullAlways,
SecurityContext: &corev1.SecurityContext{Privileged: ptr(true)},
@@ -261,15 +279,24 @@ func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"/azure.sh /artifacts/%s.raw /artifacts/%s.vhd",
artifact.Name,
artifact.Name,
),
azureCmd.String(),
},
VolumeMounts: volumeMounts,
}
var gceCmd strings.Builder
gceCmd.WriteString("auroraboot --debug")
gceCmd.WriteString(" --set 'disk.gce=true'")
gceCmd.WriteString(" --set 'disable_netboot=true'")
gceCmd.WriteString(" --set 'disable_http_server=true'")
gceCmd.WriteString(" --set 'state_dir=/artifacts'")
gceCmd.WriteString(" --set 'container_image=dir:/rootfs'")
if artifact.Spec.CloudConfigRef != nil {
gceCmd.WriteString(" --cloud-config /cloud-config.yaml")
}
gceCmd.WriteString(fmt.Sprintf(" && file=$(ls /artifacts/*.raw.gce.tar.gz 2>/dev/null | head -n1) && [ -n \"$file\" ] && mv \"$file\" /artifacts/%s.gce.tar.gz", artifact.Name))
buildGCECloudImageContainer := corev1.Container{
ImagePullPolicy: corev1.PullAlways,
SecurityContext: &corev1.SecurityContext{Privileged: ptr(true)},
@@ -277,11 +304,7 @@ func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"/gce.sh /artifacts/%s.raw /artifacts/%s.gce.raw",
artifact.Name,
artifact.Name,
),
gceCmd.String(),
},
VolumeMounts: volumeMounts,
}
@@ -385,8 +408,11 @@ func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder
podSpec.InitContainers = append(podSpec.InitContainers, kairosReleaseContainer(r.ToolImage))
}
// build-iso runs as an init container to ensure it completes before build-netboot
// (which extracts artifacts from the ISO). Init containers run sequentially and must
// succeed before regular containers start.
if artifact.Spec.ISO || artifact.Spec.Netboot {
podSpec.Containers = append(podSpec.Containers, buildIsoContainer)
podSpec.InitContainers = append(podSpec.InitContainers, buildIsoContainer)
}
if artifact.Spec.Netboot {

View File

@@ -40,6 +40,7 @@ import (
const (
FinalizerName = "build.kairos.io/osbuilder-finalizer"
CompatibleAurorabootVersion = "v0.14.0"
artifactLabel = "build.kairos.io/artifact"
artifactExporterIndexAnnotation = "build.kairos.io/export-index"
)
@@ -127,6 +128,14 @@ func (r *OSArtifactReconciler) createPVC(ctx context.Context, artifact *osbuilde
return pvc, err
}
if err := r.Create(ctx, pvc); err != nil {
if apierrors.IsAlreadyExists(err) {
// PVC already exists, fetch and return it
existingPVC := &corev1.PersistentVolumeClaim{}
if err := r.Get(ctx, client.ObjectKeyFromObject(pvc), existingPVC); err != nil {
return pvc, err
}
return existingPVC, nil
}
return pvc, err
}

View File

@@ -8,7 +8,6 @@ import (
osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/phayes/freeport"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -20,6 +19,7 @@ import (
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var _ = Describe("OSArtifactReconciler", func() {
@@ -51,20 +51,15 @@ var _ = Describe("OSArtifactReconciler", func() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(osbuilder.AddToScheme(scheme))
metricsPort, err := freeport.GetFreePort()
Expect(err).ToNot(HaveOccurred())
fmt.Printf("metricsPort = %+v\n", metricsPort)
mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
Scheme: scheme,
MetricsBindAddress: fmt.Sprintf("127.0.0.1:%d", metricsPort),
})
Expect(err).ToNot(HaveOccurred())
r = &OSArtifactReconciler{
ToolImage: "quay.io/kairos/auroraboot:latest",
ToolImage: fmt.Sprintf("quay.io/kairos/auroraboot:%s", CompatibleAurorabootVersion),
}
err = (r).SetupWithManager(mgr)
// Create a direct client (no cache) for tests - we don't need reconciliation
// This avoids the complexity of managing a running manager
directClient, err := client.New(restConfig, client.Options{Scheme: scheme})
Expect(err).ToNot(HaveOccurred())
err = r.InjectClient(directClient)
Expect(err).ToNot(HaveOccurred())
})
@@ -170,4 +165,247 @@ var _ = Describe("OSArtifactReconciler", func() {
})
})
})
Describe("Auroraboot Commands", func() {
BeforeEach(func() {
artifact.Spec.ImageName = "quay.io/kairos/opensuse:leap-15.6-core-amd64-generic-v3.6.0"
})
When("CloudImage is enabled", func() {
BeforeEach(func() {
artifact.Spec.CloudImage = true
})
It("creates build-cloud-image container with correct auroraboot command", func() {
pvc, err := r.createPVC(context.TODO(), artifact)
Expect(err).ToNot(HaveOccurred())
pod, err := r.createBuilderPod(context.TODO(), artifact, pvc)
Expect(err).ToNot(HaveOccurred())
var cloudImageContainer *corev1.Container
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == "build-cloud-image" {
cloudImageContainer = &pod.Spec.Containers[i]
break
}
}
Expect(cloudImageContainer).ToNot(BeNil())
Expect(cloudImageContainer.Args).To(HaveLen(1))
Expect(cloudImageContainer.Args[0]).To(ContainSubstring("auroraboot --debug --set 'disk.raw=true'"))
Expect(cloudImageContainer.Args[0]).To(ContainSubstring("--set 'state_dir=/artifacts'"))
Expect(cloudImageContainer.Args[0]).To(ContainSubstring("dir:/rootfs"))
Expect(cloudImageContainer.Args[0]).To(ContainSubstring(fmt.Sprintf("file=$(ls /artifacts/*.raw 2>/dev/null | head -n1) && [ -n \"$file\" ] && mv \"$file\" /artifacts/%s.raw", artifact.Name)))
})
When("CloudConfigRef is set", func() {
BeforeEach(func() {
secretName := artifact.Name + "-cloudconfig"
_, err := clientset.CoreV1().Secrets(namespace).Create(context.TODO(),
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
StringData: map[string]string{
"cloud-config.yaml": "#cloud-config\nusers:\n - name: test",
},
Type: "Opaque",
}, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
artifact.Spec.CloudConfigRef = &osbuilder.SecretKeySelector{
Name: secretName,
Key: "cloud-config.yaml",
}
})
It("includes cloud-config flag in auroraboot command", func() {
pvc, err := r.createPVC(context.TODO(), artifact)
Expect(err).ToNot(HaveOccurred())
pod, err := r.createBuilderPod(context.TODO(), artifact, pvc)
Expect(err).ToNot(HaveOccurred())
var cloudImageContainer *corev1.Container
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == "build-cloud-image" {
cloudImageContainer = &pod.Spec.Containers[i]
break
}
}
Expect(cloudImageContainer).ToNot(BeNil())
Expect(cloudImageContainer.Args[0]).To(ContainSubstring("--cloud-config /cloud-config.yaml"))
})
})
})
When("Netboot is enabled", func() {
BeforeEach(func() {
artifact.Spec.Netboot = true
artifact.Spec.ISO = true
artifact.Spec.NetbootURL = "http://example.com"
})
It("creates build-netboot container with correct auroraboot netboot command", func() {
pvc, err := r.createPVC(context.TODO(), artifact)
Expect(err).ToNot(HaveOccurred())
pod, err := r.createBuilderPod(context.TODO(), artifact, pvc)
Expect(err).ToNot(HaveOccurred())
var netbootContainer *corev1.Container
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == "build-netboot" {
netbootContainer = &pod.Spec.Containers[i]
break
}
}
Expect(netbootContainer).ToNot(BeNil())
Expect(netbootContainer.Args).To(HaveLen(1))
Expect(netbootContainer.Args[0]).To(ContainSubstring("auroraboot --debug netboot"))
Expect(netbootContainer.Args[0]).To(ContainSubstring(fmt.Sprintf("/artifacts/%s.iso", artifact.Name)))
Expect(netbootContainer.Args[0]).To(ContainSubstring("/artifacts"))
Expect(netbootContainer.Args[0]).To(ContainSubstring(artifact.Name))
})
})
When("AzureImage is enabled", func() {
BeforeEach(func() {
artifact.Spec.AzureImage = true
})
It("creates build-azure-cloud-image container with correct auroraboot command", func() {
pvc, err := r.createPVC(context.TODO(), artifact)
Expect(err).ToNot(HaveOccurred())
pod, err := r.createBuilderPod(context.TODO(), artifact, pvc)
Expect(err).ToNot(HaveOccurred())
var azureContainer *corev1.Container
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == "build-azure-cloud-image" {
azureContainer = &pod.Spec.Containers[i]
break
}
}
Expect(azureContainer).ToNot(BeNil())
Expect(azureContainer.Args).To(HaveLen(1))
Expect(azureContainer.Args[0]).To(ContainSubstring("auroraboot --debug --set 'disk.vhd=true'"))
Expect(azureContainer.Args[0]).To(ContainSubstring("--set 'state_dir=/artifacts'"))
Expect(azureContainer.Args[0]).To(ContainSubstring("dir:/rootfs"))
Expect(azureContainer.Args[0]).To(ContainSubstring(fmt.Sprintf("file=$(ls /artifacts/*.vhd 2>/dev/null | head -n1) && [ -n \"$file\" ] && mv \"$file\" /artifacts/%s.vhd", artifact.Name)))
})
When("CloudConfigRef is set", func() {
BeforeEach(func() {
secretName := artifact.Name + "-cloudconfig"
_, err := clientset.CoreV1().Secrets(namespace).Create(context.TODO(),
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
StringData: map[string]string{
"cloud-config.yaml": "#cloud-config\nusers:\n - name: test",
},
Type: "Opaque",
}, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
artifact.Spec.CloudConfigRef = &osbuilder.SecretKeySelector{
Name: secretName,
Key: "cloud-config.yaml",
}
})
It("includes cloud-config flag in auroraboot command", func() {
pvc, err := r.createPVC(context.TODO(), artifact)
Expect(err).ToNot(HaveOccurred())
pod, err := r.createBuilderPod(context.TODO(), artifact, pvc)
Expect(err).ToNot(HaveOccurred())
var azureContainer *corev1.Container
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == "build-azure-cloud-image" {
azureContainer = &pod.Spec.Containers[i]
break
}
}
Expect(azureContainer).ToNot(BeNil())
Expect(azureContainer.Args[0]).To(ContainSubstring("--cloud-config /cloud-config.yaml"))
})
})
})
When("GCEImage is enabled", func() {
BeforeEach(func() {
artifact.Spec.GCEImage = true
})
It("creates build-gce-cloud-image container with correct auroraboot command", func() {
pvc, err := r.createPVC(context.TODO(), artifact)
Expect(err).ToNot(HaveOccurred())
pod, err := r.createBuilderPod(context.TODO(), artifact, pvc)
Expect(err).ToNot(HaveOccurred())
var gceContainer *corev1.Container
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == "build-gce-cloud-image" {
gceContainer = &pod.Spec.Containers[i]
break
}
}
Expect(gceContainer).ToNot(BeNil())
Expect(gceContainer.Args).To(HaveLen(1))
Expect(gceContainer.Args[0]).To(ContainSubstring("auroraboot --debug --set 'disk.gce=true'"))
Expect(gceContainer.Args[0]).To(ContainSubstring("--set 'state_dir=/artifacts'"))
Expect(gceContainer.Args[0]).To(ContainSubstring("dir:/rootfs"))
Expect(gceContainer.Args[0]).To(ContainSubstring(fmt.Sprintf("file=$(ls /artifacts/*.raw.gce.tar.gz 2>/dev/null | head -n1) && [ -n \"$file\" ] && mv \"$file\" /artifacts/%s.gce.tar.gz", artifact.Name)))
})
When("CloudConfigRef is set", func() {
BeforeEach(func() {
secretName := artifact.Name + "-cloudconfig"
_, err := clientset.CoreV1().Secrets(namespace).Create(context.TODO(),
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
StringData: map[string]string{
"cloud-config.yaml": "#cloud-config\nusers:\n - name: test",
},
Type: "Opaque",
}, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
artifact.Spec.CloudConfigRef = &osbuilder.SecretKeySelector{
Name: secretName,
Key: "cloud-config.yaml",
}
})
It("includes cloud-config flag in auroraboot command", func() {
pvc, err := r.createPVC(context.TODO(), artifact)
Expect(err).ToNot(HaveOccurred())
pod, err := r.createBuilderPod(context.TODO(), artifact, pvc)
Expect(err).ToNot(HaveOccurred())
var gceContainer *corev1.Container
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == "build-gce-cloud-image" {
gceContainer = &pod.Spec.Containers[i]
break
}
}
Expect(gceContainer).ToNot(BeNil())
Expect(gceContainer.Args[0]).To(ContainSubstring("--cloud-config /cloud-config.yaml"))
})
})
})
})
})

View File

@@ -21,10 +21,12 @@ import (
"math/rand"
"path/filepath"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
@@ -73,7 +75,6 @@ var _ = BeforeSuite(func() {
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
})
var _ = AfterSuite(func() {
@@ -100,10 +101,27 @@ func createRandomNamespace(clientset *kubernetes.Clientset) string {
}, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
// Create default service account to avoid pod creation errors
_, err = clientset.CoreV1().ServiceAccounts(name).Create(context.Background(), &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: name,
},
}, metav1.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
Expect(err).ToNot(HaveOccurred())
}
return name
}
func deleteNamepace(clientset *kubernetes.Clientset, name string) {
err := clientset.CoreV1().Namespaces().Delete(context.Background(), name, metav1.DeleteOptions{})
Expect(err).ToNot(HaveOccurred())
// Wait for the namespace to be fully deleted to ensure clean test isolation
Eventually(func() bool {
_, err := clientset.CoreV1().Namespaces().Get(context.Background(), name, metav1.GetOptions{})
return apierrors.IsNotFound(err)
}, 2*time.Minute, 1*time.Second).Should(BeTrue(), "namespace should be deleted")
}

8
go.mod
View File

@@ -60,12 +60,12 @@ require (
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
golang.org/x/tools v0.24.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect

16
go.sum
View File

@@ -544,8 +544,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -728,12 +728,12 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -743,8 +743,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@@ -18,6 +18,7 @@ package main
import (
"flag"
"fmt"
"os"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
@@ -57,7 +58,7 @@ func main() {
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
// It needs luet inside
flag.StringVar(&toolImage, "tool-image", "quay.io/kairos/auroraboot:latest", "Tool image.")
flag.StringVar(&toolImage, "tool-image", fmt.Sprintf("quay.io/kairos/auroraboot:%s", controllers.CompatibleAurorabootVersion), "Tool image.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,

View File

@@ -0,0 +1,356 @@
package e2e_test
import (
osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2"
. "github.com/onsi/ginkgo/v2"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
)
var _ = Describe("Artifact Format Tests", func() {
var tc *TestClients
BeforeEach(func() {
tc = SetupTestClients()
})
Describe("CloudImage (Raw Disk)", func() {
var artifactName string
var artifactLabelSelector labels.Selector
BeforeEach(func() {
artifact := &osbuilder.OSArtifact{
TypeMeta: metav1.TypeMeta{
Kind: "OSArtifact",
APIVersion: osbuilder.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
GenerateName: "cloudimage-",
},
Spec: osbuilder.OSArtifactSpec{
ImageName: "quay.io/kairos/opensuse:leap-15.6-core-amd64-generic-v3.6.0",
CloudImage: true,
Exporters: []batchv1.JobSpec{
{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{
{
Name: "verify",
Image: "debian:latest",
Command: []string{"bash"},
Args: []string{
"-xec",
`
set -e
# Check that raw file exists
raw_file=$(ls /artifacts/*.raw 2>/dev/null | head -n1)
if [ -z "$raw_file" ]; then
echo "No .raw file found"
exit 1
fi
# Check that it's a valid disk image (has non-zero size)
if [ ! -s "$raw_file" ]; then
echo "Raw file is empty"
exit 1
fi
# Check file size is reasonable (at least 100MB)
size=$(stat -c%s "$raw_file")
if [ "$size" -lt 104857600 ]; then
echo "Raw file too small: $size bytes"
exit 1
fi
echo "Raw disk verification passed: $raw_file"
`,
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "artifacts",
ReadOnly: true,
MountPath: "/artifacts",
},
},
},
},
},
},
},
},
},
}
artifactName, artifactLabelSelector = tc.CreateArtifact(artifact)
})
It("builds a valid raw disk image", func() {
tc.WaitForBuildCompletion(artifactName, artifactLabelSelector)
tc.WaitForExportCompletion(artifactLabelSelector)
tc.Cleanup(artifactName, artifactLabelSelector)
})
})
Describe("Netboot", func() {
var artifactName string
var artifactLabelSelector labels.Selector
BeforeEach(func() {
artifact := &osbuilder.OSArtifact{
TypeMeta: metav1.TypeMeta{
Kind: "OSArtifact",
APIVersion: osbuilder.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
GenerateName: "netboot-",
},
Spec: osbuilder.OSArtifactSpec{
ImageName: "quay.io/kairos/opensuse:leap-15.6-core-amd64-generic-v3.6.0",
ISO: true,
Netboot: true,
NetbootURL: "http://example.com",
Exporters: []batchv1.JobSpec{
{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{
{
Name: "verify",
Image: "debian:latest",
Command: []string{"bash"},
Args: []string{
"-xec",
`
set -e
# Check for kernel file (pattern: *-kernel)
kernel_file=$(ls /artifacts/*-kernel 2>/dev/null | head -n1)
if [ -z "$kernel_file" ]; then
echo "No kernel file found (pattern: *-kernel)"
ls -la /artifacts/ || true
exit 1
fi
# Check for initrd file (pattern: *-initrd)
initrd_file=$(ls /artifacts/*-initrd 2>/dev/null | head -n1)
if [ -z "$initrd_file" ]; then
echo "No initrd file found (pattern: *-initrd)"
ls -la /artifacts/ || true
exit 1
fi
# Check for squashfs file (pattern: *.squashfs)
squashfs_file=$(ls /artifacts/*.squashfs 2>/dev/null | head -n1)
if [ -z "$squashfs_file" ]; then
echo "No squashfs file found (pattern: *.squashfs)"
ls -la /artifacts/ || true
exit 1
fi
# Verify files are non-empty
for file in "$kernel_file" "$initrd_file" "$squashfs_file"; do
if [ ! -s "$file" ]; then
echo "File is empty: $file"
exit 1
fi
done
echo "Netboot artifacts verification passed"
echo "Kernel: $kernel_file"
echo "Initrd: $initrd_file"
echo "Squashfs: $squashfs_file"
`,
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "artifacts",
ReadOnly: true,
MountPath: "/artifacts",
},
},
},
},
},
},
},
},
},
}
artifactName, artifactLabelSelector = tc.CreateArtifact(artifact)
})
It("builds valid netboot artifacts", func() {
tc.WaitForBuildCompletion(artifactName, artifactLabelSelector)
tc.WaitForExportCompletion(artifactLabelSelector)
tc.Cleanup(artifactName, artifactLabelSelector)
})
})
Describe("AzureImage (VHD)", func() {
var artifactName string
var artifactLabelSelector labels.Selector
BeforeEach(func() {
artifact := &osbuilder.OSArtifact{
TypeMeta: metav1.TypeMeta{
Kind: "OSArtifact",
APIVersion: osbuilder.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
GenerateName: "azure-",
},
Spec: osbuilder.OSArtifactSpec{
ImageName: "quay.io/kairos/opensuse:leap-15.6-core-amd64-generic-v3.6.0",
AzureImage: true,
Exporters: []batchv1.JobSpec{
{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{
{
Name: "verify",
Image: "debian:latest",
Command: []string{"bash"},
Args: []string{
"-xec",
`
set -e
# Check that VHD file exists
vhd_file=$(ls /artifacts/*.vhd 2>/dev/null | head -n1)
if [ -z "$vhd_file" ]; then
echo "No .vhd file found"
exit 1
fi
# Check that it's non-empty
if [ ! -s "$vhd_file" ]; then
echo "VHD file is empty"
exit 1
fi
# Check file size is reasonable (at least 100MB)
size=$(stat -c%s "$vhd_file")
if [ "$size" -lt 104857600 ]; then
echo "VHD file too small: $size bytes"
exit 1
fi
# Check VHD footer (last 512 bytes should contain VHD signature)
# VHD footer starts at offset -512 and contains "conectix" string
tail -c 512 "$vhd_file" | grep -q "conectix" || {
echo "VHD file does not have valid VHD footer"
exit 1
}
echo "VHD verification passed: $vhd_file"
`,
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "artifacts",
ReadOnly: true,
MountPath: "/artifacts",
},
},
},
},
},
},
},
},
},
}
artifactName, artifactLabelSelector = tc.CreateArtifact(artifact)
})
It("builds a valid Azure VHD image", func() {
tc.WaitForBuildCompletion(artifactName, artifactLabelSelector)
tc.WaitForExportCompletion(artifactLabelSelector)
tc.Cleanup(artifactName, artifactLabelSelector)
})
})
Describe("GCEImage", func() {
var artifactName string
var artifactLabelSelector labels.Selector
BeforeEach(func() {
artifact := &osbuilder.OSArtifact{
TypeMeta: metav1.TypeMeta{
Kind: "OSArtifact",
APIVersion: osbuilder.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
GenerateName: "gce-",
},
Spec: osbuilder.OSArtifactSpec{
ImageName: "quay.io/kairos/opensuse:leap-15.6-core-amd64-generic-v3.6.0",
GCEImage: true,
Exporters: []batchv1.JobSpec{
{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{
{
Name: "verify",
Image: "debian:latest",
Command: []string{"bash"},
Args: []string{
"-xec",
`
set -e
# Check that GCE tar.gz file exists
gce_file=$(ls /artifacts/*.gce.tar.gz 2>/dev/null | head -n1)
if [ -z "$gce_file" ]; then
echo "No .gce.tar.gz file found"
exit 1
fi
# Check that it's non-empty
if [ ! -s "$gce_file" ]; then
echo "GCE tar.gz file is empty"
exit 1
fi
# Extract and verify it contains disk.raw
temp_dir=$(mktemp -d)
trap "rm -rf $temp_dir" EXIT
tar -xzf "$gce_file" -C "$temp_dir"
if [ ! -f "$temp_dir/disk.raw" ]; then
echo "GCE archive does not contain disk.raw"
exit 1
fi
# Verify disk.raw is non-empty and reasonable size
if [ ! -s "$temp_dir/disk.raw" ]; then
echo "disk.raw in archive is empty"
exit 1
fi
size=$(stat -c%s "$temp_dir/disk.raw")
if [ "$size" -lt 104857600 ]; then
echo "disk.raw too small: $size bytes"
exit 1
fi
echo "GCE verification passed: $gce_file"
`,
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "artifacts",
ReadOnly: true,
MountPath: "/artifacts",
},
},
},
},
},
},
},
},
},
}
artifactName, artifactLabelSelector = tc.CreateArtifact(artifact)
})
It("builds a valid GCE image", func() {
tc.WaitForBuildCompletion(artifactName, artifactLabelSelector)
tc.WaitForExportCompletion(artifactLabelSelector)
tc.Cleanup(artifactName, artifactLabelSelector)
})
})
})

View File

@@ -1,41 +1,21 @@
package e2e_test
import (
"context"
"time"
osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
ctrl "sigs.k8s.io/controller-runtime"
)
var _ = Describe("ISO build test", func() {
var artifactName string
var artifacts, pods, pvcs, jobs dynamic.ResourceInterface
var scheme *runtime.Scheme
var artifactLabelSelector labels.Selector
var tc *TestClients
BeforeEach(func() {
k8s := dynamic.NewForConfigOrDie(ctrl.GetConfigOrDie())
scheme = runtime.NewScheme()
err := osbuilder.AddToScheme(scheme)
Expect(err).ToNot(HaveOccurred())
artifacts = k8s.Resource(schema.GroupVersionResource{Group: osbuilder.GroupVersion.Group, Version: osbuilder.GroupVersion.Version, Resource: "osartifacts"}).Namespace("default")
pods = k8s.Resource(schema.GroupVersionResource{Group: corev1.GroupName, Version: corev1.SchemeGroupVersion.Version, Resource: "pods"}).Namespace("default")
pvcs = k8s.Resource(schema.GroupVersionResource{Group: corev1.GroupName, Version: corev1.SchemeGroupVersion.Version, Resource: "persistentvolumeclaims"}).Namespace("default")
jobs = k8s.Resource(schema.GroupVersionResource{Group: batchv1.GroupName, Version: batchv1.SchemeGroupVersion.Version, Resource: "jobs"}).Namespace("default")
tc = SetupTestClients()
artifact := &osbuilder.OSArtifact{
TypeMeta: metav1.TypeMeta{
@@ -46,7 +26,7 @@ var _ = Describe("ISO build test", func() {
GenerateName: "simple-",
},
Spec: osbuilder.OSArtifactSpec{
ImageName: "quay.io/kairos/core-opensuse:latest",
ImageName: "quay.io/kairos/opensuse:leap-15.6-core-amd64-generic-v3.6.0",
ISO: true,
DiskSize: "",
Exporters: []batchv1.JobSpec{
@@ -76,87 +56,12 @@ var _ = Describe("ISO build test", func() {
},
}
uArtifact := unstructured.Unstructured{}
uArtifact.Object, _ = runtime.DefaultUnstructuredConverter.ToUnstructured(artifact)
resp, err := artifacts.Create(context.TODO(), &uArtifact, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
artifactName = resp.GetName()
artifactLabelSelectorReq, err := labels.NewRequirement("build.kairos.io/artifact", selection.Equals, []string{artifactName})
Expect(err).ToNot(HaveOccurred())
artifactLabelSelector = labels.NewSelector().Add(*artifactLabelSelectorReq)
artifactName, artifactLabelSelector = tc.CreateArtifact(artifact)
})
It("works", func() {
By("starting the build")
Eventually(func(g Gomega) {
w, err := pods.Watch(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
Expect(err).ToNot(HaveOccurred())
var stopped bool
for !stopped {
event, ok := <-w.ResultChan()
stopped = event.Type != watch.Deleted && event.Type != watch.Error || !ok
}
}).WithTimeout(time.Hour).Should(Succeed())
By("exporting the artifacts")
Eventually(func(g Gomega) {
w, err := jobs.Watch(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
Expect(err).ToNot(HaveOccurred())
var stopped bool
for !stopped {
event, ok := <-w.ResultChan()
stopped = event.Type != watch.Deleted && event.Type != watch.Error || !ok
}
}).WithTimeout(time.Hour).Should(Succeed())
By("building the artifacts successfully")
Eventually(func(g Gomega) {
w, err := artifacts.Watch(context.TODO(), metav1.ListOptions{})
Expect(err).ToNot(HaveOccurred())
var artifact osbuilder.OSArtifact
var stopped bool
for !stopped {
event, ok := <-w.ResultChan()
stopped = !ok
if event.Type == watch.Modified && event.Object.(*unstructured.Unstructured).GetName() == artifactName {
err := scheme.Convert(event.Object, &artifact, nil)
Expect(err).ToNot(HaveOccurred())
stopped = artifact.Status.Phase == osbuilder.Ready
}
}
}).WithTimeout(time.Hour).Should(Succeed())
By("cleaning up resources on deletion")
err := artifacts.Delete(context.TODO(), artifactName, metav1.DeleteOptions{})
Expect(err).ToNot(HaveOccurred())
Eventually(func(g Gomega) int {
res, err := artifacts.List(context.TODO(), metav1.ListOptions{})
Expect(err).ToNot(HaveOccurred())
return len(res.Items)
}).WithTimeout(time.Minute).Should(Equal(0))
Eventually(func(g Gomega) int {
res, err := pods.List(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
Expect(err).ToNot(HaveOccurred())
return len(res.Items)
}).WithTimeout(time.Minute).Should(Equal(0))
Eventually(func(g Gomega) int {
res, err := pvcs.List(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
Expect(err).ToNot(HaveOccurred())
return len(res.Items)
}).WithTimeout(time.Minute).Should(Equal(0))
Eventually(func(g Gomega) int {
res, err := jobs.List(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
Expect(err).ToNot(HaveOccurred())
return len(res.Items)
}).WithTimeout(time.Minute).Should(Equal(0))
tc.WaitForBuildCompletion(artifactName, artifactLabelSelector)
tc.WaitForExportCompletion(artifactLabelSelector)
tc.Cleanup(artifactName, artifactLabelSelector)
})
})

View File

@@ -1,13 +1,160 @@
package e2e_test
import (
"context"
"testing"
"time"
osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
ctrl "sigs.k8s.io/controller-runtime"
)
func TestE2e(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "kairos-operator e2e test Suite")
}
// TestClients holds common Kubernetes clients used across e2e tests
type TestClients struct {
Artifacts dynamic.ResourceInterface
Pods dynamic.ResourceInterface
PVCs dynamic.ResourceInterface
Jobs dynamic.ResourceInterface
Scheme *runtime.Scheme
}
// SetupTestClients initializes and returns common Kubernetes clients
func SetupTestClients() *TestClients {
k8s := dynamic.NewForConfigOrDie(ctrl.GetConfigOrDie())
scheme := runtime.NewScheme()
err := osbuilder.AddToScheme(scheme)
Expect(err).ToNot(HaveOccurred())
return &TestClients{
Artifacts: k8s.Resource(schema.GroupVersionResource{
Group: osbuilder.GroupVersion.Group,
Version: osbuilder.GroupVersion.Version,
Resource: "osartifacts",
}).Namespace("default"),
Pods: k8s.Resource(schema.GroupVersionResource{
Group: corev1.GroupName,
Version: corev1.SchemeGroupVersion.Version,
Resource: "pods",
}).Namespace("default"),
PVCs: k8s.Resource(schema.GroupVersionResource{
Group: corev1.GroupName,
Version: corev1.SchemeGroupVersion.Version,
Resource: "persistentvolumeclaims",
}).Namespace("default"),
Jobs: k8s.Resource(schema.GroupVersionResource{
Group: batchv1.GroupName,
Version: batchv1.SchemeGroupVersion.Version,
Resource: "jobs",
}).Namespace("default"),
Scheme: scheme,
}
}
// CreateArtifact creates an OSArtifact and returns its name and label selector
func (tc *TestClients) CreateArtifact(artifact *osbuilder.OSArtifact) (string, labels.Selector) {
uArtifact := unstructured.Unstructured{}
uArtifact.Object, _ = runtime.DefaultUnstructuredConverter.ToUnstructured(artifact)
resp, err := tc.Artifacts.Create(context.TODO(), &uArtifact, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
artifactName := resp.GetName()
artifactLabelSelectorReq, err := labels.NewRequirement("build.kairos.io/artifact", selection.Equals, []string{artifactName})
Expect(err).ToNot(HaveOccurred())
artifactLabelSelector := labels.NewSelector().Add(*artifactLabelSelectorReq)
return artifactName, artifactLabelSelector
}
// WaitForBuildCompletion waits for the build pod to complete and artifact to be ready
func (tc *TestClients) WaitForBuildCompletion(artifactName string, artifactLabelSelector labels.Selector) {
By("waiting for build pod to complete")
Eventually(func(g Gomega) {
w, err := tc.Pods.Watch(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
g.Expect(err).ToNot(HaveOccurred())
var stopped bool
for !stopped {
event, ok := <-w.ResultChan()
stopped = event.Type != watch.Deleted && event.Type != watch.Error || !ok
}
}).WithTimeout(time.Hour).Should(Succeed())
By("waiting for artifact to be ready")
Eventually(func(g Gomega) {
w, err := tc.Artifacts.Watch(context.TODO(), metav1.ListOptions{})
g.Expect(err).ToNot(HaveOccurred())
var artifact osbuilder.OSArtifact
var stopped bool
for !stopped {
event, ok := <-w.ResultChan()
stopped = !ok
if event.Type == watch.Modified && event.Object.(*unstructured.Unstructured).GetName() == artifactName {
err := tc.Scheme.Convert(event.Object, &artifact, nil)
g.Expect(err).ToNot(HaveOccurred())
stopped = artifact.Status.Phase == osbuilder.Ready
}
}
}).WithTimeout(time.Hour).Should(Succeed())
}
// WaitForExportCompletion waits for the export job to complete
func (tc *TestClients) WaitForExportCompletion(artifactLabelSelector labels.Selector) {
By("waiting for export job to complete")
Eventually(func(g Gomega) {
w, err := tc.Jobs.Watch(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
g.Expect(err).ToNot(HaveOccurred())
var stopped bool
for !stopped {
event, ok := <-w.ResultChan()
stopped = event.Type != watch.Deleted && event.Type != watch.Error || !ok
}
}).WithTimeout(time.Hour).Should(Succeed())
}
// Cleanup deletes the artifact and waits for all related resources to be cleaned up
func (tc *TestClients) Cleanup(artifactName string, artifactLabelSelector labels.Selector) {
By("cleaning up resources")
err := tc.Artifacts.Delete(context.TODO(), artifactName, metav1.DeleteOptions{})
Expect(err).ToNot(HaveOccurred())
Eventually(func(g Gomega) int {
res, err := tc.Artifacts.List(context.TODO(), metav1.ListOptions{})
g.Expect(err).ToNot(HaveOccurred())
return len(res.Items)
}).WithTimeout(time.Minute).Should(Equal(0))
Eventually(func(g Gomega) int {
res, err := tc.Pods.List(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
g.Expect(err).ToNot(HaveOccurred())
return len(res.Items)
}).WithTimeout(time.Minute).Should(Equal(0))
Eventually(func(g Gomega) int {
res, err := tc.PVCs.List(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
g.Expect(err).ToNot(HaveOccurred())
return len(res.Items)
}).WithTimeout(time.Minute).Should(Equal(0))
Eventually(func(g Gomega) int {
res, err := tc.Jobs.List(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
g.Expect(err).ToNot(HaveOccurred())
return len(res.Items)
}).WithTimeout(time.Minute).Should(Equal(0))
}