diff --git a/internal/ibmcloud-janitor/resources/types.go b/internal/ibmcloud-janitor/resources/types.go index 8fe8e82f..bdc28ffa 100644 --- a/internal/ibmcloud-janitor/resources/types.go +++ b/internal/ibmcloud-janitor/resources/types.go @@ -45,6 +45,7 @@ var PowervsResources = []Resource{ var VpcResources = []Resource{ VPCInstance{}, + VPCLoadBalancer{}, VPCNetwork{}, VPCs{}, } @@ -74,6 +75,9 @@ type VPC interface { GetSubnetPublicGateway(options *vpcv1.GetSubnetPublicGatewayOptions) (*vpcv1.PublicGateway, *core.DetailedResponse, error) DeletePublicGateway(options *vpcv1.DeletePublicGatewayOptions) (*core.DetailedResponse, error) UnsetSubnetPublicGateway(options *vpcv1.UnsetSubnetPublicGatewayOptions) (*core.DetailedResponse, error) + DeleteLoadBalancer(options *vpcv1.DeleteLoadBalancerOptions) (*core.DetailedResponse, error) + ListLoadBalancers(options *vpcv1.ListLoadBalancersOptions) (*vpcv1.LoadBalancerCollection, *core.DetailedResponse, error) + GetLoadBalancer(options *vpcv1.GetLoadBalancerOptions) (result *vpcv1.LoadBalancer, response *core.DetailedResponse, err error) } type ServiceIDClient interface { diff --git a/internal/ibmcloud-janitor/resources/util.go b/internal/ibmcloud-janitor/resources/util.go new file mode 100644 index 00000000..da58f4e0 --- /dev/null +++ b/internal/ibmcloud-janitor/resources/util.go @@ -0,0 +1,67 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "net/url" + + "github.com/pkg/errors" +) + +// pagingHelper is used while listing resources. It parses and fetches the start token for +// getting the next set of resources if nextURL is returned by func f. +// func f has start as a parameter and returns the following: +// isDone bool - true denotes that iterating is not needed as there is no next set of resources +// nextURL string - denotes a URL for a page of resources which is parsed for fetching start token for next iteration +// e error - will break and return error if e is not nil +func pagingHelper(f func(string) (bool, string, error)) (err error) { + start := "" + + getStartToken := func(nextURL string) (string, error) { + url, err := url.Parse(nextURL) + if err != nil || url == nil { + return "", errors.Wrapf(err, "failed to parse next url for getting next resources") + } + + start := url.Query().Get("start") + return start, nil + } + + for { + isDone, nextURL, e := f(start) + + if e != nil { + err = e + break + } + + if isDone { + break + } + + if nextURL != "" { + start, err = getStartToken(nextURL) + if err != nil { + break + } + } else { + break + } + } + + return +} diff --git a/internal/ibmcloud-janitor/resources/vpc_client.go b/internal/ibmcloud-janitor/resources/vpc_client.go index 30e446dd..038af521 100644 --- a/internal/ibmcloud-janitor/resources/vpc_client.go +++ b/internal/ibmcloud-janitor/resources/vpc_client.go @@ -78,6 +78,18 @@ func (c *IBMVPCClient) UnsetSubnetPublicGateway(options *vpcv1.UnsetSubnetPublic return c.vpcService.UnsetSubnetPublicGateway(options) } +func (c *IBMVPCClient) DeleteLoadBalancer(options *vpcv1.DeleteLoadBalancerOptions) (*core.DetailedResponse, error) { + return c.vpcService.DeleteLoadBalancer(options) +} + +func (c *IBMVPCClient) ListLoadBalancers(options *vpcv1.ListLoadBalancersOptions) (*vpcv1.LoadBalancerCollection, *core.DetailedResponse, error) { + return c.vpcService.ListLoadBalancers(options) +} + +func (c *IBMVPCClient) GetLoadBalancer(options *vpcv1.GetLoadBalancerOptions) (result *vpcv1.LoadBalancer, response *core.DetailedResponse, err error) { + return c.vpcService.GetLoadBalancer(options) +} + // Creates a new VPC Client func NewVPCClient(options *CleanupOptions) (*IBMVPCClient, error) { client := &IBMVPCClient{} diff --git a/internal/ibmcloud-janitor/resources/vpc_lbs.go b/internal/ibmcloud-janitor/resources/vpc_lbs.go new file mode 100644 index 00000000..10dd996e --- /dev/null +++ b/internal/ibmcloud-janitor/resources/vpc_lbs.go @@ -0,0 +1,127 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "strings" + "time" + + "github.com/IBM/vpc-go-sdk/vpcv1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/wait" +) + +type VPCLoadBalancer struct{} + +var ( + lbDeletionTimeout = time.Minute * 4 + lbPollingInterval = time.Second * 30 + resourceLogger *logrus.Entry +) + +// Cleans up the load balancers in a given region +func (VPCLoadBalancer) cleanup(options *CleanupOptions) error { + resourceLogger = logrus.WithFields(logrus.Fields{"resource": options.Resource.Name}) + resourceLogger.Info("Cleaning up the load balancers") + client, err := NewVPCClient(options) + if err != nil { + return errors.Wrap(err, "couldn't create VPC client") + } + + var deletedLBList []string + var errs []error + + f := func(start string) (bool, string, error) { + listLbOpts := &vpcv1.ListLoadBalancersOptions{} + if start != "" { + listLbOpts.Start = &start + } + + loadBalancers, _, err := client.ListLoadBalancers(listLbOpts) + if err != nil { + return false, "", errors.Wrap(err, "failed to list the load balancers") + } + + if loadBalancers == nil || len(loadBalancers.LoadBalancers) <= 0 { + resourceLogger.Info("there are no available load balancers to delete") + return true, "", nil + } + + for _, lb := range loadBalancers.LoadBalancers { + if *lb.ResourceGroup.ID == client.ResourceGroupID { + if _, err := client.DeleteLoadBalancer(&vpcv1.DeleteLoadBalancerOptions{ + ID: lb.ID, + }); err != nil { + resourceLogger.WithField("name", *lb.Name).Error("failed to delete load balancer") + errs = append(errs, err) + continue + } + deletedLBList = append(deletedLBList, *lb.ID) + resourceLogger.WithField("name", *lb.Name).Info("load balancer deletetion triggered") + } + } + + if loadBalancers.Next != nil && *loadBalancers.Next.Href != "" { + return false, *loadBalancers.Next.Href, nil + } + + if len(errs) > 0 { + return false, "", kerrors.NewAggregate(errs) + } + + return true, "", nil + } + + if err = pagingHelper(f); err != nil { + return errors.Wrapf(err, "failed to clean up the load balancers") + } + + // check if the LBs are properly deleted + if err := checkLBs(deletedLBList, client); err != nil { + return errors.Wrapf(err, "failed to verify the deletion of load balancers") + } + + resourceLogger.Info("Successfully deleted the load balancers") + return nil +} + +func checkLBs(list []string, client *IBMVPCClient) error { + var errs []error + check := func(id string) error { + return wait.PollImmediate(lbPollingInterval, lbDeletionTimeout, func() (bool, error) { + _, _, err := client.GetLoadBalancer(&vpcv1.GetLoadBalancerOptions{ID: &id}) + if err != nil && strings.Contains(err.Error(), "cannot be found") { + return true, nil + } + return false, err + }) + } + + for _, lb := range list { + if err := check(lb); err != nil { + resourceLogger.WithField("ID", lb).Error("failed to check the deletion of load balancer") + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return kerrors.NewAggregate(errs) + } + return nil +}