Skip to content

Commit 5d67371

Browse files
committed
Implement Updating Machine phase
Signed-off-by: Stefan Büringer buringerst@vmware.com
1 parent 6c59755 commit 5d67371

File tree

8 files changed

+133
-3
lines changed

8 files changed

+133
-3
lines changed

api/core/v1beta1/conversion.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ func (src *Machine) ConvertTo(dstRaw conversion.Hub) error {
408408
// Recover other values.
409409
if ok {
410410
dst.Spec.MinReadySeconds = restored.Spec.MinReadySeconds
411+
// Restore the phase, this also means that any client using v1beta1 during a round-trip
412+
// won't be able to write the Phase field. But that's okay as the only client writing the Phase
413+
// field should be the Machine controller.
414+
dst.Status.Phase = restored.Status.Phase
411415
}
412416

413417
return nil
@@ -1653,6 +1657,13 @@ func Convert_v1beta2_MachineStatus_To_v1beta1_MachineStatus(in *clusterv1.Machin
16531657
if err := autoConvert_v1beta2_MachineStatus_To_v1beta1_MachineStatus(in, out, s); err != nil {
16541658
return err
16551659
}
1660+
1661+
// Convert v1beta2 Updating phase to v1beta1 Running as Updating did not exist in v1beta1.
1662+
// We don't have to support a round-trip as only the core CAPI controller should write the Phase field.
1663+
if out.Phase == "Updating" {
1664+
out.Phase = "Running"
1665+
}
1666+
16561667
if !reflect.DeepEqual(in.LastUpdated, metav1.Time{}) {
16571668
out.LastUpdated = ptr.To(in.LastUpdated)
16581669
}

api/core/v1beta1/conversion_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,9 @@ func hubMachineSpec(in *clusterv1.MachineSpec, c randfill.Continue) {
514514

515515
func hubMachineStatus(in *clusterv1.MachineStatus, c randfill.Continue) {
516516
c.FillNoCustom(in)
517+
518+
in.Phase = []string{"Updating", "Running"}[c.Intn(2)]
519+
517520
// Drop empty structs with only omit empty fields.
518521
if in.Deprecated != nil {
519522
if in.Deprecated.V1Beta1 == nil || reflect.DeepEqual(in.Deprecated.V1Beta1, &clusterv1.MachineV1Beta1DeprecatedStatus{}) {

api/core/v1beta2/machine_phase_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ const (
4545
// become a Kubernetes Node in a Ready state.
4646
MachinePhaseRunning = MachinePhase("Running")
4747

48+
// MachinePhaseUpdating is the Machine state when the Machine
49+
// is updating.
50+
MachinePhaseUpdating = MachinePhase("Updating")
51+
4852
// MachinePhaseDeleting is the Machine state when a delete
4953
// request has been sent to the API Server,
5054
// but its infrastructure has not yet been fully deleted.

api/core/v1beta2/machine_types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ type MachineStatus struct {
570570

571571
// phase represents the current phase of machine actuation.
572572
// +optional
573-
// +kubebuilder:validation:Enum=Pending;Provisioning;Provisioned;Running;Deleting;Deleted;Failed;Unknown
573+
// +kubebuilder:validation:Enum=Pending;Provisioning;Provisioned;Running;Updating;Deleting;Deleted;Failed;Unknown
574574
Phase string `json:"phase,omitempty"`
575575

576576
// certificatesExpiryDate is the expiry date of the machine certificates.
@@ -728,6 +728,7 @@ func (m *MachineStatus) GetTypedPhase() MachinePhase {
728728
MachinePhaseProvisioning,
729729
MachinePhaseProvisioned,
730730
MachinePhaseRunning,
731+
MachinePhaseUpdating,
731732
MachinePhaseDeleting,
732733
MachinePhaseDeleted,
733734
MachinePhaseFailed:

config/crd/bases/cluster.x-k8s.io_machines.yaml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controllers/machine/machine_controller_status.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,10 @@ func setMachinePhaseAndLastUpdated(_ context.Context, m *clusterv1.Machine) {
837837
m.Status.SetTypedPhase(clusterv1.MachinePhaseRunning)
838838
}
839839

840+
if conditions.IsTrue(m, clusterv1.MachineUpdatingCondition) {
841+
m.Status.SetTypedPhase(clusterv1.MachinePhaseUpdating)
842+
}
843+
840844
// Set the phase to "deleting" if the deletion timestamp is set.
841845
if !m.DeletionTimestamp.IsZero() {
842846
m.Status.SetTypedPhase(clusterv1.MachinePhaseDeleting)

internal/controllers/machine/machine_controller_status_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ import (
2626
corev1 "k8s.io/api/core/v1"
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
29+
utilfeature "k8s.io/component-base/featuregate/testing"
2930
"k8s.io/utils/ptr"
3031
"sigs.k8s.io/controller-runtime/pkg/client"
3132

3233
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
34+
runtimev1 "sigs.k8s.io/cluster-api/api/runtime/v1beta2"
3335
"sigs.k8s.io/cluster-api/controllers/clustercache"
36+
"sigs.k8s.io/cluster-api/feature"
3437
"sigs.k8s.io/cluster-api/util"
38+
"sigs.k8s.io/cluster-api/util/annotations"
3539
"sigs.k8s.io/cluster-api/util/conditions"
3640
v1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1"
3741
"sigs.k8s.io/cluster-api/util/kubeconfig"
@@ -2444,6 +2448,104 @@ func TestReconcileMachinePhases(t *testing.T) {
24442448
}, 10*time.Second).Should(BeTrue())
24452449
})
24462450

2451+
t.Run("Should set `Updating` when Machine is in-place updating", func(t *testing.T) {
2452+
utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.InPlaceUpdates, true)
2453+
2454+
g := NewWithT(t)
2455+
2456+
ns, err := env.CreateNamespace(ctx, "test-reconcile-machine-phases")
2457+
g.Expect(err).ToNot(HaveOccurred())
2458+
defer func() {
2459+
g.Expect(env.Cleanup(ctx, ns)).To(Succeed())
2460+
}()
2461+
2462+
nodeProviderID := fmt.Sprintf("test://%s", util.RandomString(6))
2463+
2464+
cluster := defaultCluster.DeepCopy()
2465+
cluster.Namespace = ns.Name
2466+
2467+
bootstrapConfig := defaultBootstrap.DeepCopy()
2468+
bootstrapConfig.SetNamespace(ns.Name)
2469+
infraMachine := defaultInfra.DeepCopy()
2470+
infraMachine.SetNamespace(ns.Name)
2471+
g.Expect(unstructured.SetNestedField(infraMachine.Object, nodeProviderID, "spec", "providerID")).To(Succeed())
2472+
machine := defaultMachine.DeepCopy()
2473+
machine.Namespace = ns.Name
2474+
2475+
// Create Node.
2476+
node := &corev1.Node{
2477+
ObjectMeta: metav1.ObjectMeta{
2478+
GenerateName: "machine-test-node-",
2479+
},
2480+
Spec: corev1.NodeSpec{ProviderID: nodeProviderID},
2481+
}
2482+
g.Expect(env.Create(ctx, node)).To(Succeed())
2483+
defer func() {
2484+
g.Expect(env.Cleanup(ctx, node)).To(Succeed())
2485+
}()
2486+
2487+
g.Expect(env.Create(ctx, cluster)).To(Succeed())
2488+
defaultKubeconfigSecret = kubeconfig.GenerateSecret(cluster, kubeconfig.FromEnvTestConfig(env.Config, cluster))
2489+
g.Expect(env.Create(ctx, defaultKubeconfigSecret)).To(Succeed())
2490+
// Set InfrastructureReady to true so ClusterCache creates the clusterAccessor.
2491+
patch := client.MergeFrom(cluster.DeepCopy())
2492+
cluster.Status.Initialization.InfrastructureProvisioned = ptr.To(true)
2493+
g.Expect(env.Status().Patch(ctx, cluster, patch)).To(Succeed())
2494+
2495+
g.Expect(env.Create(ctx, bootstrapConfig)).To(Succeed())
2496+
g.Expect(env.Create(ctx, infraMachine)).To(Succeed())
2497+
// We have to subtract 2 seconds, because .status.lastUpdated does not contain milliseconds.
2498+
preUpdate := time.Now().Add(-2 * time.Second)
2499+
// Create and wait on machine to make sure caches sync and reconciliation triggers.
2500+
g.Expect(env.CreateAndWait(ctx, machine)).To(Succeed())
2501+
2502+
modifiedMachine := machine.DeepCopy()
2503+
// Set NodeRef.
2504+
machine.Status.NodeRef = clusterv1.MachineNodeReference{Name: node.Name}
2505+
g.Expect(env.Status().Patch(ctx, modifiedMachine, client.MergeFrom(machine))).To(Succeed())
2506+
2507+
// Set bootstrap ready.
2508+
modifiedBootstrapConfig := bootstrapConfig.DeepCopy()
2509+
g.Expect(unstructured.SetNestedField(modifiedBootstrapConfig.Object, true, "status", "initialization", "dataSecretCreated")).To(Succeed())
2510+
g.Expect(unstructured.SetNestedField(modifiedBootstrapConfig.Object, "secret-data", "status", "dataSecretName")).To(Succeed())
2511+
g.Expect(env.Status().Patch(ctx, modifiedBootstrapConfig, client.MergeFrom(bootstrapConfig))).To(Succeed())
2512+
2513+
// Set infra ready.
2514+
modifiedInfraMachine := infraMachine.DeepCopy()
2515+
g.Expect(unstructured.SetNestedField(modifiedInfraMachine.Object, true, "status", "initialization", "provisioned")).To(Succeed())
2516+
g.Expect(env.Status().Patch(ctx, modifiedInfraMachine, client.MergeFrom(infraMachine))).To(Succeed())
2517+
2518+
// Set annotations on Machine, BootstrapConfig and InfraMachine to trigger an in-place update.
2519+
orig := modifiedBootstrapConfig.DeepCopy()
2520+
annotations.AddAnnotations(modifiedBootstrapConfig, map[string]string{
2521+
clusterv1.UpdateInProgressAnnotation: "",
2522+
})
2523+
g.Expect(env.Patch(ctx, modifiedBootstrapConfig, client.MergeFrom(orig))).To(Succeed())
2524+
orig = modifiedInfraMachine.DeepCopy()
2525+
annotations.AddAnnotations(modifiedInfraMachine, map[string]string{
2526+
clusterv1.UpdateInProgressAnnotation: "",
2527+
})
2528+
g.Expect(env.Patch(ctx, modifiedInfraMachine, client.MergeFrom(orig))).To(Succeed())
2529+
origMachine := modifiedMachine.DeepCopy()
2530+
annotations.AddAnnotations(modifiedMachine, map[string]string{
2531+
runtimev1.PendingHooksAnnotation: "UpdateMachine",
2532+
clusterv1.UpdateInProgressAnnotation: "",
2533+
})
2534+
g.Expect(env.Patch(ctx, modifiedMachine, client.MergeFrom(origMachine))).To(Succeed())
2535+
2536+
// Wait until Machine was reconciled.
2537+
g.Eventually(func(g Gomega) bool {
2538+
if err := env.DirectAPIServerGet(ctx, client.ObjectKeyFromObject(machine), machine); err != nil {
2539+
return false
2540+
}
2541+
g.Expect(machine.Status.GetTypedPhase()).To(Equal(clusterv1.MachinePhaseUpdating))
2542+
// Verify that the LastUpdated timestamp was updated
2543+
g.Expect(machine.Status.LastUpdated.IsZero()).To(BeFalse())
2544+
g.Expect(machine.Status.LastUpdated.After(preUpdate)).To(BeTrue())
2545+
return true
2546+
}, 10*time.Second).Should(BeTrue())
2547+
})
2548+
24472549
t.Run("Should set `Provisioned` when there is a ProviderID and there is no Node", func(t *testing.T) {
24482550
g := NewWithT(t)
24492551

internal/controllers/machine/suite_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import (
3939
"sigs.k8s.io/cluster-api/api/core/v1beta2/index"
4040
"sigs.k8s.io/cluster-api/controllers/clustercache"
4141
"sigs.k8s.io/cluster-api/controllers/remote"
42+
runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
43+
fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake"
4244
"sigs.k8s.io/cluster-api/internal/test/envtest"
4345
)
4446

@@ -99,8 +101,10 @@ func TestMain(m *testing.M) {
99101
clusterCache.(interface{ DisablePrivateKeyGeneration() }).DisablePrivateKeyGeneration()
100102

101103
if err := (&Reconciler{
102-
Client: mgr.GetClient(),
103-
APIReader: mgr.GetAPIReader(),
104+
Client: mgr.GetClient(),
105+
APIReader: mgr.GetAPIReader(),
106+
// Just adding a minimal RuntimeClient to avoid panics in tests.
107+
RuntimeClient: fakeruntimeclient.NewRuntimeClientBuilder().WithCatalog(runtimecatalog.New()).Build(),
104108
ClusterCache: clusterCache,
105109
RemoteConditionsGracePeriod: 5 * time.Minute,
106110
AdditionalSyncMachineLabels: nil,

0 commit comments

Comments
 (0)