Skip to content

Commit d591252

Browse files
committed
Allow exiting the externally provisioned state
This change unblocks setting externallyProvisioned to false to allow a host to follow the regular lifecycle. During the transition, image and customDeploy are copied over to the status to make sure the host does not get unconditionally deprovisioned. Hosts without image and customDeploy will still be deprovisioned. Signed-off-by: Dmitry Tantsur <dtantsur@protonmail.com>
1 parent c202b9a commit d591252

5 files changed

Lines changed: 67 additions & 32 deletions

File tree

internal/controller/metal3.io/baremetalhost_controller_test.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,7 +1581,7 @@ func TestExternallyProvisionedTransitions(t *testing.T) {
15811581
waitForProvisioningState(t, r, host, metal3api.StateExternallyProvisioned)
15821582
})
15831583

1584-
t.Run("externally provisioned to inspecting", func(t *testing.T) {
1584+
t.Run("externally provisioned to provisioned", func(t *testing.T) {
15851585
host := newDefaultHost(t)
15861586
host.Spec.Online = true
15871587
host.Spec.ExternallyProvisioned = true
@@ -1596,7 +1596,49 @@ func TestExternallyProvisionedTransitions(t *testing.T) {
15961596
}
15971597
t.Log("set externally provisioned to false")
15981598

1599-
waitForProvisioningState(t, r, host, metal3api.StateInspecting)
1599+
waitForProvisioningState(t, r, host, metal3api.StateProvisioned)
1600+
})
1601+
1602+
t.Run("externally provisioned to provisioned copies image to status", func(t *testing.T) {
1603+
host := newDefaultHost(t)
1604+
host.Spec.Online = true
1605+
host.Spec.ExternallyProvisioned = true
1606+
host.Spec.Image = &metal3api.Image{URL: "foo", Checksum: "123"}
1607+
r := newTestReconciler(t, host)
1608+
1609+
waitForProvisioningState(t, r, host, metal3api.StateExternallyProvisioned)
1610+
1611+
host.Spec.ExternallyProvisioned = false
1612+
err := r.Update(t.Context(), host)
1613+
if err != nil {
1614+
t.Fatal(err)
1615+
}
1616+
1617+
tryReconcile(t, r, host,
1618+
func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
1619+
return host.Status.Provisioning.State == metal3api.StateProvisioned &&
1620+
host.Status.Provisioning.Image.URL == "foo"
1621+
},
1622+
)
1623+
assert.Equal(t, "foo", host.Status.Provisioning.Image.URL)
1624+
assert.Equal(t, "123", host.Status.Provisioning.Image.Checksum)
1625+
})
1626+
1627+
t.Run("externally provisioned without image deprovisions", func(t *testing.T) {
1628+
host := newDefaultHost(t)
1629+
host.Spec.Online = true
1630+
host.Spec.ExternallyProvisioned = true
1631+
r := newTestReconciler(t, host)
1632+
1633+
waitForProvisioningState(t, r, host, metal3api.StateExternallyProvisioned)
1634+
1635+
host.Spec.ExternallyProvisioned = false
1636+
err := r.Update(t.Context(), host)
1637+
if err != nil {
1638+
t.Fatal(err)
1639+
}
1640+
1641+
waitForProvisioningState(t, r, host, metal3api.StateDeprovisioning)
16001642
})
16011643

16021644
t.Run("preparing to externally provisioned", func(t *testing.T) {

internal/controller/metal3.io/host_state_machine.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -464,11 +464,17 @@ func (hsm *hostStateMachine) handleExternallyProvisioned(ctx context.Context, in
464464
return hsm.Reconciler.actionManageSteadyState(ctx, hsm.Provisioner, info)
465465
}
466466

467-
if hsm.Host.NeedsHardwareInspection() {
468-
hsm.NextState = metal3api.StateInspecting
469-
} else {
470-
hsm.NextState = metal3api.StatePreparing
467+
// The host is exiting externally provisioned at this point.
468+
// Set image and customDeploy are set in status to prevent unconditional deprovisioning.
469+
if hsm.Host.Spec.Image != nil {
470+
hsm.Host.Status.Provisioning.Image = *hsm.Host.Spec.Image
471471
}
472+
if hsm.Host.Spec.CustomDeploy != nil {
473+
hsm.Host.Status.Provisioning.CustomDeploy = hsm.Host.Spec.CustomDeploy.DeepCopy()
474+
}
475+
476+
// Move to Provisioned. If image and customDeploy are not set, deprovisioning will start on the next reconciliation.
477+
hsm.NextState = metal3api.StateProvisioned
472478
return actionComplete{}
473479
}
474480

internal/webhooks/metal3.io/v1alpha1/baremetalhost_validation.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,6 @@ func (webhook *BareMetalHost) validateChanges(oldObj *metal3api.BareMetalHost, n
113113
errs = append(errs, errors.New("bootMACAddress can not be changed once it is set"))
114114
}
115115

116-
// Disallow disabling externallyProvisioned.
117-
if oldObj.Spec.ExternallyProvisioned && !newObj.Spec.ExternallyProvisioned {
118-
errs = append(errs, errors.New("externallyProvisioned can not be changed from true to false"))
119-
}
120-
121116
// Only allow enabling externallyProvisioned from Available state.
122117
if !oldObj.Spec.ExternallyProvisioned && newObj.Spec.ExternallyProvisioned &&
123118
newObj.Status.Provisioning.State != metal3api.StateAvailable {

internal/webhooks/metal3.io/v1alpha1/baremetalhost_validation_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,12 +1139,12 @@ func TestValidateUpdate(t *testing.T) {
11391139
wantedErr: "address invalid-mac: invalid MAC address",
11401140
},
11411141
{
1142-
name: "rejectDisablingExternallyProvisioned",
1142+
name: "allowDisablingExternallyProvisioned",
11431143
newBMH: &metal3api.BareMetalHost{
11441144
TypeMeta: tm, ObjectMeta: om, Spec: metal3api.BareMetalHostSpec{ExternallyProvisioned: false}},
11451145
oldBMH: &metal3api.BareMetalHost{
11461146
TypeMeta: tm, ObjectMeta: om, Spec: metal3api.BareMetalHostSpec{ExternallyProvisioned: true}},
1147-
wantedErr: "externallyProvisioned can not be changed from true to false",
1147+
wantedErr: "",
11481148
},
11491149
{
11501150
name: "rejectEnablingExternallyProvisionedWhenNotInAvailable",

test/e2e/externally_provisioned_test.go

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package e2e
66
import (
77
"context"
88
"path"
9-
"time"
109

1110
metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
1211
. "github.com/onsi/ginkgo/v2"
@@ -38,7 +37,7 @@ var _ = Describe("Create as externally provisioned, deprovision", Label("require
3837
})
3938
})
4039

41-
It("provisions a BMH as externally provisioned, validates state immutability, then deprovisions", func() {
40+
It("provisions a BMH as externally provisioned, then removes the flag to deprovision", func() {
4241
testSecretName := secretName + "-external"
4342
By("Creating a secret with BMH credentials")
4443
bmcCredentialsData := map[string]string{
@@ -85,7 +84,7 @@ var _ = Describe("Create as externally provisioned, deprovision", Label("require
8584
Expect(bmh.Status.OperationHistory.Inspect.Start.IsZero()).To(BeTrue())
8685
Expect(bmh.Status.OperationHistory.Provision.Start.IsZero()).To(BeTrue())
8786

88-
By("Attempting to set ExternallyProvisioned to false (should be blocked by webhook)")
87+
By("Setting ExternallyProvisioned to false to trigger deprovisioning")
8988
err = clusterProxy.GetClient().Get(ctx, types.NamespacedName{
9089
Name: bmh.Name,
9190
Namespace: bmh.Namespace,
@@ -95,22 +94,21 @@ var _ = Describe("Create as externally provisioned, deprovision", Label("require
9594
patch := client.MergeFrom(bmh.DeepCopy())
9695
bmh.Spec.ExternallyProvisioned = false
9796
err = clusterProxy.GetClient().Patch(ctx, &bmh, patch)
98-
Expect(err).To(HaveOccurred(), "Webhook should reject transition from true to false")
99-
Expect(err.Error()).To(ContainSubstring("externallyProvisioned can not be changed from true to false"))
97+
Expect(err).NotTo(HaveOccurred())
98+
99+
By("Waiting for the BMH to reach Available state after deprovisioning")
100+
WaitForBmhInProvisioningState(ctx, WaitForBmhInProvisioningStateInput{
101+
Client: clusterProxy.GetClient(),
102+
Bmh: bmh,
103+
State: metal3api.StateAvailable,
104+
}, e2eConfig.GetIntervals(specName, "wait-available")...)
100105

101-
By("Verifying BMH remains in ExternallyProvisioned state")
106+
By("Cleaning up the BMH")
102107
err = clusterProxy.GetClient().Get(ctx, types.NamespacedName{
103108
Name: bmh.Name,
104109
Namespace: bmh.Namespace,
105110
}, &bmh)
106111
Expect(err).NotTo(HaveOccurred())
107-
Expect(bmh.Spec.ExternallyProvisioned).To(BeTrue(), "ExternallyProvisioned should still be true")
108-
Expect(bmh.Status.Provisioning.State).To(Equal(metal3api.StateExternallyProvisioned))
109-
110-
By("Deleting the BMH")
111-
// Wait for 2 seconds to allow time to confirm annotation is set
112-
// TODO: fix this so we do not need the sleep
113-
time.Sleep(2 * time.Second)
114112

115113
err = clusterProxy.GetClient().Delete(ctx, &bmh)
116114
Expect(err).NotTo(HaveOccurred())
@@ -120,12 +118,6 @@ var _ = Describe("Create as externally provisioned, deprovision", Label("require
120118
Client: clusterProxy.GetClient(),
121119
BmhName: bmh.Name,
122120
Namespace: bmh.Namespace,
123-
UndesiredStates: []metal3api.ProvisioningState{
124-
metal3api.StateProvisioning,
125-
metal3api.StateRegistering,
126-
metal3api.StateDeprovisioning,
127-
metal3api.StateInspecting,
128-
},
129121
}, e2eConfig.GetIntervals(specName, "wait-bmh-deleted")...)
130122

131123
By("Waiting for the secret to be deleted")

0 commit comments

Comments
 (0)