Skip to content

Commit 1bac1f6

Browse files
Merge pull request #29741 from RishabhSaini/vsphere
MCO-1656: Component Readiness for vSphere Bootimage Update
2 parents 09fe353 + 17679e8 commit 1bac1f6

File tree

5 files changed

+289
-10
lines changed

5 files changed

+289
-10
lines changed

test/extended/machine_config/boot_image_update_agnostic.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package machine_config
22

33
import (
44
"context"
5+
"encoding/json"
6+
"fmt"
7+
"regexp"
58
"strings"
69
"time"
710

@@ -14,6 +17,8 @@ import (
1417
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1518
"k8s.io/kubernetes/test/e2e/framework"
1619
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
20+
21+
streammeta "github.com/coreos/stream-metadata-go/stream"
1722
)
1823

1924
func AllMachineSetTest(oc *exutil.CLI, fixture string) {
@@ -159,3 +164,164 @@ func EnsureConfigMapStampTest(oc *exutil.CLI, fixture string) {
159164
}, 2*time.Minute, 5*time.Second).Should(o.BeTrue())
160165
framework.Logf("Succesfully verified that the configmap has been correctly stamped")
161166
}
167+
168+
func UploadTovCentreTest(oc *exutil.CLI, fixture string) {
169+
170+
// This fixture applies a boot image update configuration that opts in any machineset with the label test=boot
171+
ApplyMachineConfigurationFixture(oc, fixture)
172+
173+
// Pick a random machineset to test
174+
machineClient, err := machineclient.NewForConfig(oc.KubeFramework().ClientConfig())
175+
o.Expect(err).NotTo(o.HaveOccurred())
176+
machineSetUnderTest := getRandomMachineSet(machineClient)
177+
framework.Logf("MachineSet under test: %s", machineSetUnderTest.Name)
178+
179+
// Label this machineset with the test=boot label
180+
err = oc.Run("label").Args(mapiMachinesetQualifiedName, machineSetUnderTest.Name, "-n", mapiNamespace, "test=boot").Execute()
181+
o.Expect(err).NotTo(o.HaveOccurred())
182+
defer func() {
183+
// Unlabel the machineset at the end of test
184+
err = oc.Run("label").Args(mapiMachinesetQualifiedName, machineSetUnderTest.Name, "-n", mapiNamespace, "test-").Execute()
185+
o.Expect(err).NotTo(o.HaveOccurred())
186+
}()
187+
188+
// Modify coreos-bootimage cm to an older known version (transient application since CVO should revert immediately)
189+
cm, err := oc.AdminKubeClient().CoreV1().ConfigMaps(mcoNamespace).Get(context.TODO(), cmName, metav1.GetOptions{})
190+
o.Expect(err).NotTo(o.HaveOccurred())
191+
var stream streammeta.Stream
192+
err = json.Unmarshal([]byte(cm.Data["stream"]), &stream)
193+
o.Expect(err).NotTo(o.HaveOccurred())
194+
vmware := stream.Architectures["x86_64"].Artifacts["vmware"]
195+
currentVmwareRelease := vmware.Release
196+
vmware.Release = "418.94.202501221327-0"
197+
vmware.Formats["ova"].Disk.Location = "https://rhcos.mirror.openshift.com/art/storage/prod/streams/4.18-9.4/builds/418.94.202501221327-0/x86_64/rhcos-418.94.202501221327-0-vmware.x86_64.ova"
198+
vmware.Formats["ova"].Disk.Sha256 = "6fd6e9fa2ff949154d54572c3f6a0c400f3e801457aa88b585b751a0955bda19"
199+
stream.Architectures["x86_64"].Artifacts["vmware"] = vmware
200+
updatedStreamBytes, err := json.MarshalIndent(stream, "", " ")
201+
o.Expect(err).NotTo(o.HaveOccurred())
202+
escapedStreamBytes, err := json.Marshal(string(updatedStreamBytes))
203+
o.Expect(err).NotTo(o.HaveOccurred())
204+
patchPayload := fmt.Sprintf(`{"data":{"stream":%s}}`, string(escapedStreamBytes))
205+
err = oc.Run("patch").Args("configmap", cmName, "-n", mcoNamespace, "-p", patchPayload).Execute()
206+
o.Expect(err).NotTo(o.HaveOccurred())
207+
208+
// Ensure boot image controller is not progressing
209+
framework.Logf("Waiting until the boot image controller is not progressing...")
210+
WaitForBootImageControllerToComplete(oc)
211+
212+
// Ensure MSBIC moves successfully from current bootimage -> known old bootimage -> current bootimage
213+
currentToOldLog := fmt.Sprintf("Existing RHCOS v%s does not match current RHCOS v%s. Starting reconciliation process.", currentVmwareRelease, vmware.Release)
214+
oldToCurrentLog := fmt.Sprintf("Existing RHCOS v%s does not match current RHCOS v%s. Starting reconciliation process.", vmware.Release, currentVmwareRelease)
215+
successfullyPatchedLog := fmt.Sprintf("Successfully patched machineset %s", machineSetUnderTest.Name)
216+
o.Eventually(func() bool {
217+
podNames, err := oc.Run("get").Args(
218+
"pods",
219+
"-n", "openshift-machine-config-operator",
220+
"-l", "k8s-app=machine-config-controller",
221+
"-o", "go-template={{range .items}}{{.metadata.name}}{{\"\\n\"}}{{end}}",
222+
).Output()
223+
if err != nil {
224+
return false
225+
}
226+
podNamesArr := strings.Split(podNames, "\n")
227+
if len(podNamesArr) == 0 {
228+
return false
229+
}
230+
mccPodName := podNamesArr[0]
231+
logs, err := oc.Run("logs").Args(mccPodName, "-n", "openshift-machine-config-operator", "--tail=50").Output()
232+
if err != nil {
233+
return false
234+
}
235+
return checkOrder(logs, currentToOldLog, successfullyPatchedLog, oldToCurrentLog, successfullyPatchedLog)
236+
}, 15*time.Minute, 10*time.Second).Should(o.BeTrue())
237+
238+
// Scale-up the machineset to verify we have not accidentally broken the bootimage updates
239+
mcdPods, err := getMCDPodSet(oc)
240+
err = oc.Run("scale").Args(mapiMachinesetQualifiedName, machineSetUnderTest.Name, "-n", mapiNamespace, fmt.Sprintf("--replicas=%d", *machineSetUnderTest.Spec.Replicas+1)).Execute()
241+
o.Expect(err).NotTo(o.HaveOccurred())
242+
243+
defer func() {
244+
// Scale-down the machineset at the end of test
245+
err = oc.Run("scale").Args(mapiMachinesetQualifiedName, machineSetUnderTest.Name, "-n", mapiNamespace, fmt.Sprintf("--replicas=%d", *machineSetUnderTest.Spec.Replicas)).Execute()
246+
o.Expect(err).NotTo(o.HaveOccurred())
247+
}()
248+
249+
o.Eventually(func() bool {
250+
machineset, err := machineClient.MachineV1beta1().MachineSets(mapiNamespace).Get(context.TODO(), machineSetUnderTest.Name, metav1.GetOptions{})
251+
if err != nil {
252+
return false
253+
}
254+
return machineset.Status.AvailableReplicas == *machineSetUnderTest.Spec.Replicas+1
255+
}, 15*time.Minute, 10*time.Second).Should(o.BeTrue())
256+
257+
o.Eventually(func() bool {
258+
updatedMcdPods, err := getMCDPodSet(oc)
259+
if err != nil {
260+
return false
261+
}
262+
newPods := diffNewPods(mcdPods, updatedMcdPods)
263+
if len(newPods) == 0 {
264+
return false
265+
}
266+
for _, pod := range newPods {
267+
logs, err := oc.Run("logs").Args(pod, "-n", "openshift-machine-config-operator").Output()
268+
if err != nil {
269+
continue
270+
}
271+
lines := strings.Split(logs, "\n")
272+
if len(lines) > 50 {
273+
lines = lines[:50]
274+
}
275+
first50 := strings.Join(lines, "\n")
276+
if strings.Contains(first50, currentVmwareRelease) {
277+
return true
278+
}
279+
}
280+
return false
281+
}, 15*time.Minute, 10*time.Second).Should(o.BeTrue())
282+
}
283+
284+
// checkOrder returns true if all statements appear in the log in the given order.
285+
func checkOrder(log string, statements ...string) bool {
286+
if len(statements) == 0 {
287+
return false
288+
}
289+
parts := make([]string, len(statements))
290+
for i, s := range statements {
291+
parts[i] = regexp.QuoteMeta(s) // escape regex metacharacters
292+
}
293+
pattern := "(?s)" + strings.Join(parts, ".*") // allow anything between statements
294+
return regexp.MustCompile(pattern).MatchString(log)
295+
}
296+
297+
func diffNewPods(before, after map[string]struct{}) []string {
298+
var newPods []string
299+
for pod := range after {
300+
if _, found := before[pod]; !found {
301+
newPods = append(newPods, pod)
302+
}
303+
}
304+
return newPods
305+
}
306+
307+
func getMCDPodSet(oc *exutil.CLI) (map[string]struct{}, error) {
308+
out, err := oc.Run("get").Args(
309+
"pods",
310+
"-n", "openshift-machine-config-operator",
311+
"-l", "k8s-app=machine-config-daemon",
312+
"-o", "go-template={{range .items}}{{.metadata.name}}{{\"\\n\"}}{{end}}",
313+
).Output()
314+
if err != nil {
315+
return nil, fmt.Errorf("failed to get pods: %v", err)
316+
}
317+
318+
podSet := make(map[string]struct{})
319+
lines := strings.Split(out, "\n")
320+
for _, line := range lines {
321+
name := strings.TrimSpace(line)
322+
if name != "" {
323+
podSet[name] = struct{}{}
324+
}
325+
}
326+
return podSet, nil
327+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package machine_config
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
7+
osconfigv1 "github.com/openshift/api/config/v1"
8+
9+
g "github.com/onsi/ginkgo/v2"
10+
exutil "github.com/openshift/origin/test/extended/util"
11+
)
12+
13+
// This test is [Serial] because it modifies the cluster/machineconfigurations.operator.openshift.io object in each test.
14+
var _ = g.Describe("[Suite:openshift/machine-config-operator/disruptive][sig-mco][OCPFeatureGate:ManagedBootImagesvSphere][Serial]", func() {
15+
defer g.GinkgoRecover()
16+
var (
17+
MCOMachineConfigurationBaseDir = exutil.FixturePath("testdata", "machine_config", "machineconfigurations")
18+
partialMachineSetFixture = filepath.Join(MCOMachineConfigurationBaseDir, "managedbootimages-partial.yaml")
19+
allMachineSetFixture = filepath.Join(MCOMachineConfigurationBaseDir, "managedbootimages-all.yaml")
20+
noneMachineSetFixture = filepath.Join(MCOMachineConfigurationBaseDir, "managedbootimages-none.yaml")
21+
emptyMachineSetFixture = filepath.Join(MCOMachineConfigurationBaseDir, "managedbootimages-empty.yaml")
22+
oc = exutil.NewCLIWithoutNamespace("machine-config")
23+
)
24+
25+
g.BeforeEach(func(ctx context.Context) {
26+
//skip this test if not on vSphere platform
27+
skipUnlessTargetPlatform(oc, osconfigv1.VSpherePlatformType)
28+
//skip this test if the cluster is not using MachineAPI
29+
skipUnlessFunctionalMachineAPI(oc)
30+
//skip this test on single node platforms
31+
skipOnSingleNodeTopology(oc)
32+
})
33+
34+
g.AfterEach(func() {
35+
ApplyMachineConfigurationFixture(oc, emptyMachineSetFixture)
36+
})
37+
38+
g.It("Should update boot images only on MachineSets that are opted in [apigroup:machineconfiguration.openshift.io]", func() {
39+
PartialMachineSetTest(oc, partialMachineSetFixture)
40+
})
41+
42+
g.It("Should update boot images on all MachineSets when configured [apigroup:machineconfiguration.openshift.io]", func() {
43+
AllMachineSetTest(oc, allMachineSetFixture)
44+
})
45+
46+
g.It("Should not update boot images on any MachineSet when not configured [apigroup:machineconfiguration.openshift.io]", func() {
47+
NoneMachineSetTest(oc, noneMachineSetFixture)
48+
})
49+
50+
g.It("Should degrade on a MachineSet with an OwnerReference [apigroup:machineconfiguration.openshift.io]", func() {
51+
DegradeOnOwnerRefTest(oc, allMachineSetFixture)
52+
})
53+
54+
g.It("Should stamp coreos-bootimages configmap with current MCO hash and release version [apigroup:machineconfiguration.openshift.io]", func() {
55+
EnsureConfigMapStampTest(oc, allMachineSetFixture)
56+
})
57+
58+
g.It("Should upload the latest bootimage to the appropriate vCentre [apigroup:machineconfiguration.openshift.io]", func() {
59+
UploadTovCentreTest(oc, partialMachineSetFixture)
60+
})
61+
})

