diff --git a/.golangci.yml b/.golangci.yml index d1cfa3260a..9ffdd93845 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -85,6 +85,8 @@ linters-settings: alias: infrav1alpha4 - pkg: sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1 alias: infrav1 + - pkg: sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors + alias: capoerrors # CAPI - pkg: sigs.k8s.io/cluster-api/api/v1alpha3 alias: clusterv1alpha3 diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index db48e67ed1..45c9bee8c4 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -2,7 +2,7 @@ // +build !ignore_autogenerated /* -Copyright 2021 The Kubernetes Authors. +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. diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go index 6d5e70e26b..8fed804a6a 100644 --- a/api/v1alpha3/zz_generated.deepcopy.go +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -2,7 +2,7 @@ // +build !ignore_autogenerated /* -Copyright 2021 The Kubernetes Authors. +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. diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index c7c9ac9bec..83cfc3c9c3 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -2,7 +2,7 @@ // +build !ignore_autogenerated /* -Copyright 2021 The Kubernetes Authors. +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. diff --git a/api/v1alpha4/zz_generated.deepcopy.go b/api/v1alpha4/zz_generated.deepcopy.go index e0a16bd742..a5b7798c9a 100644 --- a/api/v1alpha4/zz_generated.deepcopy.go +++ b/api/v1alpha4/zz_generated.deepcopy.go @@ -2,7 +2,7 @@ // +build !ignore_autogenerated /* -Copyright 2021 The Kubernetes Authors. +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. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 8f8f39dce4..d2c9de389f 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -2,7 +2,7 @@ // +build !ignore_autogenerated /* -Copyright 2021 The Kubernetes Authors. +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. diff --git a/hack/boilerplate/boilerplate.generatego.txt b/hack/boilerplate/boilerplate.generatego.txt index daba3a171a..ccf5db1192 100644 --- a/hack/boilerplate/boilerplate.generatego.txt +++ b/hack/boilerplate/boilerplate.generatego.txt @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +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. diff --git a/pkg/cloud/services/compute/client_mock.go b/pkg/cloud/services/compute/client_mock.go index 8290db16ec..c281802de4 100644 --- a/pkg/cloud/services/compute/client_mock.go +++ b/pkg/cloud/services/compute/client_mock.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +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. diff --git a/pkg/cloud/services/loadbalancer/client.go b/pkg/cloud/services/loadbalancer/client.go new file mode 100644 index 0000000000..1ce285ec02 --- /dev/null +++ b/pkg/cloud/services/loadbalancer/client.go @@ -0,0 +1,231 @@ +/* +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 loadbalancer + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/providers" + + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" + capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors" +) + +type LbClient interface { + CreateLoadBalancer(opts loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error) + ListLoadBalancers(opts loadbalancers.ListOptsBuilder) ([]loadbalancers.LoadBalancer, error) + GetLoadBalancer(id string) (*loadbalancers.LoadBalancer, error) + DeleteLoadBalancer(id string, opts loadbalancers.DeleteOptsBuilder) error + CreateListener(opts listeners.CreateOptsBuilder) (*listeners.Listener, error) + ListListeners(opts listeners.ListOptsBuilder) ([]listeners.Listener, error) + GetListener(id string) (*listeners.Listener, error) + DeleteListener(id string) error + CreatePool(opts pools.CreateOptsBuilder) (*pools.Pool, error) + ListPools(opts pools.ListOptsBuilder) ([]pools.Pool, error) + GetPool(id string) (*pools.Pool, error) + DeletePool(id string) error + CreatePoolMember(poolID string, opts pools.CreateMemberOptsBuilder) (*pools.Member, error) + ListPoolMember(poolID string, opts pools.ListMembersOptsBuilder) ([]pools.Member, error) + DeletePoolMember(poolID string, lbMemberID string) error + CreateMonitor(opts monitors.CreateOptsBuilder) (*monitors.Monitor, error) + ListMonitors(opts monitors.ListOptsBuilder) ([]monitors.Monitor, error) + DeleteMonitor(id string) error + ListLoadBalancerProviders() ([]providers.Provider, error) +} + +type lbClient struct { + serviceClient *gophercloud.ServiceClient +} + +func (l lbClient) CreateLoadBalancer(opts loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer", "create") + lb, err := loadbalancers.Create(l.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return lb, nil +} + +func (l lbClient) ListLoadBalancers(opts loadbalancers.ListOptsBuilder) ([]loadbalancers.LoadBalancer, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer", "list") + allPages, err := loadbalancers.List(l.serviceClient, opts).AllPages() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return loadbalancers.ExtractLoadBalancers(allPages) +} + +func (l lbClient) GetLoadBalancer(id string) (*loadbalancers.LoadBalancer, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer", "get") + lb, err := loadbalancers.Get(l.serviceClient, id).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return lb, nil +} + +func (l lbClient) DeleteLoadBalancer(id string, opts loadbalancers.DeleteOptsBuilder) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer", "delete") + err := loadbalancers.Delete(l.serviceClient, id, opts).ExtractErr() + if mc.ObserveRequestIgnoreNotFound(err) != nil && !capoerrors.IsNotFound(err) { + return err + } + return nil +} + +func (l lbClient) CreateListener(opts listeners.CreateOptsBuilder) (*listeners.Listener, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "create") + listener, err := listeners.Create(l.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return listener, nil +} + +func (l lbClient) ListListeners(opts listeners.ListOptsBuilder) ([]listeners.Listener, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "list") + allPages, err := listeners.List(l.serviceClient, opts).AllPages() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return listeners.ExtractListeners(allPages) +} + +func (l lbClient) GetListener(id string) (*listeners.Listener, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "get") + listener, err := listeners.Get(l.serviceClient, id).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return listener, nil +} + +func (l lbClient) DeleteListener(id string) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "delete") + err := listeners.Delete(l.serviceClient, id).ExtractErr() + if mc.ObserveRequestIgnoreNotFound(err) != nil && !capoerrors.IsNotFound(err) { + return fmt.Errorf("error deleting lbaas listener %s: %v", id, err) + } + return nil +} + +func (l lbClient) CreatePool(opts pools.CreateOptsBuilder) (*pools.Pool, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "create") + pool, err := pools.Create(l.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return pool, nil +} + +func (l lbClient) ListPools(opts pools.ListOptsBuilder) ([]pools.Pool, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "list") + allPages, err := pools.List(l.serviceClient, opts).AllPages() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return pools.ExtractPools(allPages) +} + +func (l lbClient) GetPool(id string) (*pools.Pool, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "get") + pool, err := pools.Get(l.serviceClient, id).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return pool, nil +} + +func (l lbClient) DeletePool(id string) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "delete") + err := pools.Delete(l.serviceClient, id).ExtractErr() + if mc.ObserveRequestIgnoreNotFound(err) != nil && !capoerrors.IsNotFound(err) { + return fmt.Errorf("error deleting lbaas pool %s: %v", id, err) + } + return nil +} + +func (l lbClient) CreatePoolMember(poolID string, lbMemberOpts pools.CreateMemberOptsBuilder) (*pools.Member, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "create") + member, err := pools.CreateMember(l.serviceClient, poolID, lbMemberOpts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, fmt.Errorf("error create lbmember: %s", err) + } + return member, nil +} + +func (l lbClient) ListPoolMember(poolID string, opts pools.ListMembersOptsBuilder) ([]pools.Member, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "list") + allPages, err := pools.ListMembers(l.serviceClient, poolID, opts).AllPages() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return pools.ExtractMembers(allPages) +} + +func (l lbClient) DeletePoolMember(poolID string, lbMemberID string) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "delete") + err := pools.DeleteMember(l.serviceClient, poolID, lbMemberID).ExtractErr() + if mc.ObserveRequest(err) != nil { + return fmt.Errorf("error deleting lbmember: %s", err) + } + return nil +} + +func (l lbClient) CreateMonitor(opts monitors.CreateOptsBuilder) (*monitors.Monitor, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_healthmonitor", "create") + monitor, err := monitors.Create(l.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return monitor, nil +} + +func (l lbClient) ListMonitors(opts monitors.ListOptsBuilder) ([]monitors.Monitor, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_healthmonitor", "list") + allPages, err := monitors.List(l.serviceClient, opts).AllPages() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return monitors.ExtractMonitors(allPages) +} + +func (l lbClient) DeleteMonitor(id string) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer_healthmonitor", "delete") + err := monitors.Delete(l.serviceClient, id).ExtractErr() + if mc.ObserveRequestIgnoreNotFound(err) != nil && !capoerrors.IsNotFound(err) { + return fmt.Errorf("error deleting lbaas monitor %s: %v", id, err) + } + return nil +} + +func (l lbClient) ListLoadBalancerProviders() ([]providers.Provider, error) { + allPages, err := providers.List(l.serviceClient, providers.ListOpts{}).AllPages() + if err != nil { + return nil, fmt.Errorf("listing providers: %v", err) + } + providersList, err := providers.ExtractProviders(allPages) + if err != nil { + return nil, fmt.Errorf("extracting loadbalancer providers pages: %v", err) + } + return providersList, nil +} diff --git a/pkg/cloud/services/loadbalancer/loadbalancer.go b/pkg/cloud/services/loadbalancer/loadbalancer.go index f50821de5b..0cef0cf2e3 100644 --- a/pkg/cloud/services/loadbalancer/loadbalancer.go +++ b/pkg/cloud/services/loadbalancer/loadbalancer.go @@ -30,8 +30,8 @@ import ( "sigs.k8s.io/cluster-api/util" infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" - "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" "sigs.k8s.io/cluster-api-provider-openstack/pkg/record" + capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors" "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/names" ) @@ -127,9 +127,8 @@ func (s *Service) getOrCreateLoadBalancer(openStackCluster *infrav1.OpenStackClu VipAddress: vipAddress, Description: names.GetDescription(clusterName), } - mc := metrics.NewMetricPrometheusContext("loadbalancer", "create") - lb, err = loadbalancers.Create(s.loadbalancerClient, lbCreateOpts).Extract() - if mc.ObserveRequest(err) != nil { + lb, err = s.loadbalancerClient.CreateLoadBalancer(lbCreateOpts) + if err != nil { record.Warnf(openStackCluster, "FailedCreateLoadBalancer", "Failed to create load balancer %s: %v", loadBalancerName, err) return nil, err } @@ -161,9 +160,8 @@ func (s *Service) getOrCreateListener(openStackCluster *infrav1.OpenStackCluster ProtocolPort: port, LoadbalancerID: lbID, } - mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "create") - listener, err = listeners.Create(s.loadbalancerClient, listenerCreateOpts).Extract() - if mc.ObserveRequest(err) != nil { + listener, err = s.loadbalancerClient.CreateListener(listenerCreateOpts) + if err != nil { record.Warnf(openStackCluster, "FailedCreateListener", "Failed to create listener %s: %v", listenerName, err) return nil, err } @@ -200,9 +198,8 @@ func (s *Service) getOrCreatePool(openStackCluster *infrav1.OpenStackCluster, po LBMethod: pools.LBMethodRoundRobin, ListenerID: listenerID, } - mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "create") - pool, err = pools.Create(s.loadbalancerClient, poolCreateOpts).Extract() - if mc.ObserveRequest(err) != nil { + pool, err = s.loadbalancerClient.CreatePool(poolCreateOpts) + if err != nil { record.Warnf(openStackCluster, "FailedCreatePool", "Failed to create pool %s: %v", poolName, err) return nil, err } @@ -236,9 +233,8 @@ func (s *Service) getOrCreateMonitor(openStackCluster *infrav1.OpenStackCluster, Timeout: 5, MaxRetries: 3, } - mc := metrics.NewMetricPrometheusContext("loadbalancer_healthmonitor", "create") - monitor, err = monitors.Create(s.loadbalancerClient, monitorCreateOpts).Extract() - if mc.ObserveRequest(err) != nil { + monitor, err = s.loadbalancerClient.CreateMonitor(monitorCreateOpts) + if err != nil { record.Warnf(openStackCluster, "FailedCreateMonitor", "Failed to create monitor %s: %v", monitorName, err) return err } @@ -304,10 +300,8 @@ func (s *Service) ReconcileLoadBalancerMember(openStackCluster *infrav1.OpenStac if err != nil { return err } - mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "delete") - err = pools.DeleteMember(s.loadbalancerClient, pool.ID, lbMember.ID).ExtractErr() - if mc.ObserveRequest(err) != nil { - return fmt.Errorf("error deleting lbmember: %s", err) + if err := s.loadbalancerClient.DeletePoolMember(pool.ID, lbMember.ID); err != nil { + return err } err = s.waitForLoadBalancerActive(lbID) if err != nil { @@ -327,11 +321,11 @@ func (s *Service) ReconcileLoadBalancerMember(openStackCluster *infrav1.OpenStac if err := s.waitForLoadBalancerActive(lbID); err != nil { return err } - mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "create") - _, err = pools.CreateMember(s.loadbalancerClient, pool.ID, lbMemberOpts).Extract() - if mc.ObserveRequest(err) != nil { - return fmt.Errorf("error create lbmember: %s", err) + + if _, err := s.loadbalancerClient.CreatePoolMember(pool.ID, lbMemberOpts); err != nil { + return err } + if err := s.waitForLoadBalancerActive(lbID); err != nil { return err } @@ -370,9 +364,8 @@ func (s *Service) DeleteLoadBalancer(openStackCluster *infrav1.OpenStackCluster, Cascade: true, } s.logger.Info("Deleting load balancer", "name", loadBalancerName, "cascade", deleteOpts.Cascade) - mc := metrics.NewMetricPrometheusContext("loadbalancer", "delete") - err = loadbalancers.Delete(s.loadbalancerClient, lb.ID, deleteOpts).ExtractErr() - if mc.ObserveRequest(err) != nil { + err = s.loadbalancerClient.DeleteLoadBalancer(lb.ID, deleteOpts) + if err != nil && !capoerrors.IsNotFound(err) { record.Warnf(openStackCluster, "FailedDeleteLoadBalancer", "Failed to delete load balancer %s with id %s: %v", lb.Name, lb.ID, err) return err } @@ -424,10 +417,8 @@ func (s *Service) DeleteLoadBalancerMember(openStackCluster *infrav1.OpenStackCl if err != nil { return err } - mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "delete") - err = pools.DeleteMember(s.loadbalancerClient, pool.ID, lbMember.ID).ExtractErr() - if mc.ObserveRequest(err) != nil { - return fmt.Errorf("error deleting load balancer member: %s", err) + if err := s.loadbalancerClient.DeletePoolMember(pool.ID, lbMember.ID); err != nil { + return err } err = s.waitForLoadBalancerActive(lbID) if err != nil { @@ -443,12 +434,7 @@ func getLoadBalancerName(clusterName string) string { } func (s *Service) checkIfLbExists(name string) (*loadbalancers.LoadBalancer, error) { - mc := metrics.NewMetricPrometheusContext("loadbalancer", "list") - allPages, err := loadbalancers.List(s.loadbalancerClient, loadbalancers.ListOpts{Name: name}).AllPages() - if mc.ObserveRequest(err) != nil { - return nil, err - } - lbList, err := loadbalancers.ExtractLoadBalancers(allPages) + lbList, err := s.loadbalancerClient.ListLoadBalancers(loadbalancers.ListOpts{Name: name}) if err != nil { return nil, err } @@ -459,12 +445,7 @@ func (s *Service) checkIfLbExists(name string) (*loadbalancers.LoadBalancer, err } func (s *Service) checkIfListenerExists(name string) (*listeners.Listener, error) { - mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "list") - allPages, err := listeners.List(s.loadbalancerClient, listeners.ListOpts{Name: name}).AllPages() - if mc.ObserveRequest(err) != nil { - return nil, err - } - listenerList, err := listeners.ExtractListeners(allPages) + listenerList, err := s.loadbalancerClient.ListListeners(listeners.ListOpts{Name: name}) if err != nil { return nil, err } @@ -475,12 +456,7 @@ func (s *Service) checkIfListenerExists(name string) (*listeners.Listener, error } func (s *Service) checkIfPoolExists(name string) (*pools.Pool, error) { - mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "list") - allPages, err := pools.List(s.loadbalancerClient, pools.ListOpts{Name: name}).AllPages() - if mc.ObserveRequest(err) != nil { - return nil, err - } - poolList, err := pools.ExtractPools(allPages) + poolList, err := s.loadbalancerClient.ListPools(pools.ListOpts{Name: name}) if err != nil { return nil, err } @@ -491,12 +467,7 @@ func (s *Service) checkIfPoolExists(name string) (*pools.Pool, error) { } func (s *Service) checkIfMonitorExists(name string) (*monitors.Monitor, error) { - mc := metrics.NewMetricPrometheusContext("loadbalancer_healthmonitor", "list") - allPages, err := monitors.List(s.loadbalancerClient, monitors.ListOpts{Name: name}).AllPages() - if mc.ObserveRequest(err) != nil { - return nil, err - } - monitorList, err := monitors.ExtractMonitors(allPages) + monitorList, err := s.loadbalancerClient.ListMonitors(monitors.ListOpts{Name: name}) if err != nil { return nil, err } @@ -507,12 +478,7 @@ func (s *Service) checkIfMonitorExists(name string) (*monitors.Monitor, error) { } func (s *Service) checkIfLbMemberExists(poolID, name string) (*pools.Member, error) { - mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "list") - allPages, err := pools.ListMembers(s.loadbalancerClient, poolID, pools.ListMembersOpts{Name: name}).AllPages() - if mc.ObserveRequest(err) != nil { - return nil, err - } - lbMemberList, err := pools.ExtractMembers(allPages) + lbMemberList, err := s.loadbalancerClient.ListPoolMember(poolID, pools.ListMembersOpts{Name: name}) if err != nil { return nil, err } @@ -533,9 +499,8 @@ var backoff = wait.Backoff{ func (s *Service) waitForLoadBalancerActive(id string) error { s.logger.Info("Waiting for load balancer", "id", id, "targetStatus", "ACTIVE") return wait.ExponentialBackoff(backoff, func() (bool, error) { - mc := metrics.NewMetricPrometheusContext("loadbalancer", "get") - lb, err := loadbalancers.Get(s.loadbalancerClient, id).Extract() - if mc.ObserveRequest(err) != nil { + lb, err := s.loadbalancerClient.GetLoadBalancer(id) + if err != nil { return false, err } return lb.ProvisioningStatus == loadBalancerProvisioningStatusActive, nil @@ -545,9 +510,8 @@ func (s *Service) waitForLoadBalancerActive(id string) error { func (s *Service) waitForListener(id, target string) error { s.logger.Info("Waiting for load balancer listener", "id", id, "targetStatus", target) return wait.ExponentialBackoff(backoff, func() (bool, error) { - mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "get") - _, err := listeners.Get(s.loadbalancerClient, id).Extract() - if mc.ObserveRequest(err) != nil { + _, err := s.loadbalancerClient.GetListener(id) + if err != nil { return false, err } // The listener resource has no Status attribute, so a successful Get is the best we can do diff --git a/pkg/cloud/services/loadbalancer/loadbalancer_test.go b/pkg/cloud/services/loadbalancer/loadbalancer_test.go new file mode 100644 index 0000000000..5aeebefc2c --- /dev/null +++ b/pkg/cloud/services/loadbalancer/loadbalancer_test.go @@ -0,0 +1,96 @@ +/* +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 loadbalancer + +import ( + "fmt" + "testing" + + "github.com/go-logr/logr" + "github.com/golang/mock/gomock" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers" + . "github.com/onsi/gomega" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/loadbalancer/mock_loadbalancer" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/networking" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/networking/mock_networking" +) + +func Test_ReconcileLoadBalancer(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + openStackCluster := &infrav1.OpenStackCluster{Status: infrav1.OpenStackClusterStatus{ + ExternalNetwork: &infrav1.Network{ + ID: "aaaaaaaa-bbbb-cccc-dddd-111111111111", + }, + Network: &infrav1.Network{ + Subnet: &infrav1.Subnet{ + ID: "aaaaaaaa-bbbb-cccc-dddd-222222222222", + }, + }, + }} + type serviceFields struct { + projectID string + networkingClient *mock_networking.MockNetworkClient + loadbalancerClient *mock_loadbalancer.MockLbClient + } + lbtests := []struct { + name string + fields serviceFields + prepareServiceMock func(sf *serviceFields) + expectNetwork func(m *mock_networking.MockNetworkClientMockRecorder) + expectLoadBalancer func(m *mock_loadbalancer.MockLbClientMockRecorder) + wantError error + }{ + { + name: "reconcile loadbalancer in non active state should should cause error", + prepareServiceMock: func(sf *serviceFields) { + sf.networkingClient = mock_networking.NewMockNetworkClient(mockCtrl) + sf.loadbalancerClient = mock_loadbalancer.NewMockLbClient(mockCtrl) + }, + expectNetwork: func(m *mock_networking.MockNetworkClientMockRecorder) { + // add network api call results here + }, + expectLoadBalancer: func(m *mock_loadbalancer.MockLbClientMockRecorder) { + // return existing loadbalancer in non-active state + lbList := []loadbalancers.LoadBalancer{ + { + ID: "aaaaaaaa-bbbb-cccc-dddd-333333333333", + Name: "k8s-clusterapi-cluster-AAAAA-kubeapi", + ProvisioningStatus: "PENDING_CREATE", + }, + } + m.ListLoadBalancers(loadbalancers.ListOpts{Name: "k8s-clusterapi-cluster-AAAAA-kubeapi"}).Return(lbList, nil) + }, + wantError: fmt.Errorf("load balancer %q is not in expected state %s, current state is %s", "aaaaaaaa-bbbb-cccc-dddd-333333333333", "ACTIVE", "PENDING_CREATE"), + }, + } + for _, tt := range lbtests { + t.Run(tt.name, func(t *testing.T) { + tt.prepareServiceMock(&tt.fields) + networkingService := networking.NewTestService(tt.fields.projectID, tt.fields.networkingClient, logr.Discard()) + lbs := NewLoadBalancerTestService(tt.fields.projectID, tt.fields.loadbalancerClient, networkingService, logr.Discard()) + g := NewWithT(t) + tt.expectNetwork(tt.fields.networkingClient.EXPECT()) + tt.expectLoadBalancer(tt.fields.loadbalancerClient.EXPECT()) + err := lbs.ReconcileLoadBalancer(openStackCluster, "AAAAA", 0) + g.Expect(err).To(MatchError(tt.wantError)) + }) + } +} diff --git a/pkg/cloud/services/loadbalancer/mock_loadbalancer/loadbalancer_service_mock.go b/pkg/cloud/services/loadbalancer/mock_loadbalancer/loadbalancer_service_mock.go new file mode 100644 index 0000000000..a42977549e --- /dev/null +++ b/pkg/cloud/services/loadbalancer/mock_loadbalancer/loadbalancer_service_mock.go @@ -0,0 +1,335 @@ +/* +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. +*/ + +// Code generated by MockGen. DO NOT EDIT. +// Source: sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/loadbalancer (interfaces: LbClient) + +// Package mock_loadbalancer is a generated GoMock package. +package mock_loadbalancer + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + listeners "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" + loadbalancers "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers" + monitors "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors" + pools "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + providers "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/providers" +) + +// MockLbClient is a mock of LbClient interface. +type MockLbClient struct { + ctrl *gomock.Controller + recorder *MockLbClientMockRecorder +} + +// MockLbClientMockRecorder is the mock recorder for MockLbClient. +type MockLbClientMockRecorder struct { + mock *MockLbClient +} + +// NewMockLbClient creates a new mock instance. +func NewMockLbClient(ctrl *gomock.Controller) *MockLbClient { + mock := &MockLbClient{ctrl: ctrl} + mock.recorder = &MockLbClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLbClient) EXPECT() *MockLbClientMockRecorder { + return m.recorder +} + +// CreateListener mocks base method. +func (m *MockLbClient) CreateListener(arg0 listeners.CreateOptsBuilder) (*listeners.Listener, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateListener", arg0) + ret0, _ := ret[0].(*listeners.Listener) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateListener indicates an expected call of CreateListener. +func (mr *MockLbClientMockRecorder) CreateListener(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateListener", reflect.TypeOf((*MockLbClient)(nil).CreateListener), arg0) +} + +// CreateLoadBalancer mocks base method. +func (m *MockLbClient) CreateLoadBalancer(arg0 loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateLoadBalancer", arg0) + ret0, _ := ret[0].(*loadbalancers.LoadBalancer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateLoadBalancer indicates an expected call of CreateLoadBalancer. +func (mr *MockLbClientMockRecorder) CreateLoadBalancer(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLoadBalancer", reflect.TypeOf((*MockLbClient)(nil).CreateLoadBalancer), arg0) +} + +// CreateMonitor mocks base method. +func (m *MockLbClient) CreateMonitor(arg0 monitors.CreateOptsBuilder) (*monitors.Monitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateMonitor", arg0) + ret0, _ := ret[0].(*monitors.Monitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateMonitor indicates an expected call of CreateMonitor. +func (mr *MockLbClientMockRecorder) CreateMonitor(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMonitor", reflect.TypeOf((*MockLbClient)(nil).CreateMonitor), arg0) +} + +// CreatePool mocks base method. +func (m *MockLbClient) CreatePool(arg0 pools.CreateOptsBuilder) (*pools.Pool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePool", arg0) + ret0, _ := ret[0].(*pools.Pool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePool indicates an expected call of CreatePool. +func (mr *MockLbClientMockRecorder) CreatePool(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePool", reflect.TypeOf((*MockLbClient)(nil).CreatePool), arg0) +} + +// CreatePoolMember mocks base method. +func (m *MockLbClient) CreatePoolMember(arg0 string, arg1 pools.CreateMemberOptsBuilder) (*pools.Member, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePoolMember", arg0, arg1) + ret0, _ := ret[0].(*pools.Member) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePoolMember indicates an expected call of CreatePoolMember. +func (mr *MockLbClientMockRecorder) CreatePoolMember(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePoolMember", reflect.TypeOf((*MockLbClient)(nil).CreatePoolMember), arg0, arg1) +} + +// DeleteListener mocks base method. +func (m *MockLbClient) DeleteListener(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteListener", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteListener indicates an expected call of DeleteListener. +func (mr *MockLbClientMockRecorder) DeleteListener(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteListener", reflect.TypeOf((*MockLbClient)(nil).DeleteListener), arg0) +} + +// DeleteLoadBalancer mocks base method. +func (m *MockLbClient) DeleteLoadBalancer(arg0 string, arg1 loadbalancers.DeleteOptsBuilder) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteLoadBalancer", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteLoadBalancer indicates an expected call of DeleteLoadBalancer. +func (mr *MockLbClientMockRecorder) DeleteLoadBalancer(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLoadBalancer", reflect.TypeOf((*MockLbClient)(nil).DeleteLoadBalancer), arg0, arg1) +} + +// DeleteMonitor mocks base method. +func (m *MockLbClient) DeleteMonitor(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteMonitor", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteMonitor indicates an expected call of DeleteMonitor. +func (mr *MockLbClientMockRecorder) DeleteMonitor(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMonitor", reflect.TypeOf((*MockLbClient)(nil).DeleteMonitor), arg0) +} + +// DeletePool mocks base method. +func (m *MockLbClient) DeletePool(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePool", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePool indicates an expected call of DeletePool. +func (mr *MockLbClientMockRecorder) DeletePool(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePool", reflect.TypeOf((*MockLbClient)(nil).DeletePool), arg0) +} + +// DeletePoolMember mocks base method. +func (m *MockLbClient) DeletePoolMember(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePoolMember", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePoolMember indicates an expected call of DeletePoolMember. +func (mr *MockLbClientMockRecorder) DeletePoolMember(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePoolMember", reflect.TypeOf((*MockLbClient)(nil).DeletePoolMember), arg0, arg1) +} + +// GetListener mocks base method. +func (m *MockLbClient) GetListener(arg0 string) (*listeners.Listener, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetListener", arg0) + ret0, _ := ret[0].(*listeners.Listener) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetListener indicates an expected call of GetListener. +func (mr *MockLbClientMockRecorder) GetListener(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListener", reflect.TypeOf((*MockLbClient)(nil).GetListener), arg0) +} + +// GetLoadBalancer mocks base method. +func (m *MockLbClient) GetLoadBalancer(arg0 string) (*loadbalancers.LoadBalancer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLoadBalancer", arg0) + ret0, _ := ret[0].(*loadbalancers.LoadBalancer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLoadBalancer indicates an expected call of GetLoadBalancer. +func (mr *MockLbClientMockRecorder) GetLoadBalancer(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoadBalancer", reflect.TypeOf((*MockLbClient)(nil).GetLoadBalancer), arg0) +} + +// GetPool mocks base method. +func (m *MockLbClient) GetPool(arg0 string) (*pools.Pool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPool", arg0) + ret0, _ := ret[0].(*pools.Pool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPool indicates an expected call of GetPool. +func (mr *MockLbClientMockRecorder) GetPool(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPool", reflect.TypeOf((*MockLbClient)(nil).GetPool), arg0) +} + +// ListListeners mocks base method. +func (m *MockLbClient) ListListeners(arg0 listeners.ListOptsBuilder) ([]listeners.Listener, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListListeners", arg0) + ret0, _ := ret[0].([]listeners.Listener) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListListeners indicates an expected call of ListListeners. +func (mr *MockLbClientMockRecorder) ListListeners(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListListeners", reflect.TypeOf((*MockLbClient)(nil).ListListeners), arg0) +} + +// ListLoadBalancerProviders mocks base method. +func (m *MockLbClient) ListLoadBalancerProviders() ([]providers.Provider, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListLoadBalancerProviders") + ret0, _ := ret[0].([]providers.Provider) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListLoadBalancerProviders indicates an expected call of ListLoadBalancerProviders. +func (mr *MockLbClientMockRecorder) ListLoadBalancerProviders() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLoadBalancerProviders", reflect.TypeOf((*MockLbClient)(nil).ListLoadBalancerProviders)) +} + +// ListLoadBalancers mocks base method. +func (m *MockLbClient) ListLoadBalancers(arg0 loadbalancers.ListOptsBuilder) ([]loadbalancers.LoadBalancer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListLoadBalancers", arg0) + ret0, _ := ret[0].([]loadbalancers.LoadBalancer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListLoadBalancers indicates an expected call of ListLoadBalancers. +func (mr *MockLbClientMockRecorder) ListLoadBalancers(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLoadBalancers", reflect.TypeOf((*MockLbClient)(nil).ListLoadBalancers), arg0) +} + +// ListMonitors mocks base method. +func (m *MockLbClient) ListMonitors(arg0 monitors.ListOptsBuilder) ([]monitors.Monitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListMonitors", arg0) + ret0, _ := ret[0].([]monitors.Monitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListMonitors indicates an expected call of ListMonitors. +func (mr *MockLbClientMockRecorder) ListMonitors(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMonitors", reflect.TypeOf((*MockLbClient)(nil).ListMonitors), arg0) +} + +// ListPoolMember mocks base method. +func (m *MockLbClient) ListPoolMember(arg0 string, arg1 pools.ListMembersOptsBuilder) ([]pools.Member, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPoolMember", arg0, arg1) + ret0, _ := ret[0].([]pools.Member) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPoolMember indicates an expected call of ListPoolMember. +func (mr *MockLbClientMockRecorder) ListPoolMember(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPoolMember", reflect.TypeOf((*MockLbClient)(nil).ListPoolMember), arg0, arg1) +} + +// ListPools mocks base method. +func (m *MockLbClient) ListPools(arg0 pools.ListOptsBuilder) ([]pools.Pool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPools", arg0) + ret0, _ := ret[0].([]pools.Pool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPools indicates an expected call of ListPools. +func (mr *MockLbClientMockRecorder) ListPools(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPools", reflect.TypeOf((*MockLbClient)(nil).ListPools), arg0) +} diff --git a/pkg/cloud/services/loadbalancer/service.go b/pkg/cloud/services/loadbalancer/service.go index 6c4f5216f1..a692faabdb 100644 --- a/pkg/cloud/services/loadbalancer/service.go +++ b/pkg/cloud/services/loadbalancer/service.go @@ -29,7 +29,8 @@ import ( // Service interfaces with the OpenStack Neutron LBaaS v2 API. type Service struct { - loadbalancerClient *gophercloud.ServiceClient + projectID string + loadbalancerClient LbClient networkingService *networking.Service logger logr.Logger } @@ -49,8 +50,21 @@ func NewService(client *gophercloud.ProviderClient, clientOpts *clientconfig.Cli } return &Service{ - loadbalancerClient: loadbalancerClient, - networkingService: networkingService, - logger: logger, + loadbalancerClient: lbClient{ + serviceClient: loadbalancerClient, + }, + networkingService: networkingService, + logger: logger, }, nil } + +// NewLoadBalancerTestService returns a Service with no initialization. It should only be used by tests. +// It helps to mock the load balancer service in other packages. +func NewLoadBalancerTestService(projectID string, lbClient LbClient, client *networking.Service, logger logr.Logger) *Service { + return &Service{ + projectID: projectID, + loadbalancerClient: lbClient, + networkingService: client, + logger: logger, + } +} diff --git a/pkg/cloud/services/networking/mock_networking/client_mock.go b/pkg/cloud/services/networking/mock_networking/client_mock.go index 6636392746..7928fc836a 100644 --- a/pkg/cloud/services/networking/mock_networking/client_mock.go +++ b/pkg/cloud/services/networking/mock_networking/client_mock.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +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.