diff --git a/controllers/openstackmachine_controller_test.go b/controllers/openstackmachine_controller_test.go index 2e3e135ab7..3e5346338e 100644 --- a/controllers/openstackmachine_controller_test.go +++ b/controllers/openstackmachine_controller_test.go @@ -263,8 +263,12 @@ func Test_reconcileDelete(t *testing.T) { deleteDefaultPorts := func(r *recorders) { trunkExtension := extensions.Extension{} trunkExtension.Alias = "trunk" + portList := []trunks.RemoveSubport{{PortID: portUUID}} + removeSubportOpts := trunks.RemoveSubportsOpts{Subports: portList} r.network.ListExtensions().Return([]extensions.Extension{trunkExtension}, nil) r.network.ListTrunk(trunks.ListOpts{PortID: portUUID}).Return([]trunks.Trunk{{ID: trunkUUID}}, nil) + r.network.ListTrunkSubports(trunkUUID).Return([]trunks.Subport{}, nil) + r.network.RemoveSubports(trunkUUID, removeSubportOpts).Return(nil) r.network.DeleteTrunk(trunkUUID).Return(nil) r.network.DeletePort(portUUID).Return(nil) } diff --git a/pkg/clients/mock/network.go b/pkg/clients/mock/network.go index e6ccbcd509..7a2a225812 100644 --- a/pkg/clients/mock/network.go +++ b/pkg/clients/mock/network.go @@ -551,6 +551,21 @@ func (mr *MockNetworkClientMockRecorder) ListTrunk(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTrunk", reflect.TypeOf((*MockNetworkClient)(nil).ListTrunk), arg0) } +// ListTrunkSubports mocks base method. +func (m *MockNetworkClient) ListTrunkSubports(arg0 string) ([]trunks.Subport, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListTrunkSubports", arg0) + ret0, _ := ret[0].([]trunks.Subport) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListTrunkSubports indicates an expected call of ListTrunkSubports. +func (mr *MockNetworkClientMockRecorder) ListTrunkSubports(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTrunkSubports", reflect.TypeOf((*MockNetworkClient)(nil).ListTrunkSubports), arg0) +} + // RemoveRouterInterface mocks base method. func (m *MockNetworkClient) RemoveRouterInterface(arg0 string, arg1 routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { m.ctrl.T.Helper() @@ -566,6 +581,20 @@ func (mr *MockNetworkClientMockRecorder) RemoveRouterInterface(arg0, arg1 any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRouterInterface", reflect.TypeOf((*MockNetworkClient)(nil).RemoveRouterInterface), arg0, arg1) } +// RemoveSubports mocks base method. +func (m *MockNetworkClient) RemoveSubports(arg0 string, arg1 trunks.RemoveSubportsOpts) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveSubports", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveSubports indicates an expected call of RemoveSubports. +func (mr *MockNetworkClientMockRecorder) RemoveSubports(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveSubports", reflect.TypeOf((*MockNetworkClient)(nil).RemoveSubports), arg0, arg1) +} + // ReplaceAllAttributesTags mocks base method. func (m *MockNetworkClient) ReplaceAllAttributesTags(arg0, arg1 string, arg2 attributestags.ReplaceAllOptsBuilder) ([]string, error) { m.ctrl.T.Helper() diff --git a/pkg/clients/networking.go b/pkg/clients/networking.go index 23ad3c8fdf..8967c7b34e 100644 --- a/pkg/clients/networking.go +++ b/pkg/clients/networking.go @@ -53,6 +53,9 @@ type NetworkClient interface { CreateTrunk(opts trunks.CreateOptsBuilder) (*trunks.Trunk, error) DeleteTrunk(id string) error + ListTrunkSubports(trunkID string) ([]trunks.Subport, error) + RemoveSubports(id string, opts trunks.RemoveSubportsOpts) error + ListRouter(opts routers.ListOpts) ([]routers.Router, error) CreateRouter(opts routers.CreateOptsBuilder) (*routers.Router, error) DeleteRouter(id string) error @@ -238,6 +241,24 @@ func (c networkClient) DeleteTrunk(id string) error { return mc.ObserveRequestIgnoreNotFound(trunks.Delete(c.serviceClient, id).ExtractErr()) } +func (c networkClient) ListTrunkSubports(trunkID string) ([]trunks.Subport, error) { + mc := metrics.NewMetricPrometheusContext("trunk", "listsubports") + subports, err := trunks.GetSubports(c.serviceClient, trunkID).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return subports, nil +} + +func (c networkClient) RemoveSubports(id string, opts trunks.RemoveSubportsOpts) error { + mc := metrics.NewMetricPrometheusContext("trunk", "deletesubports") + _, err := trunks.RemoveSubports(c.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return err + } + return nil +} + func (c networkClient) ListTrunk(opts trunks.ListOptsBuilder) ([]trunks.Trunk, error) { mc := metrics.NewMetricPrometheusContext("trunk", "list") allPages, err := trunks.List(c.serviceClient, opts).AllPages() diff --git a/pkg/cloud/services/networking/trunk.go b/pkg/cloud/services/networking/trunk.go index 12ef3f4462..9cb5e15e09 100644 --- a/pkg/cloud/services/networking/trunk.go +++ b/pkg/cloud/services/networking/trunk.go @@ -26,13 +26,15 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" + "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" ) const ( - timeoutTrunkDelete = 3 * time.Minute - retryIntervalTrunkDelete = 5 * time.Second + timeoutTrunkDelete = 3 * time.Minute + retryIntervalTrunkDelete = 5 * time.Second + retryIntervalSubportDelete = 30 * time.Second ) func (s *Service) GetTrunkSupport() (bool, error) { @@ -77,6 +79,44 @@ func (s *Service) getOrCreateTrunkForPort(eventObject runtime.Object, port *port return trunk, nil } +func (s *Service) RemoveTrunkSubports(trunkID string) error { + mc := metrics.NewMetricPrometheusContext("trunk", "deletesubports") + subports, err := s.client.ListTrunkSubports(trunkID) + + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return err + } + + if len(subports) == 0 { + return nil + } + + portList := make([]trunks.RemoveSubport, len(subports)) + for i, subport := range subports { + portList[i] = trunks.RemoveSubport{ + PortID: subport.PortID, + } + } + + removeSubportsOpts := trunks.RemoveSubportsOpts{ + Subports: portList, + } + + err = s.client.RemoveSubports(trunkID, removeSubportsOpts) + if mc.ObserveRequest(err) != nil { + return err + } + + for _, subPort := range subports { + err := s.client.DeletePort(subPort.PortID) + if mc.ObserveRequest(err) != nil { + return err + } + } + + return nil +} + func (s *Service) DeleteTrunk(eventObject runtime.Object, portID string) error { listOpts := trunks.ListOpts{ PortID: portID, @@ -88,6 +128,22 @@ func (s *Service) DeleteTrunk(eventObject runtime.Object, portID string) error { if len(trunkInfo) != 1 { return nil } + // Delete sub-ports if trunk is associated with sub-ports + err = wait.PollUntilContextTimeout(context.TODO(), retryIntervalSubportDelete, timeoutTrunkDelete, true, func(_ context.Context) (bool, error) { + if err := s.RemoveTrunkSubports(trunkInfo[0].ID); err != nil { + if capoerrors.IsNotFound(err) || capoerrors.IsConflict(err) || capoerrors.IsRetryable(err) { + return false, nil + } + return false, err + } + return true, nil + }) + if err != nil { + record.Warnf(eventObject, "FailedRemoveTrunkSubports", "Failed to delete sub ports trunk %s with id %s: %v", trunkInfo[0].Name, trunkInfo[0].ID, err) + return err + } + + record.Eventf(eventObject, "SuccessfulRemoveTrunkSubports", "Removed trunk sub ports %s with id %s", trunkInfo[0].Name, trunkInfo[0].ID) err = wait.PollUntilContextTimeout(context.TODO(), retryIntervalTrunkDelete, timeoutTrunkDelete, true, func(_ context.Context) (bool, error) { if err := s.client.DeleteTrunk(trunkInfo[0].ID); err != nil {