elemental-operator/controllers/managedosimage_controller_t...

478 lines
15 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"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
"github.com/rancher/elemental-operator/pkg/test"
fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
upgradev1 "github.com/rancher/system-upgrade-controller/pkg/apis/upgrade.cattle.io/v1"
"github.com/rancher/wrangler/v2/pkg/genericcondition"
"github.com/rancher/wrangler/v2/pkg/name"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var _ = Describe("reconcile managed os image", func() {
var r *ManagedOSImageReconciler
var managedOSImage *elementalv1.ManagedOSImage
var bundle *fleetv1.Bundle
BeforeEach(func() {
r = &ManagedOSImageReconciler{
Client: cl,
Scheme: cl.Scheme(),
}
managedOSImage = &elementalv1.ManagedOSImage{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
Namespace: "default",
},
}
bundle = &fleetv1.Bundle{
ObjectMeta: metav1.ObjectMeta{
Name: name.SafeConcatName("mos", managedOSImage.Name),
Namespace: managedOSImage.Namespace,
},
}
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, managedOSImage, bundle)).To(Succeed())
})
It("should reconcile managed os image object", func() {
Expect(cl.Create(ctx, managedOSImage)).To(Succeed())
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: managedOSImage.Namespace,
Name: managedOSImage.Name,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSImage.Name,
Namespace: managedOSImage.Namespace,
}, managedOSImage)).To(Succeed())
bundle := &fleetv1.Bundle{}
Expect(cl.Get(ctx, client.ObjectKey{
Name: name.SafeConcatName("mos", managedOSImage.Name),
Namespace: managedOSImage.Namespace,
}, bundle)).To(Succeed())
})
})
var _ = Describe("newFleetBundleResources", func() {
var r *ManagedOSImageReconciler
var managedOSImage *elementalv1.ManagedOSImage
var managedOSVersion *elementalv1.ManagedOSVersion
BeforeEach(func() {
r = &ManagedOSImageReconciler{
Client: cl,
Scheme: cl.Scheme(),
}
managedOSImage = &elementalv1.ManagedOSImage{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
Namespace: "default",
},
}
managedOSVersion = &elementalv1.ManagedOSVersion{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: elementalv1.ManagedOSVersionSpec{
Type: "container",
UpgradeContainer: &upgradev1.ContainerSpec{
Image: "foo/bar:image",
},
},
}
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, managedOSImage, managedOSVersion)).To(Succeed())
})
It("should create fleet bundle resources", func() {
bundleResources, err := r.newFleetBundleResources(ctx, managedOSImage)
Expect(err).ToNot(HaveOccurred())
Expect(bundleResources).To(HaveLen(5))
Expect(bundleResources[0].Name).To(Equal("ClusterRole--os-upgrader-test-name-d5689b3c1cd3.yaml"))
Expect(bundleResources[1].Name).To(Equal("ClusterRoleBinding--os-upgrader-test-name-cc7ce4275b54.yaml"))
Expect(bundleResources[2].Name).To(Equal("ServiceAccount-cattle-system-os-upgrader-test-name-08929531f5c0.yaml"))
Expect(bundleResources[3].Name).To(Equal("Secret-cattle-system-os-upgrader-test-name-52e9d8e041f4.yaml"))
Expect(bundleResources[4].Name).To(Equal("Plan-cattle-system-os-upgrader-test-name-24d63a562894.yaml"))
})
It("should create fleet bundle when managedOSVersion exists", func() {
Expect(r.Create(ctx, managedOSVersion)).To(Succeed())
managedOSImage.Spec.ManagedOSVersionName = "test"
bundleResources, err := r.newFleetBundleResources(ctx, managedOSImage)
Expect(err).ToNot(HaveOccurred())
Expect(bundleResources).To(HaveLen(5))
plan := &upgradev1.Plan{}
Expect(yaml.Unmarshal([]byte(bundleResources[4].Content), plan)).To(Succeed())
Expect(plan.Spec.Upgrade.Image).To(Equal("foo/bar:image"))
})
It("return error of managedOSVersion doesn't exist", func() {
managedOSImage.Spec.ManagedOSVersionName = "test"
bundleResources, err := r.newFleetBundleResources(ctx, managedOSImage)
Expect(err).To(HaveOccurred())
Expect(bundleResources).To(BeNil())
})
It("should convert ManagedOSImage name to DNS Label standard", func() {
managedOSImage.Name = "test.name"
bundleResources, err := r.newFleetBundleResources(ctx, managedOSImage)
Expect(err).ToNot(HaveOccurred())
Expect(bundleResources[0].Name).To(Equal("ClusterRole--os-upgrader-test-name-d5689b3c1cd3.yaml"))
Expect(bundleResources[1].Name).To(Equal("ClusterRoleBinding--os-upgrader-test-name-cc7ce4275b54.yaml"))
Expect(bundleResources[2].Name).To(Equal("ServiceAccount-cattle-system-os-upgrader-test-name-08929531f5c0.yaml"))
Expect(bundleResources[3].Name).To(Equal("Secret-cattle-system-os-upgrader-test-name-52e9d8e041f4.yaml"))
Expect(bundleResources[4].Name).To(Equal("Plan-cattle-system-os-upgrader-test-name-24d63a562894.yaml"))
})
})
var _ = Describe("createFleetBundle", func() {
var r *ManagedOSImageReconciler
var managedOSImage *elementalv1.ManagedOSImage
var bundle *fleetv1.Bundle
BeforeEach(func() {
r = &ManagedOSImageReconciler{
Client: cl,
Scheme: cl.Scheme(),
}
managedOSImage = &elementalv1.ManagedOSImage{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
Namespace: "default",
UID: "test",
},
Spec: elementalv1.ManagedOSImageSpec{
ClusterRolloutStrategy: &fleetv1.RolloutStrategy{
MaxUnavailable: &intstr.IntOrString{
IntVal: 3,
},
},
Targets: []fleetv1.BundleTarget{
{
Name: "test",
},
},
},
}
bundle = &fleetv1.Bundle{
ObjectMeta: metav1.ObjectMeta{
Name: name.SafeConcatName("mos", managedOSImage.Name),
Namespace: managedOSImage.Namespace,
},
}
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, managedOSImage, bundle)).To(Succeed())
})
It("should create fleet bundle resources", func() {
err := r.createFleetBundle(ctx, managedOSImage, []fleetv1.BundleResource{
{
Name: "test",
},
})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: name.SafeConcatName("mos", managedOSImage.Name),
Namespace: managedOSImage.Namespace,
}, bundle)).To(Succeed())
Expect(bundle.Spec.Resources).To(HaveLen(1))
Expect(bundle.Spec.Resources[0].Name).To(Equal("test"))
Expect(bundle.Spec.RolloutStrategy).ToNot(BeNil())
Expect(bundle.Spec.RolloutStrategy.MaxUnavailable.IntVal).To(Equal(int32(3)))
Expect(bundle.Spec.Targets).To(HaveLen(1))
Expect(bundle.Spec.Targets[0].Name).To(Equal("test"))
})
It("should change target when namespace is fleet-local", func() {
managedOSImage.Namespace = fleetLocalNamespace
err := r.createFleetBundle(ctx, managedOSImage, []fleetv1.BundleResource{
{
Name: "test",
},
})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: r.formatBundleName(*managedOSImage),
Namespace: managedOSImage.Namespace,
}, bundle)).To(Succeed())
Expect(bundle.Spec.Resources).To(HaveLen(1))
Expect(bundle.Spec.Resources[0].Name).To(Equal("test"))
Expect(bundle.Spec.RolloutStrategy).ToNot(BeNil())
Expect(bundle.Spec.RolloutStrategy.MaxUnavailable.IntVal).To(Equal(int32(3)))
Expect(bundle.Spec.Targets).To(HaveLen(1))
Expect(bundle.Spec.Targets[0].ClusterName).To(Equal("local"))
})
It("update fleet bundle if already exists", func() {
Expect(r.createFleetBundle(ctx, managedOSImage, []fleetv1.BundleResource{})).Should(Succeed())
meta.SetStatusCondition(&managedOSImage.Status.Conditions, metav1.Condition{
Type: elementalv1.FleetBundleCreation,
Reason: elementalv1.FleetBundleCreateSuccessReason,
Status: metav1.ConditionTrue,
})
wantResource := []fleetv1.BundleResource{{Name: "test", Content: "test-content"}}
Expect(r.updateFleetBundle(ctx, managedOSImage, wantResource)).Should(Succeed())
Expect(cl.Get(ctx, client.ObjectKey{
Name: r.formatBundleName(*managedOSImage),
Namespace: managedOSImage.Namespace,
}, bundle)).To(Succeed())
Expect(bundle.Spec.Resources).To(Equal(wantResource))
})
})
var _ = Describe("updateManagedOSImageStatus", func() {
var r *ManagedOSImageReconciler
var managedOSImage *elementalv1.ManagedOSImage
var bundle *fleetv1.Bundle
BeforeEach(func() {
r = &ManagedOSImageReconciler{
Client: cl,
Scheme: cl.Scheme(),
}
managedOSImage = &elementalv1.ManagedOSImage{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
Namespace: "default",
},
Spec: elementalv1.ManagedOSImageSpec{
ClusterRolloutStrategy: &fleetv1.RolloutStrategy{
MaxUnavailable: &intstr.IntOrString{
IntVal: 3,
},
},
Targets: []fleetv1.BundleTarget{
{
Name: "test",
},
},
},
}
bundle = &fleetv1.Bundle{
ObjectMeta: metav1.ObjectMeta{
Name: name.SafeConcatName("mos", managedOSImage.Name),
Namespace: managedOSImage.Namespace,
},
}
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, managedOSImage, bundle)).To(Succeed())
})
It("should update manage os image status", func() {
Expect(r.Create(ctx, bundle)).To(Succeed())
patchBase := client.MergeFrom(bundle.DeepCopy())
bundle.Status = fleetv1.BundleStatus{
Conditions: []genericcondition.GenericCondition{
{
Type: "test",
Status: corev1.ConditionTrue,
Reason: "test",
Message: "test",
},
},
}
Expect(r.Status().Patch(ctx, bundle, patchBase)).To(Succeed())
err := r.updateManagedOSImageStatus(ctx, managedOSImage)
Expect(err).ToNot(HaveOccurred())
Expect(managedOSImage.Status.Conditions).To(HaveLen(1))
Expect(managedOSImage.Status.Conditions[0].Type).To(Equal("test"))
Expect(managedOSImage.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
Expect(managedOSImage.Status.Conditions[0].Reason).To(Equal("test"))
})
It("should update manage os image status when condition is missing fields", func() {
Expect(r.Create(ctx, bundle)).To(Succeed())
patchBase := client.MergeFrom(bundle.DeepCopy())
bundle.Status = fleetv1.BundleStatus{
Conditions: []genericcondition.GenericCondition{
{
Message: "test",
},
},
}
Expect(r.Status().Patch(ctx, bundle, patchBase)).To(Succeed())
err := r.updateManagedOSImageStatus(ctx, managedOSImage)
Expect(err).ToNot(HaveOccurred())
Expect(managedOSImage.Status.Conditions).To(HaveLen(1))
Expect(managedOSImage.Status.Conditions[0].Type).To(Equal("UnknownType"))
Expect(managedOSImage.Status.Conditions[0].Status).To(Equal(metav1.ConditionUnknown))
Expect(managedOSImage.Status.Conditions[0].Reason).To(Equal("UnknownReason"))
})
})
var _ = Describe("metadataEnv", func() {
It("should container env variables when passed as strings", func() {
envMap := map[string]runtime.RawExtension{
"upgradeImage": {Raw: []byte(`"registry.com/repository/image:v1.0"`)},
"robin": {Raw: []byte(`"batman"`)},
}
env := metadataEnv(envMap)
Expect(env).To(HaveLen(2))
for _, e := range env {
if e.Name == "METADATA_UPGRADEIMAGE" {
Expect(e.Value).To(Equal("registry.com/repository/image:v1.0"))
} else {
Expect(e.Name).To(Equal("METADATA_ROBIN"))
Expect(e.Value).To(Equal("batman"))
}
}
})
It("should container env variables when passed as jsondata", func() {
jsonStruct := struct{ Foo string }{Foo: "foostruct"}
jsonData, err := json.Marshal(jsonStruct)
Expect(err).ToNot(HaveOccurred())
envMap := map[string]runtime.RawExtension{
"jsondata": {Raw: jsonData},
}
env := metadataEnv(envMap)
Expect(env).To(HaveLen(1))
Expect(env[0].Name).To(Equal("METADATA_JSONDATA"))
Expect(env[0].Value).To(Equal("{\"Foo\":\"foostruct\"}"))
})
})
var _ = Describe("getImageVersion", func() {
It("should format images with no registry and no version", func() {
osImage := &elementalv1.ManagedOSImage{
Spec: elementalv1.ManagedOSImageSpec{
OSImage: "a-test-image",
},
}
image, version, err := getImageVersion(osImage, nil)
Expect(err).ShouldNot(HaveOccurred())
Expect(version).Should(Equal("latest"))
Expect(image).Should(Equal(osImage.Spec.OSImage))
})
It("should format images with no version", func() {
osImage := &elementalv1.ManagedOSImage{
Spec: elementalv1.ManagedOSImageSpec{
OSImage: "a-test-registry/a-test-image",
},
}
image, version, err := getImageVersion(osImage, nil)
Expect(err).ShouldNot(HaveOccurred())
Expect(version).Should(Equal("latest"))
Expect(image).Should(Equal(osImage.Spec.OSImage))
})
It("should format images with fully qualified name", func() {
osImage := &elementalv1.ManagedOSImage{
Spec: elementalv1.ManagedOSImageSpec{
OSImage: "a-test-registry/a-test-image:my-version",
},
}
image, version, err := getImageVersion(osImage, nil)
Expect(err).ShouldNot(HaveOccurred())
Expect(version).Should(Equal("my-version"))
Expect(image).Should(Equal("a-test-registry/a-test-image"))
})
It("should format images with non standard registry ports", func() {
osImage := &elementalv1.ManagedOSImage{
Spec: elementalv1.ManagedOSImageSpec{
OSImage: "a-test-registry:30000/a-test-image:my-version",
},
}
image, version, err := getImageVersion(osImage, nil)
Expect(err).ShouldNot(HaveOccurred())
Expect(version).Should(Equal("my-version"))
Expect(image).Should(Equal("a-test-registry:30000/a-test-image"))
})
It("should format images with non standard registry ports and no version", func() {
osImage := &elementalv1.ManagedOSImage{
Spec: elementalv1.ManagedOSImageSpec{
OSImage: "a-test-registry:30000/a-test-image",
},
}
image, version, err := getImageVersion(osImage, nil)
Expect(err).ShouldNot(HaveOccurred())
Expect(version).Should(Equal("latest"))
Expect(image).Should(Equal(osImage.Spec.OSImage))
})
})
var _ = Describe("Plan naming", func() {
It("should replace invalid characters with -", func() {
input := "-my.invalid!name@"
wanted := "my-invalid-name"
Expect(toDNSLabel(input)).To(Equal(wanted))
})
It("should not replace valid strings", func() {
wanted := "my-valid-name"
Expect(toDNSLabel(wanted)).To(Equal(wanted))
})
})