Skip to content

Commit 58c450e

Browse files
committed
Cleanup placement during decomissioning
Usually, there should be no resources on the host left after having migrated all machines. But ocassionally, we are still left with some empty resources. This change removes all empty resources before removing the resource-provider as well.
1 parent c1abcd7 commit 58c450e

2 files changed

Lines changed: 132 additions & 5 deletions

File tree

internal/controller/decomission_controller.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/gophercloud/gophercloud/v2"
3636
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/hypervisors"
3737
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/services"
38+
"github.com/gophercloud/gophercloud/v2/openstack/placement/v1/resourceproviders"
3839
)
3940

4041
const (
@@ -43,8 +44,9 @@ const (
4344

4445
type NodeDecommissionReconciler struct {
4546
k8sclient.Client
46-
Scheme *runtime.Scheme
47-
computeClient *gophercloud.ServiceClient
47+
Scheme *runtime.Scheme
48+
computeClient *gophercloud.ServiceClient
49+
placementClient *gophercloud.ServiceClient
4850
}
4951

5052
// The counter-side in gardener is here:
@@ -126,11 +128,21 @@ func (r *NodeDecommissionReconciler) shutdownService(ctx context.Context, node *
126128

127129
// Deleting and evicted, so better delete the service
128130
err = services.Delete(ctx, r.computeClient, hypervisor.Service.ID).ExtractErr()
129-
if err == nil || gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
130-
return r.removeFinalizer(ctx, node)
131+
if err != nil && !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
132+
return ctrl.Result{}, fmt.Errorf("cannot delete service due to %w", err)
133+
}
134+
135+
rp, err := resourceproviders.Get(ctx, r.placementClient, hypervisorID).Extract()
136+
if err != nil && !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
137+
return ctrl.Result{}, fmt.Errorf("cannot get resource provider due to %w", err)
138+
}
139+
140+
err = openstack.CleanupResourceProvider(ctx, r.placementClient, rp)
141+
if err != nil {
142+
return ctrl.Result{}, fmt.Errorf("cannot clean up resource provider due to %w", err)
131143
}
132144

133-
return ctrl.Result{}, err
145+
return r.removeFinalizer(ctx, node)
134146
}
135147