test/extended/machine_config/helpers.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,20 @@ func verifyMachineSetUpdate(oc *exutil.CLI, machineSet machinev1beta1.MachineSet
193193
framework.Logf("Waiting until the boot image controller is not progressing...")
194194
WaitForBootImageControllerToComplete(oc)
195195

196-
// Fetch the providerSpec of the machineset under test again
197-
providerSpecDisks, err := oc.Run("get").Args(mapiMachinesetQualifiedName, machineSet.Name, "-o", "template", "--template=`{{.spec.template.spec.providerSpec.value}}`", "-n", mapiNamespace).Output()
198-
o.Expect(err).NotTo(o.HaveOccurred())
196+
o.Eventually(func() bool {
197+
// Fetch the providerSpec of the machineset under test again
198+
providerSpec, err := oc.Run("get").Args(mapiMachinesetQualifiedName, machineSet.Name, "-o", "template", "--template=`{{.spec.template.spec.providerSpec.value}}`", "-n", mapiNamespace).Output()
199+
if err != nil {
200+
return false
201+
}
199202

200-
// Verify that the machineset has the expected boot image values
201-
if updateExpected {
202-
o.Expect(providerSpecDisks).ShouldNot(o.ContainSubstring(newBootImage))
203-
} else {
204-
o.Expect(providerSpecDisks).Should(o.ContainSubstring(newBootImage))
205-
}
203+
// Verify that the machineset has the expected boot image values
204+
if updateExpected {
205+
return !strings.Contains(providerSpec, newBootImage)
206+
} else {
207+
return strings.Contains(providerSpec, newBootImage)
208+
}
209+
}, 10*time.Minute, 10*time.Second).Should(o.BeTrue(), "Timed out verifying MachineSet '%v'", machineSet.Name)
206210
}
207211

