Skip to content

Commit e9fc683

Browse files
CMP-4178: Add machineset scale up after remediations applied
1 parent def3a79 commit e9fc683

2 files changed

Lines changed: 347 additions & 0 deletions

File tree

e2e_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ func TestPlatformCompliance(t *testing.T) {
146146
}
147147
afterRemediation = true
148148

149+
err = helpers.ValidateMachineSetScaleWithDeletePolicyNewest(tc, c, platformBindingName)
150+
if err != nil {
151+
t.Fatalf("MachineSet scale/deletePolicy validation failed: %s", err)
152+
}
153+
149154
finalResults, err := helpers.CreateResultMap(tc, c, platformBindingName)
150155
if err != nil {
151156
t.Fatalf("Failed to create result map: %s", err)
@@ -259,6 +264,11 @@ func TestNodeCompliance(t *testing.T) {
259264
}
260265
afterRemediation = true
261266

267+
err = helpers.ValidateMachineSetScaleWithDeletePolicyNewest(tc, c, nodeBindingName)
268+
if err != nil {
269+
t.Fatalf("MachineSet scale/deletePolicy validation failed: %s", err)
270+
}
271+
262272
finalResults, err := helpers.CreateResultMap(tc, c, nodeBindingName)
263273
if err != nil {
264274
t.Fatalf("Failed to create result map: %s", err)

helpers/utilities.go

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ import (
2323
mcfgv1 "github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1"
2424
"gopkg.in/yaml.v2"
2525
appsv1 "k8s.io/api/apps/v1"
26+
corev1 "k8s.io/api/core/v1"
2627
extscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
2728
apierrors "k8s.io/apimachinery/pkg/api/errors"
2829
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2930
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3031
"k8s.io/apimachinery/pkg/labels"
3132
"k8s.io/apimachinery/pkg/runtime"
33+
"k8s.io/apimachinery/pkg/runtime/schema"
3234
"k8s.io/apimachinery/pkg/types"
3335
"k8s.io/apimachinery/pkg/util/intstr"
3436
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
@@ -1762,6 +1764,341 @@ func CreateResultMap(_ *testConfig.TestConfig, c dynclient.Client, suiteName str
17621764
return resultMap, nil
17631765
}
17641766

1767+
// ValidateMachineSetScaleWithDeletePolicyNewest validates that a worker MachineSet
1768+
// can scale up and back down after remediations and that deletePolicy=Newest
1769+
// removes the newest machine first on scale-down.
1770+
func ValidateMachineSetScaleWithDeletePolicyNewest(tc *testConfig.TestConfig, c dynclient.Client, suiteName string) error {
1771+
const keepLabelKey = "ocp4e2e.openshift.io/scan-suite"
1772+
const keepLabelValue = "kept-after-scan"
1773+
1774+
msName, msKey, err := getMachineSetForOneWorkerNode(c)
1775+
if err != nil {
1776+
return err
1777+
}
1778+
1779+
msObj, originalReadyReplicas, originalDeletePolicy, hadDeletePolicy, err := getMachineSetState(c, msKey)
1780+
if err != nil {
1781+
return fmt.Errorf("failed to get initial machineset state for %s: %w", msName, err)
1782+
}
1783+
1784+
initialMachineNames, err := labelExistingMachinesForMachineSet(c, msName, keepLabelKey, keepLabelValue, suiteName)
1785+
if err != nil {
1786+
return err
1787+
}
1788+
1789+
defer cleanupMachineSetScaleDeletePolicyAndLabels(
1790+
tc,
1791+
c,
1792+
msKey,
1793+
msName,
1794+
originalReadyReplicas,
1795+
originalDeletePolicy,
1796+
hadDeletePolicy,
1797+
keepLabelKey,
1798+
initialMachineNames,
1799+
)
1800+
1801+
if err := setMachineSetDeletePolicyNewest(c, msKey, msObj, msName); err != nil {
1802+
return err
1803+
}
1804+
if err := verifyMachineSetDeletePolicyNewest(c, msKey, msName); err != nil {
1805+
return err
1806+
}
1807+
1808+
targetReplicas := originalReadyReplicas + 1
1809+
if err := scaleMachineSetAndWait(tc, c, msKey, targetReplicas, msName, "up"); err != nil {
1810+
return err
1811+
}
1812+
if err := scaleMachineSetAndWait(tc, c, msKey, originalReadyReplicas, msName, "down"); err != nil {
1813+
return err
1814+
}
1815+
1816+
if err := verifyOriginalMachinesPreserved(c, msName, initialMachineNames); err != nil {
1817+
return err
1818+
}
1819+
1820+
log.Printf("Validated machineset scale behavior for %s with deletePolicy=Newest (suite=%s)", msName, suiteName)
1821+
return nil
1822+
}
1823+
1824+
func getMachineSetForOneWorkerNode(c dynclient.Client) (string, types.NamespacedName, error) {
1825+
const machineAPINamespace = "openshift-machine-api"
1826+
1827+
nodeSelector, err := labels.Parse("node-role.kubernetes.io/edge!=,kubernetes.io/os!=windows,node-role.kubernetes.io/worker=")
1828+
if err != nil {
1829+
return "", types.NamespacedName{}, fmt.Errorf("failed to parse worker node selector: %w", err)
1830+
}
1831+
1832+
nodes := &corev1.NodeList{}
1833+
if err := c.List(goctx.TODO(), nodes, &dynclient.ListOptions{LabelSelector: nodeSelector}); err != nil {
1834+
return "", types.NamespacedName{}, fmt.Errorf("failed to list worker nodes: %w", err)
1835+
}
1836+
if len(nodes.Items) == 0 {
1837+
return "", types.NamespacedName{}, fmt.Errorf("no worker node found for machineset scale validation")
1838+
}
1839+
workerNode := &nodes.Items[0]
1840+
1841+
machineRef, ok := workerNode.Annotations["machine.openshift.io/machine"]
1842+
if !ok || machineRef == "" {
1843+
return "", types.NamespacedName{}, fmt.Errorf("worker node %s does not have machine annotation", workerNode.Name)
1844+
}
1845+
parts := strings.SplitN(machineRef, "/", 2)
1846+
if len(parts) != 2 || parts[1] == "" {
1847+
return "", types.NamespacedName{}, fmt.Errorf("worker node %s has invalid machine annotation: %s", workerNode.Name, machineRef)
1848+
}
1849+
machineName := parts[1]
1850+
1851+
machineGVK := schema.GroupVersionKind{Group: "machine.openshift.io", Version: "v1beta1", Kind: "Machine"}
1852+
machineObj := &unstructured.Unstructured{}
1853+
machineObj.SetGroupVersionKind(machineGVK)
1854+
if err := c.Get(goctx.TODO(), types.NamespacedName{Name: machineName, Namespace: machineAPINamespace}, machineObj); err != nil {
1855+
return "", types.NamespacedName{}, fmt.Errorf("failed to get machine %s: %w", machineName, err)
1856+
}
1857+
1858+
msName, ok := machineObj.GetLabels()["machine.openshift.io/cluster-api-machineset"]
1859+
if !ok || msName == "" {
1860+
return "", types.NamespacedName{}, fmt.Errorf("machine %s does not have machineset label", machineName)
1861+
}
1862+
return msName, types.NamespacedName{Name: msName, Namespace: machineAPINamespace}, nil
1863+
}
1864+
1865+
func getMachineSetState(
1866+
c dynclient.Client,
1867+
msKey types.NamespacedName,
1868+
) (*unstructured.Unstructured, int64, string, bool, error) {
1869+
msGVK := schema.GroupVersionKind{Group: "machine.openshift.io", Version: "v1beta1", Kind: "MachineSet"}
1870+
msObj := &unstructured.Unstructured{}
1871+
msObj.SetGroupVersionKind(msGVK)
1872+
if err := c.Get(goctx.TODO(), msKey, msObj); err != nil {
1873+
return nil, 0, "", false, err
1874+
}
1875+
1876+
originalDeletePolicy, hadDeletePolicy, _ := unstructured.NestedString(msObj.Object, "spec", "deletePolicy")
1877+
originalReadyReplicas, foundReady, _ := unstructured.NestedInt64(msObj.Object, "status", "readyReplicas")
1878+
if !foundReady {
1879+
originalReadyReplicas = 0
1880+
}
1881+
return msObj, originalReadyReplicas, originalDeletePolicy, hadDeletePolicy, nil
1882+
}
1883+
1884+
func labelExistingMachinesForMachineSet(
1885+
c dynclient.Client,
1886+
msName, keepLabelKey, keepLabelValue, suiteName string,
1887+
) (map[string]struct{}, error) {
1888+
const machineAPINamespace = "openshift-machine-api"
1889+
1890+
machineSelector, err := labels.Parse("machine.openshift.io/cluster-api-machineset=" + msName)
1891+
if err != nil {
1892+
return nil, fmt.Errorf("failed to parse machineset machine selector: %w", err)
1893+
}
1894+
initialMachineList := &unstructured.UnstructuredList{}
1895+
initialMachineList.SetGroupVersionKind(schema.GroupVersionKind{
1896+
Group: "machine.openshift.io",
1897+
Version: "v1beta1",
1898+
Kind: "MachineList",
1899+
})
1900+
if err := c.List(goctx.TODO(), initialMachineList, &dynclient.ListOptions{
1901+
Namespace: machineAPINamespace,
1902+
LabelSelector: machineSelector,
1903+
}); err != nil {
1904+
return nil, fmt.Errorf("failed to list machines for machineset %s: %w", msName, err)
1905+
}
1906+
if len(initialMachineList.Items) == 0 {
1907+
return nil, fmt.Errorf("no machines found for machineset %s", msName)
1908+
}
1909+
1910+
initialMachineNames := make(map[string]struct{}, len(initialMachineList.Items))
1911+
for i := range initialMachineList.Items {
1912+
m := &initialMachineList.Items[i]
1913+
initialMachineNames[m.GetName()] = struct{}{}
1914+
lbls := m.GetLabels()
1915+
if lbls == nil {
1916+
lbls = map[string]string{}
1917+
}
1918+
lbls[keepLabelKey] = keepLabelValue
1919+
lbls[keepLabelKey+"-suite"] = suiteName
1920+
m.SetLabels(lbls)
1921+
if err := c.Update(goctx.TODO(), m); err != nil {
1922+
return nil, fmt.Errorf("failed to label machine %s: %w", m.GetName(), err)
1923+
}
1924+
}
1925+
return initialMachineNames, nil
1926+
}
1927+
1928+
func cleanupMachineSetScaleDeletePolicyAndLabels(
1929+
tc *testConfig.TestConfig,
1930+
c dynclient.Client,
1931+
msKey types.NamespacedName,
1932+
msName string,
1933+
originalReadyReplicas int64,
1934+
originalDeletePolicy string,
1935+
hadDeletePolicy bool,
1936+
keepLabelKey string,
1937+
initialMachineNames map[string]struct{},
1938+
) {
1939+
const machineAPINamespace = "openshift-machine-api"
1940+
msGVK := schema.GroupVersionKind{Group: "machine.openshift.io", Version: "v1beta1", Kind: "MachineSet"}
1941+
restoreObj := &unstructured.Unstructured{}
1942+
restoreObj.SetGroupVersionKind(msGVK)
1943+
if err := c.Get(goctx.TODO(), msKey, restoreObj); err != nil {
1944+
log.Printf("cleanup warning: failed to get machineset %s: %v", msName, err)
1945+
} else if err := unstructured.SetNestedField(restoreObj.Object, originalReadyReplicas, "spec", "replicas"); err != nil {
1946+
log.Printf("cleanup warning: failed to set replicas on machineset %s: %v", msName, err)
1947+
} else if err := c.Update(goctx.TODO(), restoreObj); err != nil {
1948+
log.Printf("cleanup warning: failed to restore replicas for machineset %s: %v", msName, err)
1949+
} else if err := waitForMachineSetReadyReplicas(tc, c, msKey, originalReadyReplicas); err != nil {
1950+
log.Printf("cleanup warning: failed waiting for replicas restore on machineset %s: %v", msName, err)
1951+
}
1952+
1953+
restoreObj = &unstructured.Unstructured{}
1954+
restoreObj.SetGroupVersionKind(msGVK)
1955+
if err := c.Get(goctx.TODO(), msKey, restoreObj); err != nil {
1956+
log.Printf("cleanup warning: failed to re-get machineset %s: %v", msName, err)
1957+
} else {
1958+
if hadDeletePolicy {
1959+
if err := unstructured.SetNestedField(restoreObj.Object, originalDeletePolicy, "spec", "deletePolicy"); err != nil {
1960+
log.Printf("cleanup warning: failed to restore deletePolicy for machineset %s: %v", msName, err)
1961+
}
1962+
} else {
1963+
unstructured.RemoveNestedField(restoreObj.Object, "spec", "deletePolicy")
1964+
}
1965+
if err := c.Update(goctx.TODO(), restoreObj); err != nil {
1966+
log.Printf("cleanup warning: failed to restore deletePolicy for machineset %s: %v", msName, err)
1967+
}
1968+
}
1969+
1970+
for machineName := range initialMachineNames {
1971+
machine := &unstructured.Unstructured{}
1972+
machine.SetGroupVersionKind(schema.GroupVersionKind{Group: "machine.openshift.io", Version: "v1beta1", Kind: "Machine"})
1973+
key := types.NamespacedName{Name: machineName, Namespace: machineAPINamespace}
1974+
if err := c.Get(goctx.TODO(), key, machine); err != nil {
1975+
continue
1976+
}
1977+
lbls := machine.GetLabels()
1978+
delete(lbls, keepLabelKey)
1979+
delete(lbls, keepLabelKey+"-suite")
1980+
machine.SetLabels(lbls)
1981+
if err := c.Update(goctx.TODO(), machine); err != nil {
1982+
log.Printf("cleanup warning: failed to remove keep labels from machine %s: %v", machineName, err)
1983+
}
1984+
}
1985+
}
1986+
1987+
func setMachineSetDeletePolicyNewest(
1988+
c dynclient.Client,
1989+
msKey types.NamespacedName,
1990+
msObj *unstructured.Unstructured,
1991+
msName string,
1992+
) error {
1993+
if err := unstructured.SetNestedField(msObj.Object, "Newest", "spec", "deletePolicy"); err != nil {
1994+
return fmt.Errorf("failed to set deletePolicy=Newest on machineset %s: %w", msName, err)
1995+
}
1996+
if err := c.Update(goctx.TODO(), msObj); err != nil {
1997+
return fmt.Errorf("failed to update machineset %s with deletePolicy=Newest: %w", msName, err)
1998+
}
1999+
return nil
2000+
}
2001+
2002+
func verifyMachineSetDeletePolicyNewest(c dynclient.Client, msKey types.NamespacedName, msName string) error {
2003+
msGVK := schema.GroupVersionKind{Group: "machine.openshift.io", Version: "v1beta1", Kind: "MachineSet"}
2004+
verifyObj := &unstructured.Unstructured{}
2005+
verifyObj.SetGroupVersionKind(msGVK)
2006+
if err := c.Get(goctx.TODO(), msKey, verifyObj); err != nil {
2007+
return fmt.Errorf("failed to verify machineset %s: %w", msName, err)
2008+
}
2009+
deletePolicy, _, _ := unstructured.NestedString(verifyObj.Object, "spec", "deletePolicy")
2010+
if deletePolicy != "Newest" {
2011+
return fmt.Errorf("expected machineset %s deletePolicy to be Newest, got %q", msName, deletePolicy)
2012+
}
2013+
return nil
2014+
}
2015+
2016+
func scaleMachineSetAndWait(
2017+
tc *testConfig.TestConfig,
2018+
c dynclient.Client,
2019+
msKey types.NamespacedName,
2020+
replicas int64,
2021+
msName, direction string,
2022+
) error {
2023+
msGVK := schema.GroupVersionKind{Group: "machine.openshift.io", Version: "v1beta1", Kind: "MachineSet"}
2024+
msObj := &unstructured.Unstructured{}
2025+
msObj.SetGroupVersionKind(msGVK)
2026+
if err := c.Get(goctx.TODO(), msKey, msObj); err != nil {
2027+
return fmt.Errorf("failed to get machineset %s before scale %s: %w", msName, direction, err)
2028+
}
2029+
if err := unstructured.SetNestedField(msObj.Object, replicas, "spec", "replicas"); err != nil {
2030+
return fmt.Errorf("failed to set replicas=%d for machineset %s scale %s: %w", replicas, msName, direction, err)
2031+
}
2032+
if err := c.Update(goctx.TODO(), msObj); err != nil {
2033+
return fmt.Errorf("failed to scale %s machineset %s: %w", direction, msName, err)
2034+
}
2035+
if err := waitForMachineSetReadyReplicas(tc, c, msKey, replicas); err != nil {
2036+
return fmt.Errorf("failed waiting for machineset %s scale %s: %w", msName, direction, err)
2037+
}
2038+
return nil
2039+
}
2040+
2041+
func verifyOriginalMachinesPreserved(
2042+
c dynclient.Client,
2043+
msName string,
2044+
initialMachineNames map[string]struct{},
2045+
) error {
2046+
const machineAPINamespace = "openshift-machine-api"
2047+
machineSelector, err := labels.Parse("machine.openshift.io/cluster-api-machineset=" + msName)
2048+
if err != nil {
2049+
return fmt.Errorf("failed to parse machineset machine selector: %w", err)
2050+
}
2051+
finalMachineList := &unstructured.UnstructuredList{}
2052+
finalMachineList.SetGroupVersionKind(schema.GroupVersionKind{
2053+
Group: "machine.openshift.io",
2054+
Version: "v1beta1",
2055+
Kind: "MachineList",
2056+
})
2057+
if err = c.List(goctx.TODO(), finalMachineList, &dynclient.ListOptions{
2058+
Namespace: machineAPINamespace,
2059+
LabelSelector: machineSelector,
2060+
}); err != nil {
2061+
return fmt.Errorf("failed to list final machines for machineset %s: %w", msName, err)
2062+
}
2063+
finalMachineNames := make(map[string]struct{}, len(finalMachineList.Items))
2064+
for i := range finalMachineList.Items {
2065+
finalMachineNames[finalMachineList.Items[i].GetName()] = struct{}{}
2066+
}
2067+
for name := range initialMachineNames {
2068+
if _, exists := finalMachineNames[name]; !exists {
2069+
return fmt.Errorf("machineset %s scaled down but older machine %s was deleted; expected newest machine to be deleted", msName, name)
2070+
}
2071+
}
2072+
return nil
2073+
}
2074+
2075+
func waitForMachineSetReadyReplicas(
2076+
tc *testConfig.TestConfig,
2077+
c dynclient.Client,
2078+
msKey types.NamespacedName,
2079+
expected int64,
2080+
) error {
2081+
bo := backoff.WithMaxRetries(backoff.NewConstantBackOff(tc.APIPollInterval), 200)
2082+
msGVK := schema.GroupVersionKind{Group: "machine.openshift.io", Version: "v1beta1", Kind: "MachineSet"}
2083+
return backoff.RetryNotify(func() error {
2084+
ms := &unstructured.Unstructured{}
2085+
ms.SetGroupVersionKind(msGVK)
2086+
if err := c.Get(goctx.TODO(), msKey, ms); err != nil {
2087+
return err
2088+
}
2089+
ready, found, _ := unstructured.NestedInt64(ms.Object, "status", "readyReplicas")
2090+
if !found {
2091+
ready = 0
2092+
}
2093+
if ready != expected {
2094+
return fmt.Errorf("readyReplicas=%d expected=%d", ready, expected)
2095+
}
2096+
return nil
2097+
}, bo, func(err error, d time.Duration) {
2098+
log.Printf("waiting for machineset %s readyReplicas=%d after %s: %v", msKey.Name, expected, d.String(), err)
2099+
})
2100+
}
2101+
17652102
// SaveResultAsYAML saves YAML data about the scan results to a file in the configured log directory.
17662103
func SaveResultAsYAML(tc *testConfig.TestConfig, results map[string]string, filename string) error {
17672104
p := path.Join(tc.LogDir, filename)

0 commit comments

Comments
 (0)