upgrade: support Kubernetes components (#839)

* upgrade: add Kubernetes components to NodeVersion

* update rfc
This commit is contained in:
3u13r 2023-01-03 12:09:53 +01:00 committed by GitHub
parent 4b43311fbd
commit f14af0c3eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 897 additions and 738 deletions

View file

@ -26,8 +26,8 @@ import (
)
const (
// NodeKubernetesComponentsHashAnnotationKey is the name of the annotation holding the hash of the installed components of this node.
NodeKubernetesComponentsHashAnnotationKey = "updates.edgeless.systems/kubernetes-components-hash"
// NodeKubernetesComponentsReferenceAnnotationKey is the name of the annotation holding the reference to the ConfigMap listing all K8s components.
NodeKubernetesComponentsReferenceAnnotationKey = "constellation.edgeless.systems/kubernetes-components"
joiningNodeNameKey = ".spec.name"
)
@ -76,7 +76,7 @@ func (r *JoiningNodesReconciler) Reconcile(ctx context.Context, req ctrl.Request
if node.Annotations == nil {
node.Annotations = map[string]string{}
}
node.Annotations[NodeKubernetesComponentsHashAnnotationKey] = joiningNode.Spec.ComponentsHash
node.Annotations[NodeKubernetesComponentsReferenceAnnotationKey] = joiningNode.Spec.ComponentsReference
return r.Update(ctx, &node)
})
if err != nil {

View file

@ -23,12 +23,12 @@ import (
var _ = Describe("JoiningNode controller", func() {
const (
nodeName1 = "node-name-1"
nodeName2 = "node-name-2"
nodeName3 = "node-name-3"
componentsHash1 = "test-hash-1"
componentsHash2 = "test-hash-2"
componentsHash3 = "test-hash-3"
nodeName1 = "node-name-1"
nodeName2 = "node-name-2"
nodeName3 = "node-name-3"
ComponentsReference1 = "test-ref-1"
ComponentsReference2 = "test-ref-2"
ComponentsReference3 = "test-ref-3"
timeout = time.Second * 20
duration = time.Second * 2
@ -47,8 +47,8 @@ var _ = Describe("JoiningNode controller", func() {
Name: nodeName1,
},
Spec: updatev1alpha1.JoiningNodeSpec{
Name: nodeName1,
ComponentsHash: componentsHash1,
Name: nodeName1,
ComponentsReference: ComponentsReference1,
},
}
Expect(k8sClient.Create(ctx, joiningNode)).Should(Succeed())
@ -57,7 +57,7 @@ var _ = Describe("JoiningNode controller", func() {
return k8sClient.Get(ctx, types.NamespacedName{Name: nodeName1}, createdJoiningNode)
}, timeout, interval).Should(Succeed())
Expect(createdJoiningNode.Spec.Name).Should(Equal(nodeName1))
Expect(createdJoiningNode.Spec.ComponentsHash).Should(Equal(componentsHash1))
Expect(createdJoiningNode.Spec.ComponentsReference).Should(Equal(ComponentsReference1))
By("creating a node")
node := &corev1.Node{
@ -80,8 +80,8 @@ var _ = Describe("JoiningNode controller", func() {
By("annotating the node")
Eventually(func() string {
_ = k8sClient.Get(ctx, types.NamespacedName{Name: nodeName1}, createdNode)
return createdNode.Annotations[NodeKubernetesComponentsHashAnnotationKey]
}, timeout, interval).Should(Equal(componentsHash1))
return createdNode.Annotations[NodeKubernetesComponentsReferenceAnnotationKey]
}, timeout, interval).Should(Equal(ComponentsReference1))
By("deleting the joining node resource")
Eventually(func() error {
@ -119,8 +119,8 @@ var _ = Describe("JoiningNode controller", func() {
Name: nodeName2,
},
Spec: updatev1alpha1.JoiningNodeSpec{
Name: nodeName2,
ComponentsHash: componentsHash2,
Name: nodeName2,
ComponentsReference: ComponentsReference2,
},
}
Expect(k8sClient.Create(ctx, joiningNode)).Should(Succeed())
@ -129,13 +129,13 @@ var _ = Describe("JoiningNode controller", func() {
return k8sClient.Get(ctx, types.NamespacedName{Name: joiningNode.Name}, createdJoiningNode)
}, timeout, interval).Should(Succeed())
Expect(createdJoiningNode.Spec.Name).Should(Equal(nodeName2))
Expect(createdJoiningNode.Spec.ComponentsHash).Should(Equal(componentsHash2))
Expect(createdJoiningNode.Spec.ComponentsReference).Should(Equal(ComponentsReference2))
By("annotating the node")
Eventually(func() string {
_ = k8sClient.Get(ctx, types.NamespacedName{Name: createdNode.Name}, createdNode)
return createdNode.Annotations[NodeKubernetesComponentsHashAnnotationKey]
}, timeout, interval).Should(Equal(componentsHash2))
return createdNode.Annotations[NodeKubernetesComponentsReferenceAnnotationKey]
}, timeout, interval).Should(Equal(ComponentsReference2))
By("deleting the joining node resource")
Eventually(func() error {
@ -154,8 +154,8 @@ var _ = Describe("JoiningNode controller", func() {
Name: nodeName3,
},
Spec: updatev1alpha1.JoiningNodeSpec{
Name: nodeName3,
ComponentsHash: componentsHash3,
Name: nodeName3,
ComponentsReference: ComponentsReference3,
// create without deadline first
},
}
@ -165,7 +165,7 @@ var _ = Describe("JoiningNode controller", func() {
return k8sClient.Get(ctx, types.NamespacedName{Name: joiningNode.Name}, createdJoiningNode)
}, timeout, interval).Should(Succeed())
Expect(createdJoiningNode.Spec.Name).Should(Equal(nodeName3))
Expect(createdJoiningNode.Spec.ComponentsHash).Should(Equal(componentsHash3))
Expect(createdJoiningNode.Spec.ComponentsReference).Should(Equal(ComponentsReference3))
By("setting the deadline to the past")
createdJoiningNode.Spec.Deadline = &metav1.Time{Time: fakes.clock.Now().Add(-time.Second)}

View file

@ -38,29 +38,29 @@ const (
// nodeJoinTimeout is the time limit pending nodes have to join the cluster before being terminated.
nodeJoinTimeout = time.Minute * 30
// nodeLeaveTimeout is the time limit pending nodes have to leave the cluster and being terminated.
nodeLeaveTimeout = time.Minute
donorAnnotation = "constellation.edgeless.systems/donor"
heirAnnotation = "constellation.edgeless.systems/heir"
scalingGroupAnnotation = "constellation.edgeless.systems/scaling-group-id"
nodeImageAnnotation = "constellation.edgeless.systems/node-image"
obsoleteAnnotation = "constellation.edgeless.systems/obsolete"
conditionNodeImageUpToDateReason = "NodeImagesUpToDate"
conditionNodeImageUpToDateMessage = "Node image of every node is up to date"
conditionNodeImageOutOfDateReason = "NodeImagesOutOfDate"
conditionNodeImageOutOfDateMessage = "Some node images are out of date"
nodeLeaveTimeout = time.Minute
donorAnnotation = "constellation.edgeless.systems/donor"
heirAnnotation = "constellation.edgeless.systems/heir"
scalingGroupAnnotation = "constellation.edgeless.systems/scaling-group-id"
nodeImageAnnotation = "constellation.edgeless.systems/node-image"
obsoleteAnnotation = "constellation.edgeless.systems/obsolete"
conditionNodeVersionUpToDateReason = "NodeVersionsUpToDate"
conditionNodeVersionUpToDateMessage = "Node version of every node is up to date"
conditionNodeVersionOutOfDateReason = "NodeVersionsOutOfDate"
conditionNodeVersionOutOfDateMessage = "Some node versions are out of date"
)
// NodeImageReconciler reconciles a NodeImage object.
type NodeImageReconciler struct {
// NodeVersionReconciler reconciles a NodeVersion object.
type NodeVersionReconciler struct {
nodeReplacer
etcdRemover
client.Client
Scheme *runtime.Scheme
}
// NewNodeImageReconciler creates a new NodeImageReconciler.
func NewNodeImageReconciler(nodeReplacer nodeReplacer, etcdRemover etcdRemover, client client.Client, scheme *runtime.Scheme) *NodeImageReconciler {
return &NodeImageReconciler{
// NewNodeVersionReconciler creates a new NodeVersionReconciler.
func NewNodeVersionReconciler(nodeReplacer nodeReplacer, etcdRemover etcdRemover, client client.Client, scheme *runtime.Scheme) *NodeVersionReconciler {
return &NodeVersionReconciler{
nodeReplacer: nodeReplacer,
etcdRemover: etcdRemover,
Client: client,
@ -68,20 +68,20 @@ func NewNodeImageReconciler(nodeReplacer nodeReplacer, etcdRemover etcdRemover,
}
}
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeimages,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeimages/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeimages/finalizers,verbs=update
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeversions,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeversions/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeversions/finalizers,verbs=update
//+kubebuilder:rbac:groups=nodemaintenance.medik8s.io,resources=nodemaintenances,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=nodes/status,verbs=get
// Reconcile replaces outdated nodes (using an old image) with new nodes (using a new image) as specified in the NodeImage spec.
func (r *NodeImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Reconcile replaces outdated nodes (using an old image) with new nodes (using a new image) as specified in the NodeVersion spec.
func (r *NodeVersionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logr := log.FromContext(ctx)
logr.Info("Reconciling NodeImage")
logr.Info("Reconciling NodeVersion")
var desiredNodeImage updatev1alpha1.NodeImage
if err := r.Get(ctx, req.NamespacedName, &desiredNodeImage); err != nil {
var desiredNodeVersion updatev1alpha1.NodeVersion
if err := r.Get(ctx, req.NamespacedName, &desiredNodeVersion); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// get list of autoscaling strategies
@ -122,7 +122,7 @@ func (r *NodeImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
scalingGroupByID[strings.ToLower(scalingGroup.Spec.GroupID)] = scalingGroup
}
annotatedNodes, invalidNodes := r.annotateNodes(ctx, nodeList.Items)
groups := groupNodes(annotatedNodes, pendingNodeList.Items, desiredNodeImage.Spec.ImageReference)
groups := groupNodes(annotatedNodes, pendingNodeList.Items, desiredNodeVersion.Spec.ImageReference, desiredNodeVersion.Spec.KubernetesComponentsReference)
logr.Info("Grouped nodes",
"outdatedNodes", len(groups.Outdated),
@ -147,7 +147,7 @@ func (r *NodeImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
}
logr.Info("Budget for new nodes", "newNodesBudget", newNodesBudget)
status := nodeImageStatus(r.Scheme, groups, pendingNodeList.Items, invalidNodes, newNodesBudget)
status := nodeVersionStatus(r.Scheme, groups, pendingNodeList.Items, invalidNodes, newNodesBudget)
if err := r.tryUpdateStatus(ctx, req.NamespacedName, status); err != nil {
logr.Error(err, "Updating status")
}
@ -159,20 +159,20 @@ func (r *NodeImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
}
if allNodesUpToDate {
logr.Info("All node images up to date")
logr.Info("All node versions up to date")
return ctrl.Result{}, nil
}
// should requeue is set if a node is deleted
var shouldRequeue bool
// find pairs of mint nodes and outdated nodes in the same scaling group to become donor & heir
replacementPairs := r.pairDonorsAndHeirs(ctx, &desiredNodeImage, groups.Outdated, groups.Mint)
replacementPairs := r.pairDonorsAndHeirs(ctx, &desiredNodeVersion, groups.Outdated, groups.Mint)
// extend replacement pairs to include existing pairs of donors and heirs
replacementPairs = r.matchDonorsAndHeirs(ctx, replacementPairs, groups.Donors, groups.Heirs)
// replace donor nodes by heirs
for _, pair := range replacementPairs {
logr.Info("Replacing node", "donorNode", pair.donor.Name, "heirNode", pair.heir.Name)
done, err := r.replaceNode(ctx, &desiredNodeImage, pair)
done, err := r.replaceNode(ctx, &desiredNodeVersion, pair)
if err != nil {
logr.Error(err, "Replacing node")
return ctrl.Result{}, err
@ -192,13 +192,13 @@ func (r *NodeImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{Requeue: shouldRequeue}, nil
}
newNodeConfig := newNodeConfig{desiredNodeImage, groups.Outdated, pendingNodeList.Items, scalingGroupByID, newNodesBudget}
newNodeConfig := newNodeConfig{desiredNodeVersion, groups.Outdated, pendingNodeList.Items, scalingGroupByID, newNodesBudget}
if err := r.createNewNodes(ctx, newNodeConfig); err != nil {
return ctrl.Result{Requeue: shouldRequeue}, nil
}
// cleanup obsolete nodes
for _, node := range groups.Obsolete {
done, err := r.deleteNode(ctx, &desiredNodeImage, node)
done, err := r.deleteNode(ctx, &desiredNodeVersion, node)
if err != nil {
logr.Error(err, "Unable to remove obsolete node")
}
@ -211,9 +211,9 @@ func (r *NodeImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
}
// SetupWithManager sets up the controller with the Manager.
func (r *NodeImageReconciler) SetupWithManager(mgr ctrl.Manager) error {
func (r *NodeVersionReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&updatev1alpha1.NodeImage{}).
For(&updatev1alpha1.NodeVersion{}).
Watches(
&source.Kind{Type: &updatev1alpha1.ScalingGroup{}},
handler.EnqueueRequestsFromMapFunc(r.findObjectsForScalingGroup),
@ -221,17 +221,17 @@ func (r *NodeImageReconciler) SetupWithManager(mgr ctrl.Manager) error {
).
Watches(
&source.Kind{Type: &updatev1alpha1.AutoscalingStrategy{}},
handler.EnqueueRequestsFromMapFunc(r.findAllNodeImages),
handler.EnqueueRequestsFromMapFunc(r.findAllNodeVersions),
builder.WithPredicates(autoscalerEnabledStatusChangedPredicate()),
).
Watches(
&source.Kind{Type: &corev1.Node{}},
handler.EnqueueRequestsFromMapFunc(r.findAllNodeImages),
handler.EnqueueRequestsFromMapFunc(r.findAllNodeVersions),
builder.WithPredicates(nodeReadyPredicate()),
).
Watches(
&source.Kind{Type: &nodemaintenancev1beta1.NodeMaintenance{}},
handler.EnqueueRequestsFromMapFunc(r.findAllNodeImages),
handler.EnqueueRequestsFromMapFunc(r.findAllNodeVersions),
builder.WithPredicates(nodeMaintenanceSucceededPredicate()),
).
Owns(&updatev1alpha1.PendingNode{}).
@ -239,7 +239,7 @@ func (r *NodeImageReconciler) SetupWithManager(mgr ctrl.Manager) error {
}
// annotateNodes takes all nodes of the cluster and annotates them with the scaling group they are in and the image they are using.
func (r *NodeImageReconciler) annotateNodes(ctx context.Context, nodes []corev1.Node) (annotatedNodes, invalidNodes []corev1.Node) {
func (r *NodeVersionReconciler) annotateNodes(ctx context.Context, nodes []corev1.Node) (annotatedNodes, invalidNodes []corev1.Node) {
logr := log.FromContext(ctx)
for _, node := range nodes {
annotations := make(map[string]string)
@ -285,7 +285,7 @@ func (r *NodeImageReconciler) annotateNodes(ctx context.Context, nodes []corev1.
// pairDonorsAndHeirs takes a list of outdated nodes (that do not yet have a heir node) and a list of mint nodes (nodes using the latest image) and pairs matching nodes to become donor and heir.
// outdatedNodes is also updated with heir annotations.
func (r *NodeImageReconciler) pairDonorsAndHeirs(ctx context.Context, controller metav1.Object, outdatedNodes []corev1.Node, mintNodes []mintNode) []replacementPair {
func (r *NodeVersionReconciler) pairDonorsAndHeirs(ctx context.Context, controller metav1.Object, outdatedNodes []corev1.Node, mintNodes []mintNode) []replacementPair {
logr := log.FromContext(ctx)
var pairs []replacementPair
for _, mintNode := range mintNodes {
@ -345,7 +345,7 @@ func (r *NodeImageReconciler) pairDonorsAndHeirs(ctx context.Context, controller
// matchDonorsAndHeirs takes separate lists of donors and heirs and matches each heir to its previously chosen donor.
// a list of replacement pairs is returned.
// donors and heirs with invalid pair references are cleaned up (the donor/heir annotations gets removed).
func (r *NodeImageReconciler) matchDonorsAndHeirs(ctx context.Context, pairs []replacementPair, donors, heirs []corev1.Node) []replacementPair {
func (r *NodeVersionReconciler) matchDonorsAndHeirs(ctx context.Context, pairs []replacementPair, donors, heirs []corev1.Node) []replacementPair {
logr := log.FromContext(ctx)
for _, heir := range heirs {
var foundPair bool
@ -389,7 +389,7 @@ func (r *NodeImageReconciler) matchDonorsAndHeirs(ctx context.Context, pairs []r
}
// ensureAutoscaling will ensure that the autoscaling is enabled or disabled as needed.
func (r *NodeImageReconciler) ensureAutoscaling(ctx context.Context, autoscalingEnabled bool, wantAutoscalingEnabled bool) error {
func (r *NodeVersionReconciler) ensureAutoscaling(ctx context.Context, autoscalingEnabled bool, wantAutoscalingEnabled bool) error {
if autoscalingEnabled == wantAutoscalingEnabled {
return nil
}
@ -418,7 +418,7 @@ func (r *NodeImageReconciler) ensureAutoscaling(ctx context.Context, autoscaling
// Labels are copied from the donor node to the heir node.
// Readiness of the heir node is awaited.
// Deletion of the donor node is scheduled.
func (r *NodeImageReconciler) replaceNode(ctx context.Context, controller metav1.Object, pair replacementPair) (bool, error) {
func (r *NodeVersionReconciler) replaceNode(ctx context.Context, controller metav1.Object, pair replacementPair) (bool, error) {
logr := log.FromContext(ctx)
if !reflect.DeepEqual(nodeutil.FilterLabels(pair.donor.Labels), nodeutil.FilterLabels(pair.heir.Labels)) {
if err := r.copyNodeLabels(ctx, pair.donor.Name, pair.heir.Name); err != nil {
@ -434,7 +434,7 @@ func (r *NodeImageReconciler) replaceNode(ctx context.Context, controller metav1
}
// deleteNode safely removes a node from the cluster and issues termination of the node by the CSP.
func (r *NodeImageReconciler) deleteNode(ctx context.Context, controller metav1.Object, node corev1.Node) (bool, error) {
func (r *NodeVersionReconciler) deleteNode(ctx context.Context, controller metav1.Object, node corev1.Node) (bool, error) {
logr := log.FromContext(ctx)
// cordon & drain node using node-maintenance-operator
var foundNodeMaintenance nodemaintenancev1beta1.NodeMaintenance
@ -509,7 +509,7 @@ func (r *NodeImageReconciler) deleteNode(ctx context.Context, controller metav1.
}
// createNewNodes creates new nodes using up to date images as replacement for outdated nodes.
func (r *NodeImageReconciler) createNewNodes(ctx context.Context, config newNodeConfig) error {
func (r *NodeVersionReconciler) createNewNodes(ctx context.Context, config newNodeConfig) error {
logr := log.FromContext(ctx)
if config.newNodesBudget < 1 || len(config.outdatedNodes) == 0 {
return nil
@ -543,8 +543,8 @@ func (r *NodeImageReconciler) createNewNodes(ctx context.Context, config newNode
logr.Info("Scaling group does not have matching resource", "scalingGroup", scalingGroupID, "scalingGroups", config.scalingGroupByID)
continue
}
if !strings.EqualFold(scalingGroup.Status.ImageReference, config.desiredNodeImage.Spec.ImageReference) {
logr.Info("Scaling group does not use latest image", "scalingGroup", scalingGroupID, "usedImage", scalingGroup.Status.ImageReference, "wantedImage", config.desiredNodeImage.Spec.ImageReference)
if !strings.EqualFold(scalingGroup.Status.ImageReference, config.desiredNodeVersion.Spec.ImageReference) {
logr.Info("Scaling group does not use latest image", "scalingGroup", scalingGroupID, "usedImage", scalingGroup.Status.ImageReference, "wantedImage", config.desiredNodeVersion.Spec.ImageReference)
continue
}
if requiredNodesPerScalingGroup[scalingGroupID] == 0 {
@ -573,7 +573,7 @@ func (r *NodeImageReconciler) createNewNodes(ctx context.Context, config newNode
Deadline: &deadline,
},
}
if err := ctrl.SetControllerReference(&config.desiredNodeImage, pendingNode, r.Scheme); err != nil {
if err := ctrl.SetControllerReference(&config.desiredNodeVersion, pendingNode, r.Scheme); err != nil {
return err
}
if err := r.Create(ctx, pendingNode); err != nil {
@ -588,7 +588,7 @@ func (r *NodeImageReconciler) createNewNodes(ctx context.Context, config newNode
}
// patchNodeAnnotations attempts to patch node annotations in a retry loop.
func (r *NodeImageReconciler) patchNodeAnnotations(ctx context.Context, nodeName string, annotations map[string]string) error {
func (r *NodeVersionReconciler) patchNodeAnnotations(ctx context.Context, nodeName string, annotations map[string]string) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
var node corev1.Node
if err := r.Get(ctx, types.NamespacedName{Name: nodeName}, &node); err != nil {
@ -601,7 +601,7 @@ func (r *NodeImageReconciler) patchNodeAnnotations(ctx context.Context, nodeName
}
// patchNodeAnnotations attempts to remove node annotations using a patch in a retry loop.
func (r *NodeImageReconciler) patchUnsetNodeAnnotations(ctx context.Context, nodeName string, annotationKeys []string) error {
func (r *NodeVersionReconciler) patchUnsetNodeAnnotations(ctx context.Context, nodeName string, annotationKeys []string) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
var node corev1.Node
if err := r.Get(ctx, types.NamespacedName{Name: nodeName}, &node); err != nil {
@ -614,7 +614,7 @@ func (r *NodeImageReconciler) patchUnsetNodeAnnotations(ctx context.Context, nod
}
// copyNodeLabels attempts to copy all node labels (except for reserved labels) from one node to another in a retry loop.
func (r *NodeImageReconciler) copyNodeLabels(ctx context.Context, oldNodeName, newNodeName string) error {
func (r *NodeVersionReconciler) copyNodeLabels(ctx context.Context, oldNodeName, newNodeName string) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
var oldNode corev1.Node
if err := r.Get(ctx, types.NamespacedName{Name: oldNodeName}, &oldNode); err != nil {
@ -630,35 +630,35 @@ func (r *NodeImageReconciler) copyNodeLabels(ctx context.Context, oldNodeName, n
})
}
// tryUpdateStatus attempts to update the NodeImage status field in a retry loop.
func (r *NodeImageReconciler) tryUpdateStatus(ctx context.Context, name types.NamespacedName, status updatev1alpha1.NodeImageStatus) error {
// tryUpdateStatus attempts to update the NodeVersion status field in a retry loop.
func (r *NodeVersionReconciler) tryUpdateStatus(ctx context.Context, name types.NamespacedName, status updatev1alpha1.NodeVersionStatus) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
var nodeImage updatev1alpha1.NodeImage
if err := r.Get(ctx, name, &nodeImage); err != nil {
var nodeVersion updatev1alpha1.NodeVersion
if err := r.Get(ctx, name, &nodeVersion); err != nil {
return err
}
nodeImage.Status = *status.DeepCopy()
if err := r.Status().Update(ctx, &nodeImage); err != nil {
nodeVersion.Status = *status.DeepCopy()
if err := r.Status().Update(ctx, &nodeVersion); err != nil {
return err
}
return nil
})
}
// nodeImageStatus generates the NodeImage.Status field given node groups and the budget for new nodes.
func nodeImageStatus(scheme *runtime.Scheme, groups nodeGroups, pendingNodes []updatev1alpha1.PendingNode, invalidNodes []corev1.Node, newNodesBudget int) updatev1alpha1.NodeImageStatus {
var status updatev1alpha1.NodeImageStatus
// nodeVersionStatus generates the NodeVersion.Status field given node groups and the budget for new nodes.
func nodeVersionStatus(scheme *runtime.Scheme, groups nodeGroups, pendingNodes []updatev1alpha1.PendingNode, invalidNodes []corev1.Node, newNodesBudget int) updatev1alpha1.NodeVersionStatus {
var status updatev1alpha1.NodeVersionStatus
outdatedCondition := metav1.Condition{
Type: updatev1alpha1.ConditionOutdated,
}
if len(groups.Outdated)+len(groups.Heirs)+len(pendingNodes)+len(groups.Obsolete) == 0 {
outdatedCondition.Status = metav1.ConditionFalse
outdatedCondition.Reason = conditionNodeImageUpToDateReason
outdatedCondition.Message = conditionNodeImageUpToDateMessage
outdatedCondition.Reason = conditionNodeVersionUpToDateReason
outdatedCondition.Message = conditionNodeVersionUpToDateMessage
} else {
outdatedCondition.Status = metav1.ConditionTrue
outdatedCondition.Reason = conditionNodeImageOutOfDateReason
outdatedCondition.Message = conditionNodeImageOutOfDateMessage
outdatedCondition.Reason = conditionNodeVersionOutOfDateReason
outdatedCondition.Message = conditionNodeVersionOutOfDateMessage
}
meta.SetStatusCondition(&status.Conditions, outdatedCondition)
for _, node := range groups.Outdated {
@ -739,20 +739,20 @@ type replacementPair struct {
// every properly annotated kubernetes node can be placed in exactly one of the sets.
type nodeGroups struct {
// Outdated nodes are nodes that
// do not use the most recent image AND
// do not use the most recent version AND
// are not yet a donor to an up to date heir node
Outdated,
// UpToDate nodes are nodes that
// use the most recent image,
// use the most recent version,
// are not an heir to an outdated donor node AND
// are not mint nodes
UpToDate,
// Donors are nodes that
// do not use the most recent image AND
// do not use the most recent version AND
// are paired up with an up to date heir node
Donors,
// Heirs are nodes that
// use the most recent image AND
// use the most recent version AND
// are paired up with an outdated donor node
Heirs,
// Obsolete nodes are nodes that
@ -761,21 +761,22 @@ type nodeGroups struct {
// They will be cleaned up by the operator.
Obsolete []corev1.Node
// Mint nodes are nodes that
// use the most recent image AND
// use the most recent version AND
// were created by the operator as replacements (heirs)
// and are awaiting pairing up with a donor node.
Mint []mintNode
}
// groupNodes classifies nodes by placing each into exactly one group.
func groupNodes(nodes []corev1.Node, pendingNodes []updatev1alpha1.PendingNode, latestImageReference string) nodeGroups {
func groupNodes(nodes []corev1.Node, pendingNodes []updatev1alpha1.PendingNode, latestImageReference string, latestK8sComponentsReference string) nodeGroups {
groups := nodeGroups{}
for _, node := range nodes {
if node.Annotations[obsoleteAnnotation] == "true" {
groups.Obsolete = append(groups.Obsolete, node)
continue
}
if !strings.EqualFold(node.Annotations[nodeImageAnnotation], latestImageReference) {
if !strings.EqualFold(node.Annotations[nodeImageAnnotation], latestImageReference) ||
!strings.EqualFold(node.Annotations[NodeKubernetesComponentsReferenceAnnotationKey], latestK8sComponentsReference) {
if heir := node.Annotations[heirAnnotation]; heir != "" {
groups.Donors = append(groups.Donors, node)
} else {
@ -816,9 +817,9 @@ type etcdRemover interface {
}
type newNodeConfig struct {
desiredNodeImage updatev1alpha1.NodeImage
outdatedNodes []corev1.Node
pendingNodes []updatev1alpha1.PendingNode
scalingGroupByID map[string]updatev1alpha1.ScalingGroup
newNodesBudget int
desiredNodeVersion updatev1alpha1.NodeVersion
outdatedNodes []corev1.Node
pendingNodes []updatev1alpha1.PendingNode
scalingGroupByID map[string]updatev1alpha1.ScalingGroup
newNodesBudget int
}

View file

@ -23,15 +23,15 @@ import (
nodemaintenancev1beta1 "github.com/medik8s/node-maintenance-operator/api/v1beta1"
)
var _ = Describe("NodeImage controller", func() {
var _ = Describe("NodeVersion controller", func() {
// Define utility constants for object names and testing timeouts/durations and intervals.
const (
nodeImageResourceName = "nodeimage"
firstNodeName = "node-1"
secondNodeName = "node-2"
firstImage = "image-1"
secondImage = "image-2"
scalingGroupID = "scaling-group"
nodeVersionResourceName = "nodeversion"
firstNodeName = "node-1"
secondNodeName = "node-2"
firstVersion = "version-1"
secondVersion = "version-2"
scalingGroupID = "scaling-group"
timeout = time.Second * 20
duration = time.Second * 2
@ -40,29 +40,29 @@ var _ = Describe("NodeImage controller", func() {
firstNodeLookupKey := types.NamespacedName{Name: firstNodeName}
secondNodeLookupKey := types.NamespacedName{Name: secondNodeName}
nodeImageLookupKey := types.NamespacedName{Name: nodeImageResourceName}
nodeVersionLookupKey := types.NamespacedName{Name: nodeVersionResourceName}
scalingGroupLookupKey := types.NamespacedName{Name: scalingGroupID}
joiningPendingNodeLookupKey := types.NamespacedName{Name: secondNodeName}
nodeMaintenanceLookupKey := types.NamespacedName{Name: firstNodeName}
Context("When updating the cluster-wide node image", func() {
Context("When updating the cluster-wide node version", func() {
It("Should update every node in the cluster", func() {
By("creating a node image resource specifying the first node image")
Expect(fakes.scalingGroupUpdater.SetScalingGroupImage(ctx, scalingGroupID, firstImage)).Should(Succeed())
nodeImage := &updatev1alpha1.NodeImage{
By("creating a node version resource specifying the first node version")
Expect(fakes.scalingGroupUpdater.SetScalingGroupImage(ctx, scalingGroupID, firstVersion)).Should(Succeed())
nodeVersion := &updatev1alpha1.NodeVersion{
TypeMeta: metav1.TypeMeta{
APIVersion: "update.edgeless.systems/v1alpha1",
Kind: "NodeImage",
Kind: "NodeVersion",
},
ObjectMeta: metav1.ObjectMeta{
Name: nodeImageResourceName,
Name: nodeVersionResourceName,
},
Spec: updatev1alpha1.NodeImageSpec{ImageReference: firstImage},
Spec: updatev1alpha1.NodeVersionSpec{ImageReference: firstVersion},
}
Expect(k8sClient.Create(ctx, nodeImage)).Should(Succeed())
Expect(k8sClient.Create(ctx, nodeVersion)).Should(Succeed())
By("creating a node resource using the first node image")
fakes.nodeReplacer.setNodeImage(firstNodeName, firstImage)
fakes.nodeReplacer.setNodeImage(firstNodeName, firstVersion)
fakes.nodeReplacer.setScalingGroupID(firstNodeName, scalingGroupID)
firstNode := &corev1.Node{
TypeMeta: metav1.TypeMeta{
@ -82,13 +82,13 @@ var _ = Describe("NodeImage controller", func() {
Expect(k8sClient.Create(ctx, firstNode)).Should(Succeed())
By("creating a scaling group resource using the first node image")
Expect(fakes.scalingGroupUpdater.SetScalingGroupImage(ctx, scalingGroupID, firstImage)).Should(Succeed())
Expect(fakes.scalingGroupUpdater.SetScalingGroupImage(ctx, scalingGroupID, firstVersion)).Should(Succeed())
scalingGroup := &updatev1alpha1.ScalingGroup{
ObjectMeta: metav1.ObjectMeta{
Name: scalingGroupID,
},
Spec: updatev1alpha1.ScalingGroupSpec{
NodeImage: nodeImageResourceName,
NodeVersion: nodeVersionResourceName,
GroupID: scalingGroupID,
Autoscaling: true,
},
@ -146,24 +146,24 @@ var _ = Describe("NodeImage controller", func() {
By("checking that all nodes are up-to-date")
Eventually(func() int {
if err := k8sClient.Get(ctx, nodeImageLookupKey, nodeImage); err != nil {
if err := k8sClient.Get(ctx, nodeVersionLookupKey, nodeVersion); err != nil {
return 0
}
return len(nodeImage.Status.UpToDate)
return len(nodeVersion.Status.UpToDate)
}, timeout, interval).Should(Equal(1))
By("updating the node image to the second image")
fakes.nodeStateGetter.setNodeState(updatev1alpha1.NodeStateReady)
fakes.nodeReplacer.setCreatedNode(secondNodeName, secondNodeName, nil)
nodeImage.Spec.ImageReference = secondImage
Expect(k8sClient.Update(ctx, nodeImage)).Should(Succeed())
nodeVersion.Spec.ImageReference = secondVersion
Expect(k8sClient.Update(ctx, nodeVersion)).Should(Succeed())
By("checking that there is an outdated node in the status")
Eventually(func() int {
if err := k8sClient.Get(ctx, nodeImageLookupKey, nodeImage); err != nil {
if err := k8sClient.Get(ctx, nodeVersionLookupKey, nodeVersion); err != nil {
return 0
}
return len(nodeImage.Status.Outdated)
return len(nodeVersion.Status.Outdated)
}, timeout, interval).Should(Equal(1))
By("checking that the scaling group is up to date")
@ -172,7 +172,7 @@ var _ = Describe("NodeImage controller", func() {
return ""
}
return scalingGroup.Status.ImageReference
}, timeout, interval).Should(Equal(secondImage))
}, timeout, interval).Should(Equal(secondVersion))
By("checking that a pending node is created")
pendingNode := &updatev1alpha1.PendingNode{}
@ -184,14 +184,14 @@ var _ = Describe("NodeImage controller", func() {
return pendingNode.Status.CSPNodeState
}).Should(Equal(updatev1alpha1.NodeStateReady))
Eventually(func() int {
if err := k8sClient.Get(ctx, nodeImageLookupKey, nodeImage); err != nil {
if err := k8sClient.Get(ctx, nodeVersionLookupKey, nodeVersion); err != nil {
return 0
}
return len(nodeImage.Status.Pending)
return len(nodeVersion.Status.Pending)
}, timeout, interval).Should(Equal(1))
By("creating a new node resource using the second node image")
fakes.nodeReplacer.setNodeImage(secondNodeName, secondImage)
fakes.nodeReplacer.setNodeImage(secondNodeName, secondVersion)
fakes.nodeReplacer.setScalingGroupID(secondNodeName, scalingGroupID)
secondNode := &corev1.Node{
TypeMeta: metav1.TypeMeta{
@ -214,7 +214,7 @@ var _ = Describe("NodeImage controller", func() {
}
return secondNode.Annotations
}, timeout, interval).Should(HaveKeyWithValue(scalingGroupAnnotation, scalingGroupID))
Expect(secondNode.Annotations).Should(HaveKeyWithValue(nodeImageAnnotation, secondImage))
Expect(secondNode.Annotations).Should(HaveKeyWithValue(nodeImageAnnotation, secondVersion))
By("checking that the nodes are paired as donor and heir")
Eventually(func() map[string]string {
@ -225,9 +225,9 @@ var _ = Describe("NodeImage controller", func() {
}, timeout, interval).Should(HaveKeyWithValue(heirAnnotation, secondNodeName))
Expect(k8sClient.Get(ctx, secondNodeLookupKey, secondNode)).Should(Succeed())
Expect(secondNode.Annotations).Should(HaveKeyWithValue(donorAnnotation, firstNodeName))
Expect(k8sClient.Get(ctx, nodeImageLookupKey, nodeImage)).Should(Succeed())
Expect(nodeImage.Status.Donors).Should(HaveLen(1))
Expect(nodeImage.Status.Heirs).Should(HaveLen(1))
Expect(k8sClient.Get(ctx, nodeVersionLookupKey, nodeVersion)).Should(Succeed())
Expect(nodeVersion.Status.Donors).Should(HaveLen(1))
Expect(nodeVersion.Status.Heirs).Should(HaveLen(1))
Expect(k8sClient.Get(ctx, joiningPendingNodeLookupKey, pendingNode)).Should(Not(Succeed()))
By("checking that node labels are copied to the heir")
@ -268,15 +268,15 @@ var _ = Describe("NodeImage controller", func() {
By("checking that all nodes are up-to-date")
Eventually(func() int {
err := k8sClient.Get(ctx, nodeImageLookupKey, nodeImage)
err := k8sClient.Get(ctx, nodeVersionLookupKey, nodeVersion)
if err != nil {
return 0
}
return len(nodeImage.Status.UpToDate)
return len(nodeVersion.Status.UpToDate)
}, timeout, interval).Should(Equal(1))
By("cleaning up all resources")
Expect(k8sClient.Delete(ctx, nodeImage)).Should(Succeed())
Expect(k8sClient.Delete(ctx, nodeVersion)).Should(Succeed())
Expect(k8sClient.Delete(ctx, scalingGroup)).Should(Succeed())
Expect(k8sClient.Delete(ctx, autoscalerDeployment)).Should(Succeed())
Expect(k8sClient.Delete(ctx, strategy)).Should(Succeed())

View file

@ -107,7 +107,7 @@ func TestAnnotateNodes(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
reconciler := NodeImageReconciler{
reconciler := NodeVersionReconciler{
nodeReplacer: &stubNodeReplacerReader{
nodeImage: "node-image",
scalingGroupID: "scaling-group-id",
@ -217,13 +217,13 @@ func TestPairDonorsAndHeirs(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
reconciler := NodeImageReconciler{
reconciler := NodeVersionReconciler{
nodeReplacer: &stubNodeReplacerReader{},
Client: &stubReadWriterClient{
stubReaderClient: *newStubReaderClient(t, []runtime.Object{&tc.outdatedNode, &tc.mintNode.node}, nil, nil),
},
}
nodeImage := updatev1alpha1.NodeImage{}
nodeImage := updatev1alpha1.NodeVersion{}
pairs := reconciler.pairDonorsAndHeirs(context.Background(), &nodeImage, []corev1.Node{tc.outdatedNode}, []mintNode{tc.mintNode})
if tc.wantPair == nil {
assert.Len(pairs, 0)
@ -307,7 +307,7 @@ func TestMatchDonorsAndHeirs(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
reconciler := NodeImageReconciler{
reconciler := NodeVersionReconciler{
nodeReplacer: &stubNodeReplacerReader{},
Client: &stubReadWriterClient{
stubReaderClient: *newStubReaderClient(t, []runtime.Object{&tc.donor, &tc.heir}, nil, nil),
@ -578,12 +578,12 @@ func TestCreateNewNodes(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
desiredNodeImage := updatev1alpha1.NodeImage{
Spec: updatev1alpha1.NodeImageSpec{
desiredNodeImage := updatev1alpha1.NodeVersion{
Spec: updatev1alpha1.NodeVersionSpec{
ImageReference: "image",
},
}
reconciler := NodeImageReconciler{
reconciler := NodeVersionReconciler{
nodeReplacer: &stubNodeReplacerWriter{},
Client: &stubReadWriterClient{
stubReaderClient: *newStubReaderClient(t, []runtime.Object{}, nil, nil),
@ -600,6 +600,7 @@ func TestCreateNewNodes(t *testing.T) {
func TestGroupNodes(t *testing.T) {
latestImageReference := "latest-image"
latestK8sComponentsReference := "latest-k8s-components-ref"
scalingGroup := "scaling-group"
wantNodeGroups := nodeGroups{
Outdated: []corev1.Node{
@ -607,8 +608,19 @@ func TestGroupNodes(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "outdated",
Annotations: map[string]string{
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: "old-image",
scalingGroupAnnotation: scalingGroup,
NodeKubernetesComponentsReferenceAnnotationKey: latestK8sComponentsReference,
nodeImageAnnotation: "old-image",
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "outdated",
Annotations: map[string]string{
scalingGroupAnnotation: scalingGroup,
NodeKubernetesComponentsReferenceAnnotationKey: "old-ref",
nodeImageAnnotation: latestImageReference,
},
},
},
@ -618,8 +630,9 @@ func TestGroupNodes(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "uptodate",
Annotations: map[string]string{
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: latestImageReference,
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: latestImageReference,
NodeKubernetesComponentsReferenceAnnotationKey: latestK8sComponentsReference,
},
},
},
@ -629,9 +642,21 @@ func TestGroupNodes(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "donor",
Annotations: map[string]string{
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: "old-image",
heirAnnotation: "heir",
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: "old-image",
NodeKubernetesComponentsReferenceAnnotationKey: latestK8sComponentsReference,
heirAnnotation: "heir",
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "donor",
Annotations: map[string]string{
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: latestImageReference,
NodeKubernetesComponentsReferenceAnnotationKey: "old-ref",
heirAnnotation: "heir",
},
},
},
@ -641,9 +666,10 @@ func TestGroupNodes(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "heir",
Annotations: map[string]string{
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: latestImageReference,
donorAnnotation: "donor",
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: latestImageReference,
NodeKubernetesComponentsReferenceAnnotationKey: latestK8sComponentsReference,
donorAnnotation: "donor",
},
},
},
@ -653,9 +679,10 @@ func TestGroupNodes(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "obsolete",
Annotations: map[string]string{
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: latestImageReference,
obsoleteAnnotation: "true",
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: latestImageReference,
NodeKubernetesComponentsReferenceAnnotationKey: latestK8sComponentsReference,
obsoleteAnnotation: "true",
},
},
},
@ -666,8 +693,9 @@ func TestGroupNodes(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "mint",
Annotations: map[string]string{
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: latestImageReference,
scalingGroupAnnotation: scalingGroup,
nodeImageAnnotation: latestImageReference,
NodeKubernetesComponentsReferenceAnnotationKey: latestK8sComponentsReference,
},
},
},
@ -695,7 +723,7 @@ func TestGroupNodes(t *testing.T) {
}
assert := assert.New(t)
groups := groupNodes(nodes, pendingNodes, latestImageReference)
groups := groupNodes(nodes, pendingNodes, latestImageReference, latestK8sComponentsReference)
assert.Equal(wantNodeGroups, groups)
}

View file

@ -94,22 +94,22 @@ func nodeMaintenanceSucceededPredicate() predicate.Predicate {
}
// findObjectsForScalingGroup requests a reconcile call for the node image referenced by a scaling group.
func (r *NodeImageReconciler) findObjectsForScalingGroup(rawScalingGroup client.Object) []reconcile.Request {
func (r *NodeVersionReconciler) findObjectsForScalingGroup(rawScalingGroup client.Object) []reconcile.Request {
scalingGroup := rawScalingGroup.(*updatev1alpha1.ScalingGroup)
return []reconcile.Request{
{NamespacedName: types.NamespacedName{Name: scalingGroup.Spec.NodeImage}},
{NamespacedName: types.NamespacedName{Name: scalingGroup.Spec.NodeVersion}},
}
}
// findAllNodeImages requests a reconcile call for all node images.
func (r *NodeImageReconciler) findAllNodeImages(_ client.Object) []reconcile.Request {
var nodeImageList updatev1alpha1.NodeImageList
err := r.List(context.TODO(), &nodeImageList)
// findAllNodeVersions requests a reconcile call for all node versions.
func (r *NodeVersionReconciler) findAllNodeVersions(_ client.Object) []reconcile.Request {
var nodeVersionList updatev1alpha1.NodeVersionList
err := r.List(context.TODO(), &nodeVersionList)
if err != nil {
return []reconcile.Request{}
}
requests := make([]reconcile.Request, len(nodeImageList.Items))
for i, item := range nodeImageList.Items {
requests := make([]reconcile.Request, len(nodeVersionList.Items))
for i, item := range nodeVersionList.Items {
requests[i] = reconcile.Request{
NamespacedName: types.NamespacedName{Name: item.GetName()},
}

View file

@ -237,39 +237,39 @@ func TestNodeMaintenanceSucceededPredicate(t *testing.T) {
func TestFindObjectsForScalingGroup(t *testing.T) {
scalingGroup := updatev1alpha1.ScalingGroup{
Spec: updatev1alpha1.ScalingGroupSpec{
NodeImage: "nodeimage",
NodeVersion: "nodeversion",
},
}
wantRequests := []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: "nodeimage",
Name: "nodeversion",
},
},
}
assert := assert.New(t)
reconciler := NodeImageReconciler{}
reconciler := NodeVersionReconciler{}
requests := reconciler.findObjectsForScalingGroup(&scalingGroup)
assert.ElementsMatch(wantRequests, requests)
}
func TestFindAllNodeImages(t *testing.T) {
func TestFindAllNodeVersions(t *testing.T) {
testCases := map[string]struct {
nodeImage client.Object
listNodeImagesErr error
wantRequests []reconcile.Request
nodeVersion client.Object
listNodeVersionsErr error
wantRequests []reconcile.Request
}{
"getting the corresponding node images fails": {
listNodeImagesErr: errors.New("get-node-images-err"),
listNodeVersionsErr: errors.New("get-node-version-err"),
},
"node image reconcile request is returned": {
nodeImage: &updatev1alpha1.NodeImage{
ObjectMeta: metav1.ObjectMeta{Name: "nodeimage"},
nodeVersion: &updatev1alpha1.NodeVersion{
ObjectMeta: metav1.ObjectMeta{Name: "nodeversion"},
},
wantRequests: []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: "nodeimage",
Name: "nodeversion",
},
},
},
@ -280,10 +280,10 @@ func TestFindAllNodeImages(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
reconciler := NodeImageReconciler{
Client: newStubReaderClient(t, []runtime.Object{tc.nodeImage}, nil, tc.listNodeImagesErr),
reconciler := NodeVersionReconciler{
Client: newStubReaderClient(t, []runtime.Object{tc.nodeVersion}, nil, tc.listNodeVersionsErr),
}
requests := reconciler.findAllNodeImages(nil)
requests := reconciler.findAllNodeVersions(nil)
assert.ElementsMatch(tc.wantRequests, requests)
})
}

View file

@ -28,7 +28,7 @@ import (
)
const (
nodeImageField = ".spec.nodeImage"
nodeVersionField = ".spec.nodeVersion"
conditionScalingGroupUpToDateReason = "ScalingGroupNodeImageUpToDate"
conditionScalingGroupUpToDateMessage = "Scaling group will use the latest image when creating new nodes"
conditionScalingGroupOutOfDateReason = "ScalingGroupNodeImageOutOfDate"
@ -54,10 +54,10 @@ func NewScalingGroupReconciler(scalingGroupUpdater scalingGroupUpdater, client c
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=scalinggroups,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=scalinggroups/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=scalinggroups/finalizers,verbs=update
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeimage,verbs=get;list;watch
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeimages/status,verbs=get
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeversion,verbs=get;list;watch
//+kubebuilder:rbac:groups=update.edgeless.systems,resources=nodeversion/status,verbs=get
// Reconcile reads the latest node image from the referenced NodeImage spec and updates the scaling group to match.
// Reconcile reads the latest node image from the referenced NodeVersion spec and updates the scaling group to match.
func (r *ScalingGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logr := log.FromContext(ctx)
@ -66,9 +66,9 @@ func (r *ScalingGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request
logr.Error(err, "Unable to fetch ScalingGroup")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
var desiredNodeImage updatev1alpha1.NodeImage
if err := r.Get(ctx, client.ObjectKey{Name: desiredScalingGroup.Spec.NodeImage}, &desiredNodeImage); err != nil {
logr.Error(err, "Unable to fetch NodeImage")
var desiredNodeVersion updatev1alpha1.NodeVersion
if err := r.Get(ctx, client.ObjectKey{Name: desiredScalingGroup.Spec.NodeVersion}, &desiredNodeVersion); err != nil {
logr.Error(err, "Unable to fetch NodeVersion")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
nodeImage, err := r.scalingGroupUpdater.GetScalingGroupImage(ctx, desiredScalingGroup.Spec.GroupID)
@ -81,7 +81,7 @@ func (r *ScalingGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request
outdatedCondition := metav1.Condition{
Type: updatev1alpha1.ConditionOutdated,
}
imagesMatch := strings.EqualFold(nodeImage, desiredNodeImage.Spec.ImageReference)
imagesMatch := strings.EqualFold(nodeImage, desiredNodeVersion.Spec.ImageReference)
if imagesMatch {
outdatedCondition.Status = metav1.ConditionFalse
outdatedCondition.Reason = conditionScalingGroupUpToDateReason
@ -99,7 +99,7 @@ func (r *ScalingGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request
if !imagesMatch {
logr.Info("ScalingGroup NodeImage is out of date")
if err := r.scalingGroupUpdater.SetScalingGroupImage(ctx, desiredScalingGroup.Spec.GroupID, desiredNodeImage.Spec.ImageReference); err != nil {
if err := r.scalingGroupUpdater.SetScalingGroupImage(ctx, desiredScalingGroup.Spec.GroupID, desiredNodeVersion.Spec.ImageReference); err != nil {
logr.Error(err, "Unable to set ScalingGroup NodeImage")
return ctrl.Result{}, err
}
@ -111,31 +111,31 @@ func (r *ScalingGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request
// SetupWithManager sets up the controller with the Manager.
func (r *ScalingGroupReconciler) SetupWithManager(mgr ctrl.Manager) error {
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &updatev1alpha1.ScalingGroup{}, nodeImageField, func(rawObj client.Object) []string {
// Extract the NodeImage name from the ScalingGroup Spec, if one is provided
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &updatev1alpha1.ScalingGroup{}, nodeVersionField, func(rawObj client.Object) []string {
// Extract the NodeVersion name from the ScalingGroup Spec, if one is provided
scalingGroup := rawObj.(*updatev1alpha1.ScalingGroup)
if scalingGroup.Spec.NodeImage == "" {
if scalingGroup.Spec.NodeVersion == "" {
return nil
}
return []string{scalingGroup.Spec.NodeImage}
return []string{scalingGroup.Spec.NodeVersion}
}); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&updatev1alpha1.ScalingGroup{}).
Watches(
&source.Kind{Type: &updatev1alpha1.NodeImage{}},
handler.EnqueueRequestsFromMapFunc(r.findObjectsForNodeImage),
&source.Kind{Type: &updatev1alpha1.NodeVersion{}},
handler.EnqueueRequestsFromMapFunc(r.findObjectsForNodeVersion),
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
).
Complete(r)
}
// findObjectsForNodeImage requests reconcile calls for every scaling group referencing the node image.
func (r *ScalingGroupReconciler) findObjectsForNodeImage(nodeImage client.Object) []reconcile.Request {
// findObjectsForNodeVersion requests reconcile calls for every scaling group referencing the node image.
func (r *ScalingGroupReconciler) findObjectsForNodeVersion(nodeVersion client.Object) []reconcile.Request {
attachedScalingGroups := &updatev1alpha1.ScalingGroupList{}
listOps := &client.ListOptions{
FieldSelector: fields.OneTermEqualSelector(nodeImageField, nodeImage.GetName()),
FieldSelector: fields.OneTermEqualSelector(nodeVersionField, nodeVersion.GetName()),
}
if err := r.List(context.TODO(), attachedScalingGroups, listOps); err != nil {
return []reconcile.Request{}

View file

@ -23,7 +23,7 @@ import (
var _ = Describe("ScalingGroup controller", func() {
// Define utility constants for object names and testing timeouts/durations and intervals.
const (
nodeImageName = "node-image"
nodeVersionName = "node-version"
scalingGroupName = "test-group"
timeout = time.Second * 20
@ -31,30 +31,30 @@ var _ = Describe("ScalingGroup controller", func() {
interval = time.Millisecond * 250
)
nodeImageLookupKey := types.NamespacedName{Name: nodeImageName}
nodeVersionLookupKey := types.NamespacedName{Name: nodeVersionName}
Context("When changing a node image resource spec", func() {
It("Should update corresponding scaling group images", func() {
By("creating a node image resource")
ctx := context.Background()
nodeImage := &updatev1alpha1.NodeImage{
nodeVersion := &updatev1alpha1.NodeVersion{
TypeMeta: metav1.TypeMeta{
APIVersion: "update.edgeless.systems/v1alpha1",
Kind: "NodeImage",
Kind: "NodeVersion",
},
ObjectMeta: metav1.ObjectMeta{
Name: nodeImageName,
Name: nodeVersionName,
},
Spec: updatev1alpha1.NodeImageSpec{
Spec: updatev1alpha1.NodeVersionSpec{
ImageReference: "image-1",
},
}
Expect(k8sClient.Create(ctx, nodeImage)).Should(Succeed())
createdNodeImage := &updatev1alpha1.NodeImage{}
Expect(k8sClient.Create(ctx, nodeVersion)).Should(Succeed())
createdNodeVersion := &updatev1alpha1.NodeVersion{}
Eventually(func() error {
return k8sClient.Get(ctx, nodeImageLookupKey, createdNodeImage)
return k8sClient.Get(ctx, nodeVersionLookupKey, createdNodeVersion)
}, timeout, interval).Should(Succeed())
Expect(createdNodeImage.Spec.ImageReference).Should(Equal("image-1"))
Expect(createdNodeVersion.Spec.ImageReference).Should(Equal("image-1"))
By("creating a scaling group")
scalingGroup := &updatev1alpha1.ScalingGroup{
@ -66,8 +66,8 @@ var _ = Describe("ScalingGroup controller", func() {
Name: scalingGroupName,
},
Spec: updatev1alpha1.ScalingGroupSpec{
NodeImage: nodeImageName,
GroupID: "group-id",
NodeVersion: nodeVersionName,
GroupID: "group-id",
},
}
Expect(k8sClient.Create(ctx, scalingGroup)).Should(Succeed())
@ -98,9 +98,9 @@ var _ = Describe("ScalingGroup controller", func() {
}, duration, interval).Should(Equal("image-1"))
By("updating the node image")
Expect(k8sClient.Get(ctx, nodeImageLookupKey, nodeImage)).Should(Succeed())
nodeImage.Spec.ImageReference = "image-2"
Expect(k8sClient.Update(ctx, nodeImage)).Should(Succeed())
Expect(k8sClient.Get(ctx, nodeVersionLookupKey, nodeVersion)).Should(Succeed())
nodeVersion.Spec.ImageReference = "image-2"
Expect(k8sClient.Update(ctx, nodeVersion)).Should(Succeed())
By("checking the scaling group eventually uses the latest image")
Eventually(func() string {
@ -118,7 +118,7 @@ var _ = Describe("ScalingGroup controller", func() {
}, duration, interval).Should(Equal("image-2"))
By("cleaning up all resources")
Expect(k8sClient.Delete(ctx, createdNodeImage)).Should(Succeed())
Expect(k8sClient.Delete(ctx, createdNodeVersion)).Should(Succeed())
Expect(k8sClient.Delete(ctx, scalingGroup)).Should(Succeed())
})
})

View file

@ -115,7 +115,7 @@ var _ = BeforeSuite(func() {
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())
err = (&NodeImageReconciler{
err = (&NodeVersionReconciler{
nodeReplacer: fakes.nodeReplacer,
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),