208212
// unmarshalProviderSpec unmarshals the machineset's provider spec into
@@ -241,6 +245,8 @@ func createFakeUpdatePatch(oc *exutil.CLI, machineSet machinev1beta1.MachineSet)
241245
return generateAWSProviderSpecPatch(machineSet)
242246
case osconfigv1.GCPPlatformType:
243247
return generateGCPProviderSpecPatch(machineSet)
248+
case osconfigv1.VSpherePlatformType:
249+
return generateVSphereProviderSpecPatch(machineSet)
244250
default:
245251
exutil.FatalErr(fmt.Errorf("unexpected platform type; should not be here"))
246252
return "", "", "", ""
@@ -293,6 +299,25 @@ func generateGCPProviderSpecPatch(machineSet machinev1beta1.MachineSet) (string,
293299
return newProviderSpecPatch, originalProviderSpecPatch, newBootImage, originalBootImage
294300
}
295301

302+
// generateVSphereProviderSpecPatch generates a fake update patch for the VSphere MachineSet
303+
func generateVSphereProviderSpecPatch(machineSet machinev1beta1.MachineSet) (string, string, string, string) {
304+
providerSpec := new(machinev1beta1.VSphereMachineProviderSpec)
305+
err := unmarshalProviderSpec(&machineSet, providerSpec)
306+
o.Expect(err).NotTo(o.HaveOccurred())
307+
308+
// Modify the boot image to a "fake" value
309+
originalBootImage := providerSpec.Template
310+
newBootImage := "fake-update"
311+
newProviderSpec := providerSpec.DeepCopy()
312+
newProviderSpec.Template = newBootImage
313+
newProviderSpecPatch, err := marshalProviderSpec(newProviderSpec)
314+
o.Expect(err).NotTo(o.HaveOccurred())
315+
originalProviderSpecPatch, err := marshalProviderSpec(providerSpec)
316+
o.Expect(err).NotTo(o.HaveOccurred())
317+
318+
return newProviderSpecPatch, originalProviderSpecPatch, newBootImage, originalBootImage
319+
}
320+
296321
// WaitForBootImageControllerToComplete waits until the boot image controller is no longer progressing
297322
func WaitForBootImageControllerToComplete(oc *exutil.CLI) {
298323
machineConfigurationClient, err := mcopclient.NewForConfig(oc.KubeFramework().ClientConfig())
@@ -306,7 +331,7 @@ func WaitForBootImageControllerToComplete(oc *exutil.CLI) {
306331
return false
307332
}
308333
return IsMachineConfigurationConditionFalse(mcop.Status.Conditions, opv1.MachineConfigurationBootImageUpdateProgressing)
309-
}, 3*time.Minute, 5*time.Second).MustPassRepeatedly(3).Should(o.BeTrue())
334+
}, 5*time.Minute, 5*time.Second).MustPassRepeatedly(3).Should(o.BeTrue())
310335
}
311336

