Skip to content

Commit 8029566

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 8029566

2 files changed

Lines changed: 118 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: 95 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,96 @@ 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+
// It is a map of consumer-ids to their alloctions, we just go over their ids
138+
// to cross-check, what is stored for the consumer itself
139+
for consumerID := range providerAllocations.Allocations {
140+
// Allocations of the consumer mapped by the resource provider, so the
141+
// "reverse" of what we got before
142+
result := ListAllocations(ctx, client, consumerID)
143+
consumerAllocations, err := result.Extract()
144+
if err != nil {
145+
return err
146+
}
147+
148+
if len(consumerAllocations.Allocations) > 0 {
149+
return fmt.Errorf("cannot clean up provider, cannot handle non-empty consumer allocations")
150+
}
151+
152+
// The consumer actually doesn't have *any* allocations, so it is just
153+
// inconsistent, and we can drop them all
154+
DeleteConsumerAllocations(ctx, client, consumerID)
155+
}
156+
157+
// We are done, let's clean it up
158+
err = resourceproviders.Delete(ctx, client, provider.UUID).ExtractErr()
159+
if err != nil {
160+
return fmt.Errorf("failed to delete after cleanup due to %w", err)
161+
}
162+
163+
return nil
164+
}

0 commit comments

Comments
 (0)