136148
func (r *NodeDecommissionReconciler) removeFinalizer(ctx context.Context, node *corev1.Node) (ctrl.Result, error) {
@@ -153,6 +165,12 @@ func (r *NodeDecommissionReconciler) SetupWithManager(mgr ctrl.Manager) error {
153165

154166
r.computeClient.Microversion = "2.93"
155167

168+
r.placementClient, err = openstack.GetServiceClient(ctx, "placement")
169+
if err != nil {
170+
return err
171+
}
172+
r.placementClient.Microversion = "1.39" // yoga, or later
173+
156174
return ctrl.NewControllerManagedBy(mgr).
157175
Named("nodeDecommission").
158176
For(&corev1.Node{}).

internal/openstack/placement.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ package openstack
1919

2020
import (
2121
"context"
22+
"fmt"
2223

2324
"github.com/gophercloud/gophercloud/v2"
25+
"github.com/gophercloud/gophercloud/v2/openstack/placement/v1/resourceproviders"
2426
)
2527

2628
// UpdateTraitsResult is the response of a Put traits operations. Call its Extract method
@@ -67,3 +69,110 @@ func UpdateTraits(ctx context.Context, client *gophercloud.ServiceClient, resour
6769
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
6870
return
6971
}
72+
73+
func getAllocationsURL(client *gophercloud.ServiceClient, consumerID string) string {
74+
return client.ServiceURL("allocations", consumerID)
75+
}
76+
77+
// ListAllocationsResult is the response of a Get allocations operations. Call its Extract method
78+
// to interpret it as a Allocations.
79+
type ListAllocationsResult struct {
80+
gophercloud.Result
81+
}
82+
83+
type ConsumerAllocations struct {
84+
Allocations map[string]resourceproviders.Allocation `json:"allocations"`
85+
ConsumerGeneration int `json:"consumer_generation"`
86+
ProjectID string `json:"project_id"`
87+
UserID string `json:"user_id"`
88+
ConsumerType string `json:"consumer_type"`
89+
}
90+
91+
// Extract interprets a ListAllocationsResult as a Allocations.
92+
func (r ListAllocationsResult) Extract() (*ConsumerAllocations, error) {
93+
var s ConsumerAllocations
94+
err := r.ExtractInto(&s)
95+
return &s, err
96+
}
97+
98+
// List Allocations for a certain consumer
99+
func ListAllocations(ctx context.Context, client *gophercloud.ServiceClient, consumerID string) (r ListAllocationsResult) {
100+
resp, err := client.Get(ctx, getAllocationsURL(client, consumerID), nil, &gophercloud.RequestOpts{
101+
OkCodes: []int{200},
102+
})
103+
if err != nil {
104+
r.Err = err
105+
return
106+
}
107+
108+
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
109+
return
110+
}
111+
112+
// Delete all Allocations for a certain consumer
113+
func DeleteConsumerAllocations(ctx context.Context, client *gophercloud.ServiceClient, consumerID string) (r ListAllocationsResult) {
114+
resp, err := client.Delete(ctx, getAllocationsURL(client, consumerID), &gophercloud.RequestOpts{
115+
OkCodes: []int{204, 404},
116+
})
117+
if err != nil {
118+
r.Err = err
119+
return
120+
}
121+
122+
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
123+
return
124+
}
125+
126+
// Remove all empty Allocations for a certain provider, and if it is empty, delete it
127+
func CleanupResourceProvider(ctx context.Context, client *gophercloud.ServiceClient, provider *resourceproviders.ResourceProvider) error {
128+
if provider == nil {
129+
return nil
130+
}
131+
132+
providerAllocations, err := resourceproviders.GetAllocations(ctx, client, provider.UUID).Extract()
133+
if err != nil {
134+
return err
135+
}
136+
137+
// No allocations, we can delete the provider
138+
if providerAllocations == nil {
139+
err = resourceproviders.Delete(ctx, client, provider.UUID).ExtractErr()
140+
return fmt.Errorf("failed to delete without cleanup due to %w", err)
141+
}
142+
143+
// We have some allocations on the provider, let's see if we can delete them all
144+
allocations := len(providerAllocations.Allocations) // Number of allocations on the provider
145+
146+
// It is a map of consumer-ids to their alloctions, we just go over their ids
147+
// to cross-check, what is stored for the consumer itself
148+
for consumerID := range providerAllocations.Allocations {
149+
// Allocations of the consumer mapped by the resource provider, so the
150+
// "reverse" of what we got before
151+
result := ListAllocations(ctx, client, consumerID)
152+
consumerAllocations, err := result.Extract()
153+
if err != nil {
154+
return err
155+
}
156+
157+
// The consumer actually doesn't have *any* allocations, so it is just
158+
// inconsistent, and we can drop them all
159+
if len(consumerAllocations.Allocations) == 0 {
160+
DeleteConsumerAllocations(ctx, client, consumerID)
161+
allocations -= 1 // That clears up one allocation
162+
} else {
163+
return fmt.Errorf("cannot clean up provider, cannot handle non-empty consumer allocations")
164+
}
165+
}
166+
167+
if allocations != 0 {
168+
return fmt.Errorf("cannot clean up provider, still has some allocation")
169+
}
170+
171+
// We are done, let's clean it up
172+
err = resourceproviders.Delete(ctx, client, provider.UUID).ExtractErr()
173+
if err != nil {
174+
return fmt.Errorf("failed to delete after cleanup due to %w", err)
175+
}
176+
177+
return nil
178+
}

0 commit comments

Comments
 (0)