|
| 1 | +package powervs |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "math" |
| 7 | + gohttp "net/http" |
| 8 | + "strings" |
| 9 | + "time" |
| 10 | + |
| 11 | + "github.com/IBM/go-sdk-core/v5/core" |
| 12 | + "github.com/IBM/platform-services-go-sdk/resourcecontrollerv2" |
| 13 | + "github.com/pkg/errors" |
| 14 | + "k8s.io/apimachinery/pkg/util/wait" |
| 15 | +) |
| 16 | + |
| 17 | +const ( |
| 18 | + serviceInstanceTypeName = "service instance" |
| 19 | + |
| 20 | + // resource ID for Power Systems Virtual Server in the Global catalog. |
| 21 | + virtualServerResourceID = "abd259f0-9990-11e8-acc8-b9f54a8f1661" |
| 22 | +) |
| 23 | + |
| 24 | +// convertResourceGroupNameToID converts a resource group name/id to an id. |
| 25 | +func (o *ClusterUninstaller) convertResourceGroupNameToID(resourceGroupID string) (string, error) { |
| 26 | + listResourceGroupsOptions := o.managementSvc.NewListResourceGroupsOptions() |
| 27 | + |
| 28 | + resourceGroups, _, err := o.managementSvc.ListResourceGroups(listResourceGroupsOptions) |
| 29 | + if err != nil { |
| 30 | + return "", err |
| 31 | + } |
| 32 | + |
| 33 | + for _, resourceGroup := range resourceGroups.Resources { |
| 34 | + if *resourceGroup.Name == resourceGroupID { |
| 35 | + return *resourceGroup.ID, nil |
| 36 | + } else if *resourceGroup.ID == resourceGroupID { |
| 37 | + return resourceGroupID, nil |
| 38 | + } |
| 39 | + } |
| 40 | + |
| 41 | + return "", errors.New(fmt.Sprintf("failed to find resource group %v", resourceGroupID)) |
| 42 | +} |
| 43 | + |
| 44 | +// listServiceInstances list service instances for the cluster. |
| 45 | +func (o *ClusterUninstaller) listServiceInstances() (cloudResources, error) { |
| 46 | + o.Logger.Debugf("Listing service instances") |
| 47 | + |
| 48 | + ctx, cancel := o.contextWithTimeout() |
| 49 | + defer cancel() |
| 50 | + |
| 51 | + select { |
| 52 | + case <-ctx.Done(): |
| 53 | + o.Logger.Debugf("listServiceInstances: case <-ctx.Done()") |
| 54 | + return nil, o.Context.Err() // we're cancelled, abort |
| 55 | + default: |
| 56 | + } |
| 57 | + |
| 58 | + var ( |
| 59 | + resourceGroupID string |
| 60 | + options *resourcecontrollerv2.ListResourceInstancesOptions |
| 61 | + resources *resourcecontrollerv2.ResourceInstancesList |
| 62 | + err error |
| 63 | + perPage int64 = 10 |
| 64 | + moreData = true |
| 65 | + nextURL *string |
| 66 | + ) |
| 67 | + |
| 68 | + resourceGroupID, err = o.convertResourceGroupNameToID(o.resourceGroupID) |
| 69 | + if err != nil { |
| 70 | + return nil, errors.Wrapf(err, "failed to convert resourceGroupID") |
| 71 | + } |
| 72 | + o.Logger.Debugf("listServiceInstances: converted %v to %v", o.resourceGroupID, resourceGroupID) |
| 73 | + |
| 74 | + options = o.controllerSvc.NewListResourceInstancesOptions() |
| 75 | + // options.SetType("resource_instance") |
| 76 | + options.SetResourceGroupID(resourceGroupID) |
| 77 | + options.SetResourceID(virtualServerResourceID) |
| 78 | + options.SetLimit(perPage) |
| 79 | + |
| 80 | + result := []cloudResource{} |
| 81 | + |
| 82 | + for moreData { |
| 83 | + if options.Start != nil { |
| 84 | + o.Logger.Debugf("listServiceInstances: options = %+v, options.Limit = %v, options.Start = %v, options.ResourceGroupID = %v", options, *options.Limit, *options.Start, *options.ResourceGroupID) |
| 85 | + } else { |
| 86 | + o.Logger.Debugf("listServiceInstances: options = %+v, options.Limit = %v, options.ResourceGroupID = %v", options, *options.Limit, *options.ResourceGroupID) |
| 87 | + } |
| 88 | + |
| 89 | + resources, _, err = o.controllerSvc.ListResourceInstancesWithContext(ctx, options) |
| 90 | + if err != nil { |
| 91 | + return nil, errors.Wrapf(err, "failed to list resource instances") |
| 92 | + } |
| 93 | + |
| 94 | + o.Logger.Debugf("listServiceInstances: resources.RowsCount = %v", *resources.RowsCount) |
| 95 | + |
| 96 | + for _, resource := range resources.Resources { |
| 97 | + var ( |
| 98 | + getResourceOptions *resourcecontrollerv2.GetResourceInstanceOptions |
| 99 | + resourceInstance *resourcecontrollerv2.ResourceInstance |
| 100 | + response *core.DetailedResponse |
| 101 | + ) |
| 102 | + |
| 103 | + o.Logger.Debugf("listServiceInstances: resource.Name = %s", *resource.Name) |
| 104 | + |
| 105 | + getResourceOptions = o.controllerSvc.NewGetResourceInstanceOptions(*resource.ID) |
| 106 | + |
| 107 | + resourceInstance, response, err = o.controllerSvc.GetResourceInstance(getResourceOptions) |
| 108 | + if err != nil { |
| 109 | + return nil, errors.Wrapf(err, "failed to get instance: %s", response) |
| 110 | + } |
| 111 | + if response != nil && response.StatusCode == gohttp.StatusNotFound { |
| 112 | + o.Logger.Debugf("listServiceInstances: gohttp.StatusNotFound") |
| 113 | + continue |
| 114 | + } else if response != nil && response.StatusCode == gohttp.StatusInternalServerError { |
| 115 | + o.Logger.Debugf("listServiceInstances: gohttp.StatusInternalServerError") |
| 116 | + continue |
| 117 | + } |
| 118 | + |
| 119 | + if resourceInstance.Type == nil { |
| 120 | + o.Logger.Debugf("listServiceInstances: type: nil") |
| 121 | + } else { |
| 122 | + o.Logger.Debugf("listServiceInstances: type: %v", *resourceInstance.Type) |
| 123 | + } |
| 124 | + |
| 125 | + if resourceInstance.Type != nil && *resourceInstance.Type == "service_instance" { |
| 126 | + if strings.Contains(*resource.Name, o.InfraID) { |
| 127 | + result = append(result, cloudResource{ |
| 128 | + key: *resource.ID, |
| 129 | + name: *resource.Name, |
| 130 | + status: "", |
| 131 | + typeName: serviceInstanceTypeName, |
| 132 | + id: *resource.ID, |
| 133 | + }) |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + // Based on: https://cloud.ibm.com/apidocs/resource-controller/resource-controller?code=go#list-resource-instances |
| 139 | + nextURL, err = core.GetQueryParam(resources.NextURL, "start") |
| 140 | + if err != nil { |
| 141 | + return nil, errors.Wrapf(err, "failed to GetQueryParam on start: %s", err) |
| 142 | + } |
| 143 | + if nextURL == nil { |
| 144 | + o.Logger.Debugf("nextURL = nil") |
| 145 | + options.SetStart("") |
| 146 | + } else { |
| 147 | + o.Logger.Debugf("nextURL = %v", *nextURL) |
| 148 | + options.SetStart(*nextURL) |
| 149 | + } |
| 150 | + |
| 151 | + moreData = *resources.RowsCount == perPage |
| 152 | + } |
| 153 | + |
| 154 | + return cloudResources{}.insert(result...), nil |
| 155 | +} |
| 156 | + |
| 157 | +// destroyServiceInstance destroys a service instance. |
| 158 | +func (o *ClusterUninstaller) destroyServiceInstance(item cloudResource) error { |
| 159 | + ctx, cancel := o.contextWithTimeout() |
| 160 | + defer cancel() |
| 161 | + |
| 162 | + select { |
| 163 | + case <-ctx.Done(): |
| 164 | + o.Logger.Debugf("destroyServiceInstance: case <-ctx.Done()") |
| 165 | + return o.Context.Err() // we're cancelled, abort |
| 166 | + default: |
| 167 | + } |
| 168 | + |
| 169 | + o.Logger.Debugf("destroyServiceInstance: Preparing to delete, item.name = %v", item.name) |
| 170 | + |
| 171 | + var ( |
| 172 | + getOptions *resourcecontrollerv2.GetResourceInstanceOptions |
| 173 | + response *core.DetailedResponse |
| 174 | + err error |
| 175 | + ) |
| 176 | + |
| 177 | + getOptions = o.controllerSvc.NewGetResourceInstanceOptions(item.id) |
| 178 | + |
| 179 | + _, response, err = o.controllerSvc.GetResourceInstanceWithContext(ctx, getOptions) |
| 180 | + |
| 181 | + if err != nil && response != nil && response.StatusCode == gohttp.StatusNotFound { |
| 182 | + // The resource is gone |
| 183 | + o.deletePendingItems(item.typeName, []cloudResource{item}) |
| 184 | + o.Logger.Infof("Deleted Service Instance %q", item.name) |
| 185 | + return nil |
| 186 | + } |
| 187 | + if err != nil && response != nil && response.StatusCode == gohttp.StatusInternalServerError { |
| 188 | + o.Logger.Infof("destroyServiceInstance: internal server error") |
| 189 | + return nil |
| 190 | + } |
| 191 | + |
| 192 | + options := o.controllerSvc.NewDeleteResourceInstanceOptions(item.id) |
| 193 | + options.SetRecursive(true) |
| 194 | + |
| 195 | + response, err = o.controllerSvc.DeleteResourceInstanceWithContext(ctx, options) |
| 196 | + |
| 197 | + if err != nil && response != nil && response.StatusCode != gohttp.StatusNotFound { |
| 198 | + return errors.Wrapf(err, "failed to delete service instance %s", item.name) |
| 199 | + } |
| 200 | + |
| 201 | + o.Logger.Infof("Deleted Service Instance %q", item.name) |
| 202 | + o.deletePendingItems(item.typeName, []cloudResource{item}) |
| 203 | + |
| 204 | + return nil |
| 205 | +} |
| 206 | + |
| 207 | +// destroyServiceInstances removes all service instances have a name containing |
| 208 | +// the cluster's infra ID. |
| 209 | +func (o *ClusterUninstaller) destroyServiceInstances() error { |
| 210 | + firstPassList, err := o.listServiceInstances() |
| 211 | + if err != nil { |
| 212 | + return err |
| 213 | + } |
| 214 | + |
| 215 | + if len(firstPassList.list()) == 0 { |
| 216 | + return nil |
| 217 | + } |
| 218 | + |
| 219 | + items := o.insertPendingItems(serviceInstanceTypeName, firstPassList.list()) |
| 220 | + |
| 221 | + ctx, cancel := o.contextWithTimeout() |
| 222 | + defer cancel() |
| 223 | + |
| 224 | + for _, item := range items { |
| 225 | + select { |
| 226 | + case <-ctx.Done(): |
| 227 | + o.Logger.Debugf("destroyServiceInstances: case <-ctx.Done()") |
| 228 | + return o.Context.Err() // we're cancelled, abort |
| 229 | + default: |
| 230 | + } |
| 231 | + |
| 232 | + backoff := wait.Backoff{ |
| 233 | + Duration: 15 * time.Second, |
| 234 | + Factor: 1.1, |
| 235 | + Cap: leftInContext(ctx), |
| 236 | + Steps: math.MaxInt32} |
| 237 | + err = wait.ExponentialBackoffWithContext(ctx, backoff, func(context.Context) (bool, error) { |
| 238 | + err2 := o.destroyServiceInstance(item) |
| 239 | + if err2 == nil { |
| 240 | + return true, err2 |
| 241 | + } |
| 242 | + o.errorTracker.suppressWarning(item.key, err2, o.Logger) |
| 243 | + return false, err2 |
| 244 | + }) |
| 245 | + if err != nil { |
| 246 | + o.Logger.Fatal("destroyServiceInstances: ExponentialBackoffWithContext (destroy) returns ", err) |
| 247 | + } |
| 248 | + } |
| 249 | + |
| 250 | + if items = o.getPendingItems(serviceInstanceTypeName); len(items) > 0 { |
| 251 | + for _, item := range items { |
| 252 | + o.Logger.Debugf("destroyServiceInstances: found %s in pending items", item.name) |
| 253 | + } |
| 254 | + return errors.Errorf("destroyServiceInstances: %d undeleted items pending", len(items)) |
| 255 | + } |
| 256 | + |
| 257 | + backoff := wait.Backoff{ |
| 258 | + Duration: 15 * time.Second, |
| 259 | + Factor: 1.1, |
| 260 | + Cap: leftInContext(ctx), |
| 261 | + Steps: math.MaxInt32} |
| 262 | + err = wait.ExponentialBackoffWithContext(ctx, backoff, func(context.Context) (bool, error) { |
| 263 | + secondPassList, err2 := o.listServiceInstances() |
| 264 | + if err2 != nil { |
| 265 | + return false, err2 |
| 266 | + } |
| 267 | + if len(secondPassList) == 0 { |
| 268 | + // We finally don't see any remaining instances! |
| 269 | + return true, nil |
| 270 | + } |
| 271 | + for _, item := range secondPassList { |
| 272 | + o.Logger.Debugf("destroyServiceInstances: found %s in second pass", item.name) |
| 273 | + } |
| 274 | + return false, nil |
| 275 | + }) |
| 276 | + if err != nil { |
| 277 | + o.Logger.Fatal("destroyServiceInstances: ExponentialBackoffWithContext (list) returns ", err) |
| 278 | + } |
| 279 | + |
| 280 | + return nil |
| 281 | +} |
0 commit comments