312337
// WaitForMachineConfigurationStatus waits until the MCO syncs the operator status to the latest spec

test/extended/util/annotate/generated/zz_generated.annotations.go

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

zz_generated.manifests/test-reporting.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,21 @@ spec:
698698
Should update boot images on all MachineSets when configured [apigroup:machineconfiguration.openshift.io]'
699699
- testName: '[Suite:openshift/machine-config-operator/disruptive][Suite:openshift/conformance/serial][sig-mco][OCPFeatureGate:ManagedBootImagesAWS][Serial]
700700
Should update boot images only on MachineSets that are opted in [apigroup:machineconfiguration.openshift.io]'
701+
- featureGate: ManagedBootImagesvSphere
702+
tests:
703+
- testName: '[Suite:openshift/machine-config-operator/disruptive][sig-mco][OCPFeatureGate:ManagedBootImagesvSphere][Serial]
704+
Should degrade on a MachineSet with an OwnerReference [apigroup:machineconfiguration.openshift.io]'
705+
- testName: '[Suite:openshift/machine-config-operator/disruptive][sig-mco][OCPFeatureGate:ManagedBootImagesvSphere][Serial]
706+
Should not update boot images on any MachineSet when not configured [apigroup:machineconfiguration.openshift.io]'
707+
- testName: '[Suite:openshift/machine-config-operator/disruptive][sig-mco][OCPFeatureGate:ManagedBootImagesvSphere][Serial]
708+
Should stamp coreos-bootimages configmap with current MCO hash and release
709+
version [apigroup:machineconfiguration.openshift.io]'
710+
- testName: '[Suite:openshift/machine-config-operator/disruptive][sig-mco][OCPFeatureGate:ManagedBootImagesvSphere][Serial]
711+
Should update boot images on all MachineSets when configured [apigroup:machineconfiguration.openshift.io]'
712+
- testName: '[Suite:openshift/machine-config-operator/disruptive][sig-mco][OCPFeatureGate:ManagedBootImagesvSphere][Serial]
713+
Should update boot images only on MachineSets that are opted in [apigroup:machineconfiguration.openshift.io]'
714+
- testName: '[Suite:openshift/machine-config-operator/disruptive][sig-mco][OCPFeatureGate:ManagedBootImagesvSphere][Serial]
715+
Should upload the latest bootimage to the appropriate vCentre [apigroup:machineconfiguration.openshift.io]'
701716
- featureGate: MetricsCollectionProfiles
702717
tests:
703718
- testName: '[sig-instrumentation][OCPFeatureGate:MetricsCollectionProfiles][Serial]

0 commit comments

Comments
 (0)