Skip to content

Commit 49c4897

Browse files
committed
Placement: Only export externally used functions, fix ListAllocations
ListAllocations (now listAllocations) didn't read the body, so we never had any allocations. Add some unit-test to ensure that is working.
1 parent 2888717 commit 49c4897

File tree

3 files changed

+362
-6
lines changed

3 files changed

+362
-6
lines changed

internal/openstack/placement.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,20 @@ func (r ListAllocationsResult) Extract() (*ConsumerAllocations, error) {
9797
}
9898

9999
// List Allocations for a certain consumer
100-
func ListAllocations(ctx context.Context, client *gophercloud.ServiceClient, consumerID string) (r ListAllocationsResult) {
101-
resp, err := client.Get(ctx, getAllocationsURL(client, consumerID), nil, &gophercloud.RequestOpts{ //nolint:bodyclose
100+
func listAllocations(ctx context.Context, client *gophercloud.ServiceClient, consumerID string) (r ListAllocationsResult) {
101+
resp, err := client.Get(ctx, getAllocationsURL(client, consumerID), &r.Body, &gophercloud.RequestOpts{ //nolint:bodyclose
102102
OkCodes: []int{200},
103103
})
104104
if err != nil {
105105
r.Err = err
106106
return
107107
}
108-
109108
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
110109
return
111110
}
112111

113112
// Delete all Allocations for a certain consumer
114-
func DeleteConsumerAllocations(ctx context.Context, client *gophercloud.ServiceClient, consumerID string) (r ListAllocationsResult) {
113+
func deleteConsumerAllocations(ctx context.Context, client *gophercloud.ServiceClient, consumerID string) (r ListAllocationsResult) {
115114
resp, err := client.Delete(ctx, getAllocationsURL(client, consumerID), &gophercloud.RequestOpts{ //nolint:bodyclose
116115
OkCodes: []int{204, 404},
117116
})
@@ -140,7 +139,7 @@ func CleanupResourceProvider(ctx context.Context, client *gophercloud.ServiceCli
140139
for consumerID := range providerAllocations.Allocations {
141140
// Allocations of the consumer mapped by the resource provider, so the
142141
// "reverse" of what we got before
143-
result := ListAllocations(ctx, client, consumerID)
142+
result := listAllocations(ctx, client, consumerID)
144143
consumerAllocations, err := result.Extract()
145144
if err != nil {
146145
return err
@@ -152,7 +151,7 @@ func CleanupResourceProvider(ctx context.Context, client *gophercloud.ServiceCli
152151

153152
// The consumer actually doesn't have *any* allocations, so it is just
154153
// inconsistent, and we can drop them all
155-
DeleteConsumerAllocations(ctx, client, consumerID)
154+
deleteConsumerAllocations(ctx, client, consumerID)
156155
}
157156

158157
// We are done, let's clean it up
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
/*
2+
SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package openstack
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"net/http"
24+
25+
"github.com/gophercloud/gophercloud/v2/openstack/placement/v1/resourceproviders"
26+
"github.com/gophercloud/gophercloud/v2/testhelper"
27+
"github.com/gophercloud/gophercloud/v2/testhelper/client"
28+
. "github.com/onsi/ginkgo/v2"
29+
. "github.com/onsi/gomega"
30+
)
31+
32+
var _ = Describe("Placement API", func() {
33+
var (
34+
fakeServer testhelper.FakeServer
35+
ctx context.Context
36+
)
37+
38+
BeforeEach(func() {
39+
ctx = context.Background()
40+
fakeServer = testhelper.SetupHTTP()
41+
DeferCleanup(fakeServer.Teardown)
42+
})
43+
44+
Describe("UpdateTraits", func() {
45+
const (
46+
resourceProviderID = "test-rp-uuid"
47+
traitsURL = "/resource_providers/test-rp-uuid/traits"
48+
)
49+
50+
Context("when the request is successful", func() {
51+
BeforeEach(func() {
52+
fakeServer.Mux.HandleFunc(traitsURL, func(w http.ResponseWriter, r *http.Request) {
53+
Expect(r.Method).To(Equal("PUT"))
54+
Expect(r.Header.Get("Content-Type")).To(Equal("application/json"))
55+
56+
w.Header().Set("Content-Type", "application/json")
57+
w.WriteHeader(http.StatusOK)
58+
fmt.Fprint(w, `{
59+
"traits": ["CUSTOM_TRAIT_1", "CUSTOM_TRAIT_2"],
60+
"resource_provider_generation": 2
61+
}`)
62+
})
63+
})
64+
65+
It("should update traits successfully", func() {
66+
opts := UpdateTraitsOpts{
67+
Traits: []string{"CUSTOM_TRAIT_1", "CUSTOM_TRAIT_2"},
68+
ResourceProviderGeneration: 1,
69+
}
70+
71+
result := UpdateTraits(ctx, client.ServiceClient(fakeServer), resourceProviderID, opts)
72+
Expect(result.Err).NotTo(HaveOccurred())
73+
})
74+
})
75+
76+
Context("when the request fails", func() {
77+
BeforeEach(func() {
78+
fakeServer.Mux.HandleFunc(traitsURL, func(w http.ResponseWriter, r *http.Request) {
79+
w.WriteHeader(http.StatusInternalServerError)
80+
fmt.Fprint(w, `{"error": "Internal Server Error"}`)
81+
})
82+
})
83+
84+
It("should return an error", func() {
85+
opts := UpdateTraitsOpts{
86+
Traits: []string{"CUSTOM_TRAIT_1"},
87+
ResourceProviderGeneration: 1,
88+
}
89+
90+
result := UpdateTraits(ctx, client.ServiceClient(fakeServer), resourceProviderID, opts)
91+
Expect(result.Err).To(HaveOccurred())
92+
})
93+
})
94+
95+
Context("when building request body fails", func() {
96+
It("should return an error", func() {
97+
// Create an invalid opts that would fail marshaling
98+
// This tests the ToResourceProviderUpdateTraitsMap error path
99+
opts := UpdateTraitsOpts{
100+
Traits: []string{"TRAIT1"},
101+
ResourceProviderGeneration: 1,
102+
}
103+
104+
// The actual function doesn't have a way to make BuildRequestBody fail
105+
// but we can still verify the happy path
106+
body, err := opts.ToResourceProviderUpdateTraitsMap()
107+
Expect(err).NotTo(HaveOccurred())
108+
Expect(body).To(HaveKey("traits"))
109+
Expect(body).To(HaveKey("resource_provider_generation"))
110+
})
111+
})
112+
})
113+
114+
Describe("CleanupResourceProvider", func() {
115+
const (
116+
providerUUID = "test-provider-uuid"
117+
providerAllocsURL = "/resource_providers/test-provider-uuid/allocations"
118+
deleteProviderURL = "/resource_providers/test-provider-uuid"
119+
)
120+
121+
Context("when provider is nil", func() {
122+
It("should return nil without errors", func() {
123+
err := CleanupResourceProvider(ctx, client.ServiceClient(fakeServer), nil)
124+
Expect(err).NotTo(HaveOccurred())
125+
})
126+
})
127+
128+
Context("when provider has no allocations", func() {
129+
BeforeEach(func() {
130+
fakeServer.Mux.HandleFunc(providerAllocsURL, func(w http.ResponseWriter, r *http.Request) {
131+
Expect(r.Method).To(Equal("GET"))
132+
w.Header().Set("Content-Type", "application/json")
133+
w.WriteHeader(http.StatusOK)
134+
fmt.Fprint(w, `{"allocations": {}}`)
135+
})
136+
137+
fakeServer.Mux.HandleFunc(deleteProviderURL, func(w http.ResponseWriter, r *http.Request) {
138+
Expect(r.Method).To(Equal("DELETE"))
139+
w.WriteHeader(http.StatusNoContent)
140+
})
141+
})
142+
143+
It("should delete the provider successfully", func() {
144+
provider := &resourceproviders.ResourceProvider{
145+
UUID: providerUUID,
146+
Name: "test-provider",
147+
}
148+
149+
err := CleanupResourceProvider(ctx, client.ServiceClient(fakeServer), provider)
150+
Expect(err).NotTo(HaveOccurred())
151+
})
152+
})
153+
154+
Context("when provider has empty consumer allocations", func() {
155+
const (
156+
consumer1ID = "consumer-1"
157+
consumer2ID = "consumer-2"
158+
)
159+
160+
BeforeEach(func() {
161+
// Provider has two consumers
162+
fakeServer.Mux.HandleFunc(providerAllocsURL, func(w http.ResponseWriter, r *http.Request) {
163+
w.Header().Set("Content-Type", "application/json")
164+
w.WriteHeader(http.StatusOK)
165+
fmt.Fprintf(w, `{
166+
"allocations": {
167+
"%s": {},
168+
"%s": {}
169+
}
170+
}`, consumer1ID, consumer2ID)
171+
})
172+
173+
// Both consumers have empty allocations
174+
fakeServer.Mux.HandleFunc("/allocations/consumer-1", func(w http.ResponseWriter, r *http.Request) {
175+
switch r.Method {
176+
case http.MethodGet:
177+
w.Header().Set("Content-Type", "application/json")
178+
w.WriteHeader(http.StatusOK)
179+
fmt.Fprint(w, `{"allocations": {}, "consumer_generation": 0}`)
180+
case http.MethodDelete:
181+
w.WriteHeader(http.StatusNoContent)
182+
}
183+
})
184+
185+
fakeServer.Mux.HandleFunc("/allocations/consumer-2", func(w http.ResponseWriter, r *http.Request) {
186+
switch r.Method {
187+
case http.MethodGet:
188+
w.Header().Set("Content-Type", "application/json")
189+
w.WriteHeader(http.StatusOK)
190+
fmt.Fprint(w, `{"allocations": {}, "consumer_generation": 0}`)
191+
case http.MethodDelete:
192+
w.WriteHeader(http.StatusNoContent)
193+
}
194+
})
195+
196+
fakeServer.Mux.HandleFunc(deleteProviderURL, func(w http.ResponseWriter, r *http.Request) {
197+
w.WriteHeader(http.StatusNoContent)
198+
})
199+
})
200+
201+
It("should clean up consumers and delete provider", func() {
202+
provider := &resourceproviders.ResourceProvider{
203+
UUID: providerUUID,
204+
Name: "test-provider",
205+
}
206+
207+
err := CleanupResourceProvider(ctx, client.ServiceClient(fakeServer), provider)
208+
Expect(err).NotTo(HaveOccurred())
209+
})
210+
})
211+
212+
Context("when provider has non-empty consumer allocations", func() {
213+
BeforeEach(func() {
214+
fakeServer.Mux.HandleFunc(providerAllocsURL, func(w http.ResponseWriter, r *http.Request) {
215+
w.Header().Set("Content-Type", "application/json")
216+
w.WriteHeader(http.StatusOK)
217+
fmt.Fprint(w, `{
218+
"allocations": {
219+
"consumer-with-allocs": {}
220+
}
221+
}`)
222+
})
223+
224+
// Consumer has actual allocations
225+
fakeServer.Mux.HandleFunc("/allocations/consumer-with-allocs", func(w http.ResponseWriter, r *http.Request) {
226+
w.Header().Set("Content-Type", "application/json")
227+
w.WriteHeader(http.StatusOK)
228+
fmt.Fprint(w, `{
229+
"allocations": {
230+
"rp-uuid": {
231+
"generation": 1,
232+
"resources": {
233+
"VCPU": 2
234+
}
235+
}
236+
},
237+
"consumer_generation": 1
238+
}`)
239+
})
240+
241+
// Don't delete the provider when it has non-empty allocations
242+
})
243+
244+
It("should return an error about non-empty allocations", func() {
245+
provider := &resourceproviders.ResourceProvider{
246+
UUID: providerUUID,
247+
Name: "test-provider",
248+
}
249+
250+
err := CleanupResourceProvider(ctx, client.ServiceClient(fakeServer), provider)
251+
Expect(err).To(HaveOccurred())
252+
Expect(err.Error()).To(ContainSubstring("cannot clean up provider"))
253+
Expect(err.Error()).To(ContainSubstring("non-empty consumer allocations"))
254+
})
255+
})
256+
257+
Context("when getting provider allocations fails", func() {
258+
BeforeEach(func() {
259+
fakeServer.Mux.HandleFunc(providerAllocsURL, func(w http.ResponseWriter, r *http.Request) {
260+
w.WriteHeader(http.StatusInternalServerError)
261+
fmt.Fprint(w, `{"error": "Internal Server Error"}`)
262+
})
263+
})
264+
265+
It("should return an error", func() {
266+
provider := &resourceproviders.ResourceProvider{
267+
UUID: providerUUID,
268+
Name: "test-provider",
269+
}
270+
271+
err := CleanupResourceProvider(ctx, client.ServiceClient(fakeServer), provider)
272+
Expect(err).To(HaveOccurred())
273+
})
274+
})
275+
276+
Context("when getting consumer allocations fails", func() {
277+
BeforeEach(func() {
278+
fakeServer.Mux.HandleFunc(providerAllocsURL, func(w http.ResponseWriter, r *http.Request) {
279+
w.Header().Set("Content-Type", "application/json")
280+
w.WriteHeader(http.StatusOK)
281+
fmt.Fprint(w, `{"allocations": {"consumer-1": {}}}`)
282+
})
283+
284+
fakeServer.Mux.HandleFunc("/allocations/consumer-1", func(w http.ResponseWriter, r *http.Request) {
285+
w.WriteHeader(http.StatusInternalServerError)
286+
fmt.Fprint(w, `{"error": "Internal Server Error"}`)
287+
})
288+
})
289+
290+
It("should return an error", func() {
291+
provider := &resourceproviders.ResourceProvider{
292+
UUID: providerUUID,
293+
Name: "test-provider",
294+
}
295+
296+
err := CleanupResourceProvider(ctx, client.ServiceClient(fakeServer), provider)
297+
Expect(err).To(HaveOccurred())
298+
})
299+
})
300+
301+
Context("when deleting provider fails", func() {
302+
BeforeEach(func() {
303+
fakeServer.Mux.HandleFunc(providerAllocsURL, func(w http.ResponseWriter, r *http.Request) {
304+
w.Header().Set("Content-Type", "application/json")
305+
w.WriteHeader(http.StatusOK)
306+
fmt.Fprint(w, `{"allocations": {}}`)
307+
})
308+
309+
fakeServer.Mux.HandleFunc(deleteProviderURL, func(w http.ResponseWriter, r *http.Request) {
310+
w.WriteHeader(http.StatusInternalServerError)
311+
fmt.Fprint(w, `{"error": "Internal Server Error"}`)
312+
})
313+
})
314+
315+
It("should return an error", func() {
316+
provider := &resourceproviders.ResourceProvider{
317+
UUID: providerUUID,
318+
Name: "test-provider",
319+
}
320+
321+
err := CleanupResourceProvider(ctx, client.ServiceClient(fakeServer), provider)
322+
Expect(err).To(HaveOccurred())
323+
Expect(err.Error()).To(ContainSubstring("failed to delete after cleanup"))
324+
})
325+
})
326+
})
327+
})

0 commit comments

Comments
 (0)