@@ -24,6 +24,7 @@ import (
2424 bmov1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
2525 infrav1 "github.com/metal3-io/cluster-api-provider-metal3/api/v1beta1"
2626 ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1"
27+ irsov1alpha1 "github.com/metal3-io/ironic-standalone-operator/api/v1alpha1"
2728 . "github.com/onsi/ginkgo/v2"
2829 . "github.com/onsi/gomega"
2930 "github.com/pkg/errors"
@@ -1227,3 +1228,226 @@ func IsMetal3DataCountEqualToMachineCount(ctx context.Context, c client.Client,
12271228
12281229 return len (m3DataList .Items ) == len (machineList .Items )
12291230}
1231+
1232+ type InstallIRSOInput struct {
1233+ E2EConfig * clusterctl.E2EConfig
1234+ ClusterProxy framework.ClusterProxy
1235+ IronicNamespace string
1236+ ClusterName string
1237+ IrsoOperatorKustomize string
1238+ IronicKustomize string
1239+ }
1240+
1241+ func InstallIRSO (ctx context.Context , input InstallIRSOInput ) error {
1242+ By ("Create Ironic namespace" )
1243+ targetClusterClientSet := input .ClusterProxy .GetClientSet ()
1244+ ironicNamespaceObj := & corev1.Namespace {
1245+ ObjectMeta : metav1.ObjectMeta {
1246+ Name : input .IronicNamespace ,
1247+ },
1248+ }
1249+ _ , err := targetClusterClientSet .CoreV1 ().Namespaces ().Create (ctx , ironicNamespaceObj , metav1.CreateOptions {})
1250+ if err != nil {
1251+ if apierrors .IsAlreadyExists (err ) {
1252+ Logf ("Ironic namespace %q already exists, continuing" , input .IronicNamespace )
1253+ } else {
1254+ Expect (err ).ToNot (HaveOccurred (), "Unable to create the Ironic namespace" )
1255+ }
1256+ }
1257+
1258+ irsoDeployLogFolder := filepath .Join (os .TempDir (), "target_cluster_logs" , "ironic-deploy-logs" , input .ClusterProxy .GetName ())
1259+ By (fmt .Sprintf ("Installing IRSO from kustomization %s on the target cluster" , input .IrsoOperatorKustomize ))
1260+ err = BuildAndApplyKustomization (ctx , & BuildAndApplyKustomizationInput {
1261+ Kustomization : input .IrsoOperatorKustomize ,
1262+ ClusterProxy : input .ClusterProxy ,
1263+ WaitForDeployment : true ,
1264+ WatchDeploymentLogs : true ,
1265+ LogPath : irsoDeployLogFolder ,
1266+ DeploymentName : "ironic-standalone-operator-controller-manager" ,
1267+ DeploymentNamespace : IRSOControllerNameSpace ,
1268+ WaitIntervals : input .E2EConfig .GetIntervals ("default" , "wait-deployment" ),
1269+ })
1270+ Expect (err ).NotTo (HaveOccurred ())
1271+
1272+ By ("Install Ironic CR in the target cluster" )
1273+ By (fmt .Sprintf ("Installing Ironic from kustomization %s on the target cluster" , input .IronicKustomize ))
1274+ err = BuildAndApplyKustomization (ctx , & BuildAndApplyKustomizationInput {
1275+ Kustomization : input .IronicKustomize ,
1276+ ClusterProxy : input .ClusterProxy ,
1277+ WaitForDeployment : false ,
1278+ WatchDeploymentLogs : false ,
1279+ })
1280+ Expect (err ).NotTo (HaveOccurred ())
1281+
1282+ WaitForIronicReady (ctx , WaitForIronicInput {
1283+ Client : input .ClusterProxy .GetClient (),
1284+ Name : "ironic" ,
1285+ Namespace : input .IronicNamespace ,
1286+ Intervals : input .E2EConfig .GetIntervals ("default" , "wait-deployment" ),
1287+ })
1288+ return nil
1289+ }
1290+
1291+ // WaitForIronicReady waits until the given Ironic resource has Ready condition = True.
1292+ func WaitForIronicReady (ctx context.Context , input WaitForIronicInput ) {
1293+ Logf ("Waiting for Ironic %q to be Ready" , input .Name )
1294+
1295+ Eventually (func (g Gomega ) {
1296+ ironic := & irsov1alpha1.Ironic {}
1297+ err := input .Client .Get (ctx , client.ObjectKey {
1298+ Namespace : input .Namespace ,
1299+ Name : input .Name ,
1300+ }, ironic )
1301+ g .Expect (err ).ToNot (HaveOccurred ())
1302+
1303+ ready := false
1304+ for _ , cond := range ironic .Status .Conditions {
1305+ if cond .Type == string (irsov1alpha1 .IronicStatusReady ) && cond .Status == metav1 .ConditionTrue && ironic .Status .InstalledVersion != "" {
1306+ ready = true
1307+ break
1308+ }
1309+ }
1310+ g .Expect (ready ).To (BeTrue (), "Ironic %q is not Ready yet" , input .Name )
1311+ }, input .Intervals ... ).Should (Succeed ())
1312+
1313+ Logf ("Ironic %q is Ready" , input .Name )
1314+ }
1315+
1316+ // WaitForIronicInput bundles the parameters for WaitForIronicReady.
1317+ type WaitForIronicInput struct {
1318+ Client client.Client
1319+ Name string
1320+ Namespace string
1321+ Intervals []interface {} // e.g. []interface{}{time.Minute * 15, time.Second * 5}
1322+ }
1323+
1324+ // InstallBMOInput bundles parameters for InstallBMO.
1325+ type InstallBMOInput struct {
1326+ E2EConfig * clusterctl.E2EConfig
1327+ ClusterProxy framework.ClusterProxy
1328+ Namespace string // Namespace where BMO will run (shared with Ironic)
1329+ BmoKustomization string // Kustomization path or URL for BMO manifests
1330+ ClusterName string // For legacy log folder naming (deprecated, prefer LogFolder)
1331+ LogFolder string // Optional explicit log folder; if empty a default is derived
1332+ WaitIntervals []any // Optional override; if nil uses default e2e config intervals
1333+ WatchLogs bool // Whether to watch deployment logs
1334+ }
1335+
1336+ // InstallBMO installs the Baremetal Operator (BMO) in the target cluster similar to InstallIRSO.
1337+ func InstallBMO (ctx context.Context , input InstallBMOInput ) error {
1338+ By ("Ensure BMO namespace exists" )
1339+ clientset := input .ClusterProxy .GetClientSet ()
1340+ ns := & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : input .Namespace }}
1341+ _ , err := clientset .CoreV1 ().Namespaces ().Create (ctx , ns , metav1.CreateOptions {})
1342+ if err != nil {
1343+ if apierrors .IsAlreadyExists (err ) {
1344+ Logf ("Namespace %q already exists, continuing" , input .Namespace )
1345+ } else {
1346+ return fmt .Errorf ("failed creating namespace %q: %w" , input .Namespace , err )
1347+ }
1348+ }
1349+
1350+ // Determine log folder
1351+ logFolder := input .LogFolder
1352+ if logFolder == "" {
1353+ logFolder = filepath .Join (os .TempDir (), "target_cluster_logs" , "bmo-deploy-logs" , input .ClusterProxy .GetName ())
1354+ }
1355+ intervals := input .WaitIntervals
1356+ if intervals == nil {
1357+ intervals = input .E2EConfig .GetIntervals ("default" , "wait-deployment" )
1358+ }
1359+
1360+ By (fmt .Sprintf ("Installing BMO from kustomization %s on the target cluster" , input .BmoKustomization ))
1361+ err = BuildAndApplyKustomization (ctx , & BuildAndApplyKustomizationInput {
1362+ Kustomization : input .BmoKustomization ,
1363+ ClusterProxy : input .ClusterProxy ,
1364+ WaitForDeployment : true ,
1365+ WatchDeploymentLogs : input .WatchLogs ,
1366+ LogPath : logFolder ,
1367+ DeploymentName : "baremetal-operator-controller-manager" ,
1368+ DeploymentNamespace : input .Namespace ,
1369+ WaitIntervals : intervals ,
1370+ })
1371+ if err != nil {
1372+ return fmt .Errorf ("failed installing BMO: %w" , err )
1373+ }
1374+
1375+ By ("BMO deployment applied and available" )
1376+ return nil
1377+ }
1378+
1379+ type UninstallIRSOAndIronicResourcesInput struct {
1380+ E2EConfig * clusterctl.E2EConfig
1381+ ClusterProxy framework.ClusterProxy
1382+ IronicNamespace string
1383+ IrsoOperatorKustomize string
1384+ IronicKustomization string
1385+ IsDevEnvUninstall bool
1386+ }
1387+
1388+ // UninstallIRSOAndIronicResources removes the IRSO deployment, Ironic CR, IronicDatabase CR (if present), and related secrets.
1389+ func UninstallIRSOAndIronicResources (ctx context.Context , input UninstallIRSOAndIronicResourcesInput ) error {
1390+ if input .IsDevEnvUninstall {
1391+ ironicObj := & irsov1alpha1.Ironic {
1392+ ObjectMeta : metav1.ObjectMeta {
1393+ Name : "ironic" ,
1394+ Namespace : input .IronicNamespace ,
1395+ },
1396+ }
1397+ err := input .ClusterProxy .GetClient ().Delete (ctx , ironicObj )
1398+ Expect (err ).ToNot (HaveOccurred (), "Failed to delete Ironic" )
1399+ } else {
1400+ By ("Remove Ironic CR in the cluster " + input .ClusterProxy .GetName ())
1401+ err := BuildAndRemoveKustomization (ctx , input .IronicKustomization , input .ClusterProxy )
1402+ Expect (err ).NotTo (HaveOccurred ())
1403+ }
1404+
1405+ By ("Remove Ironic Service Deployment in the cluster " + input .ClusterProxy .GetName ())
1406+ RemoveDeployment (ctx , func () RemoveDeploymentInput {
1407+ return RemoveDeploymentInput {
1408+ ManagementCluster : input .ClusterProxy ,
1409+ Namespace : input .IronicNamespace ,
1410+ Name : "ironic-service" ,
1411+ }
1412+ })
1413+
1414+ if input .IsDevEnvUninstall {
1415+ By ("Remove Ironic Standalone Operator Deployment in the cluster " + input .ClusterProxy .GetName ())
1416+ RemoveDeployment (ctx , func () RemoveDeploymentInput {
1417+ return RemoveDeploymentInput {
1418+ ManagementCluster : input .ClusterProxy ,
1419+ Namespace : IRSOControllerNameSpace ,
1420+ Name : "ironic-standalone-operator-controller-manager" ,
1421+ }
1422+ })
1423+ } else {
1424+ By ("Uninstalling IRSO operator via kustomize" )
1425+ err := BuildAndRemoveKustomization (ctx , input .IrsoOperatorKustomize , input .ClusterProxy )
1426+ Expect (err ).NotTo (HaveOccurred ())
1427+ }
1428+
1429+ clusterClient := input .ClusterProxy .GetClient ()
1430+
1431+ // Delete secrets
1432+ secretNames := []string {"ironic-auth" , "ironic-cert" , "ironic-cacert" }
1433+ for _ , s := range secretNames {
1434+ Byf ("Deleting secret %s" , s )
1435+ secret := & corev1.Secret {ObjectMeta : metav1.ObjectMeta {Name : s , Namespace : input .IronicNamespace }}
1436+ _ = clusterClient .Delete (ctx , secret )
1437+ }
1438+
1439+ // Wait for secrets to be deleted
1440+ By ("Waiting for Ironic secrets to be deleted" )
1441+ Eventually (func () bool {
1442+ for _ , s := range secretNames {
1443+ errS := clusterClient .Get (ctx , client.ObjectKey {Name : s , Namespace : input .IronicNamespace }, & corev1.Secret {})
1444+ if errS == nil || ! apierrors .IsNotFound (errS ) {
1445+ return false
1446+ }
1447+ }
1448+ return true
1449+ }, input .E2EConfig .GetIntervals ("default" , "wait-delete-ironic" )... ).Should (BeTrue (), "IRSO/Ironic resources not fully deleted" )
1450+
1451+ By ("IRSO and Ironic resources uninstalled" )
1452+ return nil
1453+ }
0 commit comments