github.com/openshift/installer@v1.4.17/pkg/destroy/powervs/powervs.go (about)

     1  package powervs
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math"
     8  	gohttp "net/http"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/IBM-Cloud/bluemix-go/crn"
    14  	"github.com/IBM-Cloud/power-go-client/clients/instance"
    15  	"github.com/IBM-Cloud/power-go-client/ibmpisession"
    16  	"github.com/IBM/go-sdk-core/v5/core"
    17  	"github.com/IBM/networking-go-sdk/dnsrecordsv1"
    18  	"github.com/IBM/networking-go-sdk/dnszonesv1"
    19  	"github.com/IBM/networking-go-sdk/resourcerecordsv1"
    20  	"github.com/IBM/networking-go-sdk/transitgatewayapisv1"
    21  	"github.com/IBM/networking-go-sdk/zonesv1"
    22  	"github.com/IBM/platform-services-go-sdk/resourcecontrollerv2"
    23  	"github.com/IBM/platform-services-go-sdk/resourcemanagerv2"
    24  	"github.com/IBM/vpc-go-sdk/vpcv1"
    25  	"github.com/sirupsen/logrus"
    26  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    27  	"k8s.io/apimachinery/pkg/util/wait"
    28  
    29  	"github.com/openshift/installer/pkg/asset/installconfig/powervs"
    30  	"github.com/openshift/installer/pkg/destroy/providers"
    31  	"github.com/openshift/installer/pkg/types"
    32  	powervstypes "github.com/openshift/installer/pkg/types/powervs"
    33  	"github.com/openshift/installer/pkg/version"
    34  )
    35  
    36  var (
    37  	defaultTimeout = 30 * time.Minute
    38  	stageTimeout   = 15 * time.Minute
    39  )
    40  
    41  func leftInContext(ctx context.Context) time.Duration {
    42  	deadline, ok := ctx.Deadline()
    43  	if !ok {
    44  		return math.MaxInt64
    45  	}
    46  
    47  	duration := time.Until(deadline)
    48  
    49  	return duration
    50  }
    51  
    52  const (
    53  	// resource Id for Power Systems Virtual Server in the Global catalog.
    54  	powerIAASResourceID = "abd259f0-9990-11e8-acc8-b9f54a8f1661"
    55  
    56  	// cisServiceID is the Cloud Internet Services' catalog service ID.
    57  	cisServiceID = "75874a60-cb12-11e7-948e-37ac098eb1b9"
    58  )
    59  
    60  // User information.
    61  type User struct {
    62  	ID         string
    63  	Email      string
    64  	Account    string
    65  	cloudName  string `default:"bluemix"`
    66  	cloudType  string `default:"public"`
    67  	generation int    `default:"2"`
    68  }
    69  
    70  // ClusterUninstaller holds the various options for the cluster we want to delete.
    71  type ClusterUninstaller struct {
    72  	APIKey         string
    73  	BaseDomain     string
    74  	CISInstanceCRN string
    75  	ClusterName    string
    76  	Context        context.Context
    77  	DNSInstanceCRN string
    78  	DNSZone        string
    79  	InfraID        string
    80  	Logger         logrus.FieldLogger
    81  	Region         string
    82  	ServiceGUID    string
    83  	VPCRegion      string
    84  	Zone           string
    85  
    86  	managementSvc      *resourcemanagerv2.ResourceManagerV2
    87  	controllerSvc      *resourcecontrollerv2.ResourceControllerV2
    88  	vpcSvc             *vpcv1.VpcV1
    89  	zonesSvc           *zonesv1.ZonesV1
    90  	dnsRecordsSvc      *dnsrecordsv1.DnsRecordsV1
    91  	dnsZonesSvc        *dnszonesv1.DnsZonesV1
    92  	resourceRecordsSvc *resourcerecordsv1.ResourceRecordsV1
    93  	piSession          *ibmpisession.IBMPISession
    94  	instanceClient     *instance.IBMPIInstanceClient
    95  	imageClient        *instance.IBMPIImageClient
    96  	jobClient          *instance.IBMPIJobClient
    97  	keyClient          *instance.IBMPIKeyClient
    98  	dhcpClient         *instance.IBMPIDhcpClient
    99  	networkClient      *instance.IBMPINetworkClient
   100  	tgClient           *transitgatewayapisv1.TransitGatewayApisV1
   101  
   102  	resourceGroupID string
   103  	cosInstanceID   string
   104  	dnsZoneID       string
   105  
   106  	errorTracker
   107  	pendingItemTracker
   108  }
   109  
   110  // New returns an IBMCloud destroyer from ClusterMetadata.
   111  func New(logger logrus.FieldLogger, metadata *types.ClusterMetadata) (providers.Destroyer, error) {
   112  	var (
   113  		bxClient *powervs.BxClient
   114  		APIKey   string
   115  		err      error
   116  	)
   117  
   118  	// We need to prompt for missing variables because NewPISession requires them!
   119  	bxClient, err = powervs.NewBxClient(true)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	APIKey = bxClient.GetBxClientAPIKey()
   124  	if APIKey == "" {
   125  		return nil, errors.New("powervs.GetSession did not return an API key")
   126  	}
   127  
   128  	logger.Debugf("powervs.New: metadata.InfraID = %v", metadata.InfraID)
   129  	logger.Debugf("powervs.New: metadata.ClusterPlatformMetadata.PowerVS.BaseDomain = %v", metadata.ClusterPlatformMetadata.PowerVS.BaseDomain)
   130  	logger.Debugf("powervs.New: metadata.ClusterPlatformMetadata.PowerVS.CISInstanceCRN = %v", metadata.ClusterPlatformMetadata.PowerVS.CISInstanceCRN)
   131  	logger.Debugf("powervs.New: metadata.ClusterPlatformMetadata.PowerVS.DNSInstanceCRN = %v", metadata.ClusterPlatformMetadata.PowerVS.DNSInstanceCRN)
   132  	logger.Debugf("powervs.New: metadata.ClusterPlatformMetadata.PowerVS.PowerVSResourceGroup = %v", metadata.ClusterPlatformMetadata.PowerVS.PowerVSResourceGroup)
   133  	logger.Debugf("powervs.New: metadata.ClusterPlatformMetadata.PowerVS.Region = %v", metadata.ClusterPlatformMetadata.PowerVS.Region)
   134  	logger.Debugf("powervs.New: metadata.ClusterPlatformMetadata.PowerVS.VPCRegion = %v", metadata.ClusterPlatformMetadata.PowerVS.VPCRegion)
   135  	logger.Debugf("powervs.New: metadata.ClusterPlatformMetadata.PowerVS.Zone = %v", metadata.ClusterPlatformMetadata.PowerVS.Zone)
   136  	logger.Debugf("powervs.New: metadata.ClusterPlatformMetadata.PowerVS.ServiceInstanceGUID = %v", metadata.ClusterPlatformMetadata.PowerVS.ServiceInstanceGUID)
   137  
   138  	// Handle an optional setting in install-config.yaml
   139  	if metadata.ClusterPlatformMetadata.PowerVS.VPCRegion == "" {
   140  		var derivedVPCRegion string
   141  		if derivedVPCRegion, err = powervstypes.VPCRegionForPowerVSRegion(metadata.ClusterPlatformMetadata.PowerVS.Region); err != nil {
   142  			return nil, fmt.Errorf("powervs.New failed to derive VPCRegion: %w", err)
   143  		}
   144  		logger.Debugf("powervs.New: PowerVS.VPCRegion is missing, derived VPCRegion = %v", derivedVPCRegion)
   145  		metadata.ClusterPlatformMetadata.PowerVS.VPCRegion = derivedVPCRegion
   146  	}
   147  
   148  	return &ClusterUninstaller{
   149  		APIKey:             APIKey,
   150  		BaseDomain:         metadata.ClusterPlatformMetadata.PowerVS.BaseDomain,
   151  		ClusterName:        metadata.ClusterName,
   152  		Context:            context.Background(),
   153  		Logger:             logger,
   154  		InfraID:            metadata.InfraID,
   155  		CISInstanceCRN:     metadata.ClusterPlatformMetadata.PowerVS.CISInstanceCRN,
   156  		DNSInstanceCRN:     metadata.ClusterPlatformMetadata.PowerVS.DNSInstanceCRN,
   157  		Region:             metadata.ClusterPlatformMetadata.PowerVS.Region,
   158  		VPCRegion:          metadata.ClusterPlatformMetadata.PowerVS.VPCRegion,
   159  		Zone:               metadata.ClusterPlatformMetadata.PowerVS.Zone,
   160  		pendingItemTracker: newPendingItemTracker(),
   161  		resourceGroupID:    metadata.ClusterPlatformMetadata.PowerVS.PowerVSResourceGroup,
   162  		ServiceGUID:        metadata.ClusterPlatformMetadata.PowerVS.ServiceInstanceGUID,
   163  	}, nil
   164  }
   165  
   166  // Run is the entrypoint to start the uninstall process.
   167  func (o *ClusterUninstaller) Run() (*types.ClusterQuota, error) {
   168  	o.Logger.Debugf("powervs.Run")
   169  
   170  	var ctx context.Context
   171  	var deadline time.Time
   172  	var ok bool
   173  	var err error
   174  
   175  	ctx, cancel := o.contextWithTimeout()
   176  	defer cancel()
   177  
   178  	if ctx == nil {
   179  		return nil, fmt.Errorf("powervs.Run: contextWithTimeout returns nil: %w", err)
   180  	}
   181  
   182  	deadline, ok = ctx.Deadline()
   183  	if !ok {
   184  		return nil, fmt.Errorf("powervs.Run: failed to call ctx.Deadline: %w", err)
   185  	}
   186  
   187  	var duration time.Duration = deadline.Sub(time.Now())
   188  
   189  	o.Logger.Debugf("powervs.Run: duration = %v", duration)
   190  
   191  	if duration <= 0 {
   192  		return nil, fmt.Errorf("powervs.Run: duration is <= 0 (%v)", duration)
   193  	}
   194  
   195  	err = wait.PollImmediateInfinite(
   196  		duration,
   197  		o.PolledRun,
   198  	)
   199  
   200  	o.Logger.Debugf("powervs.Run: after wait.PollImmediateInfinite, err = %v", err)
   201  
   202  	return nil, err
   203  }
   204  
   205  // PolledRun is the Run function which will be called with Polling.
   206  func (o *ClusterUninstaller) PolledRun() (bool, error) {
   207  	o.Logger.Debugf("powervs.PolledRun")
   208  
   209  	var err error
   210  
   211  	err = o.loadSDKServices()
   212  	if err != nil {
   213  		o.Logger.Debugf("powervs.PolledRun: Failed loadSDKServices")
   214  		return false, err
   215  	}
   216  
   217  	err = o.destroyCluster()
   218  	if err != nil {
   219  		o.Logger.Debugf("powervs.PolledRun: Failed destroyCluster")
   220  		return false, fmt.Errorf("failed to destroy cluster: %w", err)
   221  	}
   222  
   223  	return true, nil
   224  }
   225  
   226  func (o *ClusterUninstaller) destroyCluster() error {
   227  	stagedFuncs := [][]struct {
   228  		name    string
   229  		execute func() error
   230  	}{{
   231  		{name: "Transit Gateways", execute: o.destroyTransitGateways},
   232  	}, {
   233  		{name: "Cloud Instances", execute: o.destroyCloudInstances},
   234  	}, {
   235  		{name: "Power Instances", execute: o.destroyPowerInstances},
   236  	}, {
   237  		{name: "Load Balancers", execute: o.destroyLoadBalancers},
   238  	}, {
   239  		{name: "Cloud Subnets", execute: o.destroyCloudSubnets},
   240  	}, {
   241  		{name: "Public Gateways", execute: o.destroyPublicGateways},
   242  	}, {
   243  		{name: "DHCPs", execute: o.destroyDHCPNetworks},
   244  	}, {
   245  		{name: "Power Subnets", execute: o.destroyPowerSubnets},
   246  		{name: "Images", execute: o.destroyImages},
   247  		{name: "VPCs", execute: o.destroyVPCs},
   248  	}, {
   249  		{name: "Security Groups", execute: o.destroySecurityGroups},
   250  	}, {
   251  		{name: "Cloud Object Storage Instances", execute: o.destroyCOSInstances},
   252  		{name: "Cloud SSH Keys", execute: o.destroyCloudSSHKeys},
   253  		{name: "Power SSH Keys", execute: o.destroyPowerSSHKeys},
   254  	}, {
   255  		{name: "DNS Records", execute: o.destroyDNSRecords},
   256  		{name: "DNS Resource Records", execute: o.destroyResourceRecords},
   257  	}, {
   258  		{name: "Service Instances", execute: o.destroyServiceInstances},
   259  	}}
   260  
   261  	for _, stage := range stagedFuncs {
   262  		var wg sync.WaitGroup
   263  		errCh := make(chan error)
   264  		wgDone := make(chan bool)
   265  
   266  		for _, f := range stage {
   267  			wg.Add(1)
   268  			// Start a parallel goroutine
   269  			go o.executeStageFunction(f, errCh, &wg)
   270  		}
   271  
   272  		// Start a parallel goroutine
   273  		go func() {
   274  			wg.Wait()
   275  			close(wgDone)
   276  		}()
   277  
   278  		select {
   279  		// Did the wait group goroutine finish?
   280  		case <-wgDone:
   281  			// On to the next stage
   282  			o.Logger.Debugf("destroyCluster: <-wgDone")
   283  			continue
   284  		// Have we taken too long?
   285  		case <-time.After(stageTimeout):
   286  			return fmt.Errorf("destroyCluster: timed out")
   287  		// Has an error been sent via the channel?
   288  		case err := <-errCh:
   289  			return err
   290  		}
   291  	}
   292  
   293  	return nil
   294  }
   295  
   296  func (o *ClusterUninstaller) executeStageFunction(f struct {
   297  	name    string
   298  	execute func() error
   299  }, errCh chan error, wg *sync.WaitGroup) error {
   300  	o.Logger.Debugf("executeStageFunction: Adding: %s", f.name)
   301  
   302  	defer wg.Done()
   303  
   304  	var ctx context.Context
   305  	var deadline time.Time
   306  	var ok bool
   307  	var err error
   308  
   309  	ctx, cancel := o.contextWithTimeout()
   310  	defer cancel()
   311  
   312  	if ctx == nil {
   313  		return fmt.Errorf("executeStageFunction contextWithTimeout returns nil: %w", err)
   314  	}
   315  
   316  	deadline, ok = ctx.Deadline()
   317  	if !ok {
   318  		return fmt.Errorf("executeStageFunction failed to call ctx.Deadline: %w", err)
   319  	}
   320  
   321  	var duration time.Duration = deadline.Sub(time.Now())
   322  
   323  	o.Logger.Debugf("executeStageFunction: duration = %v", duration)
   324  	if duration <= 0 {
   325  		return fmt.Errorf("executeStageFunction: duration is <= 0 (%v)", duration)
   326  	}
   327  
   328  	err = wait.PollImmediateInfinite(
   329  		duration,
   330  		func() (bool, error) {
   331  			var err error
   332  
   333  			o.Logger.Debugf("executeStageFunction: Executing: %s", f.name)
   334  
   335  			err = f.execute()
   336  			if err != nil {
   337  				o.Logger.Debugf("ERROR: executeStageFunction: %s: %v", f.name, err)
   338  
   339  				return false, err
   340  			}
   341  
   342  			return true, nil
   343  		},
   344  	)
   345  
   346  	if err != nil {
   347  		errCh <- err
   348  	}
   349  	return nil
   350  }
   351  
   352  func (o *ClusterUninstaller) newAuthenticator(apikey string) (core.Authenticator, error) {
   353  	var (
   354  		authenticator core.Authenticator
   355  		err           error
   356  	)
   357  
   358  	if apikey == "" {
   359  		return nil, errors.New("newAuthenticator: apikey is empty")
   360  	}
   361  
   362  	authenticator = &core.IamAuthenticator{
   363  		ApiKey: apikey,
   364  	}
   365  
   366  	err = authenticator.Validate()
   367  	if err != nil {
   368  		return nil, fmt.Errorf("newAuthenticator: authenticator.Validate: %w", err)
   369  	}
   370  
   371  	return authenticator, nil
   372  }
   373  
   374  func (o *ClusterUninstaller) loadSDKServices() error {
   375  	var (
   376  		err           error
   377  		authenticator core.Authenticator
   378  		versionDate   = "2023-07-04"
   379  		tgOptions     *transitgatewayapisv1.TransitGatewayApisV1Options
   380  		serviceName   string
   381  	)
   382  
   383  	defer func() {
   384  		o.Logger.Debugf("loadSDKServices: o.ServiceGUID = %v", o.ServiceGUID)
   385  		o.Logger.Debugf("loadSDKServices: o.piSession = %v", o.piSession)
   386  		o.Logger.Debugf("loadSDKServices: o.instanceClient = %v", o.instanceClient)
   387  		o.Logger.Debugf("loadSDKServices: o.imageClient = %v", o.imageClient)
   388  		o.Logger.Debugf("loadSDKServices: o.jobClient = %v", o.jobClient)
   389  		o.Logger.Debugf("loadSDKServices: o.keyClient = %v", o.keyClient)
   390  		o.Logger.Debugf("loadSDKServices: o.vpcSvc = %v", o.vpcSvc)
   391  		o.Logger.Debugf("loadSDKServices: o.managementSvc = %v", o.managementSvc)
   392  		o.Logger.Debugf("loadSDKServices: o.controllerSvc = %v", o.controllerSvc)
   393  	}()
   394  
   395  	if o.APIKey == "" {
   396  		return fmt.Errorf("loadSDKServices: missing APIKey in metadata.json")
   397  	}
   398  
   399  	user, err := powervs.FetchUserDetails(o.APIKey)
   400  	if err != nil {
   401  		return fmt.Errorf("loadSDKServices: fetchUserDetails: %w", err)
   402  	}
   403  
   404  	authenticator, err = o.newAuthenticator(o.APIKey)
   405  	if err != nil {
   406  		return err
   407  	}
   408  
   409  	var options *ibmpisession.IBMPIOptions = &ibmpisession.IBMPIOptions{
   410  		Authenticator: authenticator,
   411  		Debug:         false,
   412  		UserAccount:   user.Account,
   413  		Zone:          o.Zone,
   414  	}
   415  
   416  	o.piSession, err = ibmpisession.NewIBMPISession(options)
   417  	if (err != nil) || (o.piSession == nil) {
   418  		if err != nil {
   419  			return fmt.Errorf("loadSDKServices: ibmpisession.New: %w", err)
   420  		}
   421  		return fmt.Errorf("loadSDKServices: o.piSession is nil")
   422  	}
   423  
   424  	authenticator, err = o.newAuthenticator(o.APIKey)
   425  	if err != nil {
   426  		return err
   427  	}
   428  
   429  	// https://raw.githubusercontent.com/IBM/vpc-go-sdk/master/vpcv1/vpc_v1.go
   430  	o.vpcSvc, err = vpcv1.NewVpcV1(&vpcv1.VpcV1Options{
   431  		Authenticator: authenticator,
   432  		URL:           "https://" + o.VPCRegion + ".iaas.cloud.ibm.com/v1",
   433  	})
   434  	if err != nil {
   435  		return fmt.Errorf("loadSDKServices: vpcv1.NewVpcV1: %w", err)
   436  	}
   437  
   438  	userAgentString := fmt.Sprintf("OpenShift/4.x Destroyer/%s", version.Raw)
   439  	o.vpcSvc.Service.SetUserAgent(userAgentString)
   440  
   441  	authenticator, err = o.newAuthenticator(o.APIKey)
   442  	if err != nil {
   443  		return err
   444  	}
   445  
   446  	// Instantiate the service with an API key based IAM authenticator
   447  	o.managementSvc, err = resourcemanagerv2.NewResourceManagerV2(&resourcemanagerv2.ResourceManagerV2Options{
   448  		Authenticator: authenticator,
   449  	})
   450  	if err != nil {
   451  		return fmt.Errorf("loadSDKServices: creating ResourceManagerV2 Service: %w", err)
   452  	}
   453  
   454  	authenticator, err = o.newAuthenticator(o.APIKey)
   455  	if err != nil {
   456  		return err
   457  	}
   458  
   459  	// Instantiate the service with an API key based IAM authenticator
   460  	o.controllerSvc, err = resourcecontrollerv2.NewResourceControllerV2(&resourcecontrollerv2.ResourceControllerV2Options{
   461  		Authenticator: authenticator,
   462  		ServiceName:   "cloud-object-storage",
   463  		URL:           "https://resource-controller.cloud.ibm.com",
   464  	})
   465  	if err != nil {
   466  		return fmt.Errorf("loadSDKServices: creating ControllerV2 Service: %w", err)
   467  	}
   468  
   469  	authenticator, err = o.newAuthenticator(o.APIKey)
   470  	if err != nil {
   471  		return err
   472  	}
   473  
   474  	tgOptions = &transitgatewayapisv1.TransitGatewayApisV1Options{
   475  		Authenticator: authenticator,
   476  		Version:       &versionDate,
   477  	}
   478  
   479  	o.tgClient, err = transitgatewayapisv1.NewTransitGatewayApisV1(tgOptions)
   480  	if err != nil {
   481  		return fmt.Errorf("loadSDKServices: NewTransitGatewayApisV1: %w", err)
   482  	}
   483  
   484  	// Either CISInstanceCRN is set or DNSInstanceCRN is set. Both should not be set at the same time,
   485  	// but check both just to be safe.
   486  	if len(o.CISInstanceCRN) > 0 {
   487  		authenticator, err = o.newAuthenticator(o.APIKey)
   488  		if err != nil {
   489  			return err
   490  		}
   491  
   492  		o.zonesSvc, err = zonesv1.NewZonesV1(&zonesv1.ZonesV1Options{
   493  			Authenticator: authenticator,
   494  			Crn:           &o.CISInstanceCRN,
   495  		})
   496  		if err != nil {
   497  			return fmt.Errorf("loadSDKServices: creating zonesSvc: %w", err)
   498  		}
   499  
   500  		ctx, cancel := o.contextWithTimeout()
   501  		defer cancel()
   502  
   503  		// Get the Zone ID
   504  		zoneOptions := o.zonesSvc.NewListZonesOptions()
   505  		zoneResources, detailedResponse, err := o.zonesSvc.ListZonesWithContext(ctx, zoneOptions)
   506  		if err != nil {
   507  			return fmt.Errorf("loadSDKServices: Failed to list Zones: %w and the response is: %s", err, detailedResponse)
   508  		}
   509  
   510  		for _, zone := range zoneResources.Result {
   511  			o.Logger.Debugf("loadSDKServices: Zone: %v", *zone.Name)
   512  			if strings.Contains(o.BaseDomain, *zone.Name) {
   513  				o.dnsZoneID = *zone.ID
   514  			}
   515  		}
   516  
   517  		authenticator, err = o.newAuthenticator(o.APIKey)
   518  		if err != nil {
   519  			return err
   520  		}
   521  
   522  		o.dnsRecordsSvc, err = dnsrecordsv1.NewDnsRecordsV1(&dnsrecordsv1.DnsRecordsV1Options{
   523  			Authenticator:  authenticator,
   524  			Crn:            &o.CISInstanceCRN,
   525  			ZoneIdentifier: &o.dnsZoneID,
   526  		})
   527  		if err != nil {
   528  			return fmt.Errorf("loadSDKServices: Failed to instantiate dnsRecordsSvc: %w", err)
   529  		}
   530  	}
   531  
   532  	if len(o.DNSInstanceCRN) > 0 {
   533  		authenticator, err = o.newAuthenticator(o.APIKey)
   534  		if err != nil {
   535  			return err
   536  		}
   537  
   538  		o.dnsZonesSvc, err = dnszonesv1.NewDnsZonesV1(&dnszonesv1.DnsZonesV1Options{
   539  			Authenticator: authenticator,
   540  		})
   541  		if err != nil {
   542  			return fmt.Errorf("loadSDKServices: creating zonesSvc: %w", err)
   543  		}
   544  
   545  		// Get the Zone ID
   546  		dnsCRN, err := crn.Parse(o.DNSInstanceCRN)
   547  		if err != nil {
   548  			return fmt.Errorf("failed to parse DNSInstanceCRN: %w", err)
   549  		}
   550  		options := o.dnsZonesSvc.NewListDnszonesOptions(dnsCRN.ServiceInstance)
   551  		listZonesResponse, detailedResponse, err := o.dnsZonesSvc.ListDnszones(options)
   552  		if err != nil {
   553  			return fmt.Errorf("loadSDKServices: Failed to list Zones: %w and the response is: %s", err, detailedResponse)
   554  		}
   555  
   556  		for _, zone := range listZonesResponse.Dnszones {
   557  			o.Logger.Debugf("loadSDKServices: Zone: %v", *zone.Name)
   558  			if strings.Contains(o.BaseDomain, *zone.Name) {
   559  				o.dnsZoneID = *zone.ID
   560  			}
   561  		}
   562  
   563  		authenticator, err = o.newAuthenticator(o.APIKey)
   564  		if err != nil {
   565  			return err
   566  		}
   567  
   568  		o.resourceRecordsSvc, err = resourcerecordsv1.NewResourceRecordsV1(&resourcerecordsv1.ResourceRecordsV1Options{
   569  			Authenticator: authenticator,
   570  		})
   571  		if err != nil {
   572  			return fmt.Errorf("loadSDKServices: Failed to instantiate resourceRecordsSvc: %w", err)
   573  		}
   574  	}
   575  
   576  	// If we should have created a service instance dynamically
   577  	if o.ServiceGUID == "" {
   578  		serviceName = fmt.Sprintf("%s-power-iaas", o.InfraID)
   579  		o.Logger.Debugf("loadSDKServices: serviceName = %v", serviceName)
   580  
   581  		o.ServiceGUID, err = o.ServiceInstanceNameToGUID(context.Background(), serviceName)
   582  		if err != nil {
   583  			return fmt.Errorf("loadSDKServices: ServiceInstanceNameToGUID: %w", err)
   584  		}
   585  	}
   586  	if o.ServiceGUID == "" {
   587  		// The rest of this function relies on o.ServiceGUID, so finish now!
   588  		return nil
   589  	}
   590  
   591  	o.instanceClient = instance.NewIBMPIInstanceClient(context.Background(), o.piSession, o.ServiceGUID)
   592  	if o.instanceClient == nil {
   593  		return fmt.Errorf("loadSDKServices: o.instanceClient is nil")
   594  	}
   595  
   596  	o.imageClient = instance.NewIBMPIImageClient(context.Background(), o.piSession, o.ServiceGUID)
   597  	if o.imageClient == nil {
   598  		return fmt.Errorf("loadSDKServices: o.imageClient is nil")
   599  	}
   600  
   601  	o.jobClient = instance.NewIBMPIJobClient(context.Background(), o.piSession, o.ServiceGUID)
   602  	if o.jobClient == nil {
   603  		return fmt.Errorf("loadSDKServices: o.jobClient is nil")
   604  	}
   605  
   606  	o.keyClient = instance.NewIBMPIKeyClient(context.Background(), o.piSession, o.ServiceGUID)
   607  	if o.keyClient == nil {
   608  		return fmt.Errorf("loadSDKServices: o.keyClient is nil")
   609  	}
   610  
   611  	o.dhcpClient = instance.NewIBMPIDhcpClient(context.Background(), o.piSession, o.ServiceGUID)
   612  	if o.dhcpClient == nil {
   613  		return fmt.Errorf("loadSDKServices: o.dhcpClient is nil")
   614  	}
   615  
   616  	o.networkClient = instance.NewIBMPINetworkClient(context.Background(), o.piSession, o.ServiceGUID)
   617  	if o.networkClient == nil {
   618  		return fmt.Errorf("loadSDKServices: o.networkClient is nil")
   619  	}
   620  
   621  	return nil
   622  }
   623  
   624  // ServiceInstanceNameToGUID returns the GUID of the matching service instance name which was passed in.
   625  func (o *ClusterUninstaller) ServiceInstanceNameToGUID(ctx context.Context, name string) (string, error) {
   626  	var (
   627  		options   *resourcecontrollerv2.ListResourceInstancesOptions
   628  		resources *resourcecontrollerv2.ResourceInstancesList
   629  		err       error
   630  		perPage   int64 = 10
   631  		moreData        = true
   632  		nextURL   *string
   633  		groupID   = o.resourceGroupID
   634  	)
   635  
   636  	o.Logger.Debugf("ServiceInstanceNameToGUID: groupID = %s", groupID)
   637  	// If the user passes in a human readable group id, then we need to convert it to a UUID
   638  	listGroupOptions := o.managementSvc.NewListResourceGroupsOptions()
   639  	groups, _, err := o.managementSvc.ListResourceGroupsWithContext(ctx, listGroupOptions)
   640  	if err != nil {
   641  		return "", fmt.Errorf("failed to list resource groups: %w", err)
   642  	}
   643  	for _, group := range groups.Resources {
   644  		o.Logger.Debugf("ServiceInstanceNameToGUID: group.Name = %s", *group.Name)
   645  		if *group.Name == groupID {
   646  			groupID = *group.ID
   647  		}
   648  	}
   649  	o.Logger.Debugf("ServiceInstanceNameToGUID: groupID = %s", groupID)
   650  
   651  	options = o.controllerSvc.NewListResourceInstancesOptions()
   652  	options.SetResourceGroupID(groupID)
   653  	// resource ID for Power Systems Virtual Server in the Global catalog
   654  	options.SetResourceID(powerIAASResourceID)
   655  	options.SetLimit(perPage)
   656  
   657  	for moreData {
   658  		resources, _, err = o.controllerSvc.ListResourceInstancesWithContext(ctx, options)
   659  		if err != nil {
   660  			return "", fmt.Errorf("failed to list resource instances: %w", err)
   661  		}
   662  
   663  		for _, resource := range resources.Resources {
   664  			var (
   665  				getResourceOptions *resourcecontrollerv2.GetResourceInstanceOptions
   666  				resourceInstance   *resourcecontrollerv2.ResourceInstance
   667  				response           *core.DetailedResponse
   668  			)
   669  
   670  			o.Logger.Debugf("ServiceInstanceNameToGUID: resource.Name = %s", *resource.Name)
   671  
   672  			getResourceOptions = o.controllerSvc.NewGetResourceInstanceOptions(*resource.ID)
   673  
   674  			resourceInstance, response, err = o.controllerSvc.GetResourceInstance(getResourceOptions)
   675  			if err != nil {
   676  				return "", fmt.Errorf("failed to get instance: %w", err)
   677  			}
   678  			if response != nil && response.StatusCode == gohttp.StatusNotFound || response.StatusCode == gohttp.StatusInternalServerError {
   679  				return "", fmt.Errorf("failed to get instance, response is: %v", response)
   680  			}
   681  
   682  			if resourceInstance.Type == nil {
   683  				o.Logger.Debugf("ServiceInstanceNameToGUID: type: nil")
   684  				continue
   685  			}
   686  			o.Logger.Debugf("ServiceInstanceNameToGUID: type: %v", *resourceInstance.Type)
   687  			if resourceInstance.GUID == nil {
   688  				o.Logger.Debugf("ServiceInstanceNameToGUID: GUID: nil")
   689  				continue
   690  			}
   691  			if *resourceInstance.Type != "service_instance" && *resourceInstance.Type != "composite_instance" {
   692  				continue
   693  			}
   694  			if *resourceInstance.Name != name {
   695  				continue
   696  			}
   697  
   698  			o.Logger.Debugf("ServiceInstanceNameToGUID: Found match!")
   699  
   700  			return *resourceInstance.GUID, nil
   701  		}
   702  
   703  		// Based on: https://cloud.ibm.com/apidocs/resource-controller/resource-controller?code=go#list-resource-instances
   704  		nextURL, err = core.GetQueryParam(resources.NextURL, "start")
   705  		if err != nil {
   706  			return "", fmt.Errorf("failed to GetQueryParam on start: %w", err)
   707  		}
   708  		if nextURL == nil {
   709  			options.SetStart("")
   710  		} else {
   711  			options.SetStart(*nextURL)
   712  		}
   713  
   714  		moreData = *resources.RowsCount == perPage
   715  	}
   716  
   717  	return "", nil
   718  }
   719  
   720  func (o *ClusterUninstaller) contextWithTimeout() (context.Context, context.CancelFunc) {
   721  	return context.WithTimeout(o.Context, defaultTimeout)
   722  }
   723  
   724  func (o *ClusterUninstaller) timeout(ctx context.Context) bool {
   725  	var deadline time.Time
   726  	var ok bool
   727  
   728  	deadline, ok = ctx.Deadline()
   729  	if !ok {
   730  		o.Logger.Debugf("timeout: deadline, ok = %v, %v", deadline, ok)
   731  		return true
   732  	}
   733  
   734  	var after bool = time.Now().After(deadline)
   735  
   736  	if after {
   737  		// 01/02 03:04:05PM ‘06 -0700
   738  		o.Logger.Debugf("timeout: after deadline! (%v)", deadline.Format("2006-01-02 03:04:05PM"))
   739  	}
   740  
   741  	return after
   742  }
   743  
   744  type ibmError struct {
   745  	Status  int
   746  	Message string
   747  }
   748  
   749  func isNoOp(err *ibmError) bool {
   750  	if err == nil {
   751  		return false
   752  	}
   753  
   754  	return err.Status == gohttp.StatusNotFound
   755  }
   756  
   757  // aggregateError is a utility function that takes a slice of errors and an
   758  // optional pending argument, and returns an error or nil.
   759  func aggregateError(errs []error, pending ...int) error {
   760  	err := utilerrors.NewAggregate(errs)
   761  	if err != nil {
   762  		return err
   763  	}
   764  	if len(pending) > 0 && pending[0] > 0 {
   765  		return fmt.Errorf("%d items pending", pending[0])
   766  	}
   767  	return nil
   768  }
   769  
   770  // pendingItemTracker tracks a set of pending item names for a given type of resource.
   771  type pendingItemTracker struct {
   772  	pendingItems map[string]cloudResources
   773  }
   774  
   775  func newPendingItemTracker() pendingItemTracker {
   776  	return pendingItemTracker{
   777  		pendingItems: map[string]cloudResources{},
   778  	}
   779  }
   780  
   781  // GetAllPendintItems returns a slice of all of the pending items across all types.
   782  func (t pendingItemTracker) GetAllPendingItems() []cloudResource {
   783  	var items []cloudResource
   784  	for _, is := range t.pendingItems {
   785  		for _, i := range is {
   786  			items = append(items, i)
   787  		}
   788  	}
   789  	return items
   790  }
   791  
   792  // getPendingItems returns the list of resources to be deleted.
   793  func (t pendingItemTracker) getPendingItems(itemType string) []cloudResource {
   794  	lastFound, exists := t.pendingItems[itemType]
   795  	if !exists {
   796  		lastFound = cloudResources{}
   797  	}
   798  	return lastFound.list()
   799  }
   800  
   801  // insertPendingItems adds to the list of resources to be deleted.
   802  func (t pendingItemTracker) insertPendingItems(itemType string, items []cloudResource) []cloudResource {
   803  	lastFound, exists := t.pendingItems[itemType]
   804  	if !exists {
   805  		lastFound = cloudResources{}
   806  	}
   807  	lastFound = lastFound.insert(items...)
   808  	t.pendingItems[itemType] = lastFound
   809  	return lastFound.list()
   810  }
   811  
   812  // deletePendingItems removes from the list of resources to be deleted.
   813  func (t pendingItemTracker) deletePendingItems(itemType string, items []cloudResource) []cloudResource {
   814  	lastFound, exists := t.pendingItems[itemType]
   815  	if !exists {
   816  		lastFound = cloudResources{}
   817  	}
   818  	lastFound = lastFound.delete(items...)
   819  	t.pendingItems[itemType] = lastFound
   820  	return lastFound.list()
   821  }
   822  
   823  func isErrorStatus(code int64) bool {
   824  	return code != 0 && (code < 200 || code >= 300)
   825  }