@@ -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.
17662103func SaveResultAsYAML (tc * testConfig.TestConfig , results map [string ]string , filename string ) error {
17672104 p := path .Join (tc .LogDir , filename )
0 commit comments