Skip to content

Commit 1bb2539

Browse files
authored
Merge pull request #64 from Kuadrant/missing-permissions
Add missing delete permission for APIKeyApproval owner references
2 parents 4058346 + d6a539a commit 1bb2539

6 files changed

Lines changed: 416 additions & 48 deletions

File tree

config/rbac/role.yaml

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ rules:
1818
- devportal.kuadrant.io
1919
resources:
2020
- apikeyapprovals
21+
- apikeyrequests
22+
- apiproducts
2123
verbs:
2224
- create
25+
- delete
2326
- get
2427
- list
2528
- patch
@@ -36,19 +39,6 @@ rules:
3639
- get
3740
- patch
3841
- update
39-
- apiGroups:
40-
- devportal.kuadrant.io
41-
resources:
42-
- apikeyrequests
43-
- apiproducts
44-
verbs:
45-
- create
46-
- delete
47-
- get
48-
- list
49-
- patch
50-
- update
51-
- watch
5242
- apiGroups:
5343
- devportal.kuadrant.io
5444
resources:

internal/controller/apikeyapproval_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type APIKeyApprovalReconciler struct {
3838
Scheme *runtime.Scheme
3939
}
4040

41-
// +kubebuilder:rbac:groups=devportal.kuadrant.io,resources=apikeyapprovals,verbs=get;list;watch;update;patch
41+
// +kubebuilder:rbac:groups=devportal.kuadrant.io,resources=apikeyapprovals,verbs=get;list;watch;update;patch;delete
4242
// +kubebuilder:rbac:groups=devportal.kuadrant.io,resources=apikeyrequests,verbs=get;list;watch
4343

4444
// Reconcile handles reconciling all APIKeyApprovals in a single call. Any resource event should enqueue the
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package e2e
18+
19+
import (
20+
"fmt"
21+
"os/exec"
22+
"time"
23+
24+
. "github.com/onsi/ginkgo/v2"
25+
. "github.com/onsi/gomega"
26+
27+
"github.com/kuadrant/developer-portal-controller/test/utils"
28+
)
29+
30+
var _ = Describe("APIKeyApproval Garbage Collection", Ordered, func() {
31+
const (
32+
ownerNamespace = "gc-owner-test"
33+
consumerNamespace = "gc-consumer-test"
34+
kuadrantNamespace = "gc-kuadrant-ns"
35+
controllerNamespace = "developer-portal-controller-system"
36+
)
37+
38+
AfterEach(func() {
39+
specReport := CurrentSpecReport()
40+
if specReport.Failed() {
41+
By("Fetching controller manager pod name")
42+
cmd := exec.Command("kubectl", "get",
43+
"pods", "-l", "control-plane=controller-manager",
44+
"-o", "go-template={{ range .items }}"+
45+
"{{ if not .metadata.deletionTimestamp }}"+
46+
"{{ .metadata.name }}"+
47+
"{{ \"\\n\" }}{{ end }}{{ end }}",
48+
"-n", controllerNamespace,
49+
)
50+
podOutput, err := utils.Run(cmd)
51+
if err != nil {
52+
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get controller pod name: %s\n", err)
53+
return
54+
}
55+
podNames := utils.GetNonEmptyLines(podOutput)
56+
if len(podNames) == 0 {
57+
_, _ = fmt.Fprintf(GinkgoWriter, "No controller pod found\n")
58+
return
59+
}
60+
controllerPodName := podNames[0]
61+
62+
By("Fetching controller manager pod logs")
63+
cmd = exec.Command("kubectl", "logs", controllerPodName, "-n", controllerNamespace)
64+
controllerLogs, err := utils.Run(cmd)
65+
if err == nil {
66+
_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n%s\n", controllerLogs)
67+
} else {
68+
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s\n", err)
69+
}
70+
71+
By("Fetching Kubernetes events in owner namespace")
72+
cmd = exec.Command("kubectl", "get", "events", "-n", ownerNamespace, "--sort-by=.lastTimestamp")
73+
eventsOutput, err := utils.Run(cmd)
74+
if err == nil {
75+
_, _ = fmt.Fprintf(GinkgoWriter, "Events in %s:\n%s\n", ownerNamespace, eventsOutput)
76+
} else {
77+
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get events in %s: %s\n", ownerNamespace, err)
78+
}
79+
80+
By("Fetching Kubernetes events in consumer namespace")
81+
cmd = exec.Command("kubectl", "get", "events", "-n", consumerNamespace, "--sort-by=.lastTimestamp")
82+
eventsOutput, err = utils.Run(cmd)
83+
if err == nil {
84+
_, _ = fmt.Fprintf(GinkgoWriter, "Events in %s:\n%s\n", consumerNamespace, eventsOutput)
85+
} else {
86+
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get events in %s: %s\n", consumerNamespace, err)
87+
}
88+
}
89+
})
90+
91+
BeforeAll(func() {
92+
SetDefaultEventuallyTimeout(2 * time.Minute)
93+
SetDefaultEventuallyPollingInterval(2 * time.Second)
94+
95+
By("setting up namespaces and Kuadrant instance")
96+
SetupNamespacesAndKuadrant(ownerNamespace, consumerNamespace, kuadrantNamespace)
97+
})
98+
99+
AfterAll(func() {
100+
By("cleaning up kuadrant namespace")
101+
cmd := exec.Command("kubectl", "delete", "ns", kuadrantNamespace, "--wait=false")
102+
_, _ = utils.Run(cmd)
103+
104+
By("cleaning up owner namespace")
105+
cmd = exec.Command("kubectl", "delete", "ns", ownerNamespace, "--wait=false")
106+
_, _ = utils.Run(cmd)
107+
108+
By("cleaning up consumer namespace")
109+
cmd = exec.Command("kubectl", "delete", "ns", consumerNamespace, "--wait=false")
110+
_, _ = utils.Run(cmd)
111+
})
112+
113+
Context("APIKeyApproval owner reference and garbage collection", func() {
114+
It("should garbage collect APIKeyApproval when APIKey is deleted", func() {
115+
By("creating an HTTPRoute as a reference target")
116+
httpRouteYAML := fmt.Sprintf(`
117+
apiVersion: gateway.networking.k8s.io/v1
118+
kind: HTTPRoute
119+
metadata:
120+
name: test-route-gc
121+
namespace: %s
122+
spec:
123+
parentRefs:
124+
- name: test-gateway
125+
rules:
126+
- matches:
127+
- path:
128+
type: PathPrefix
129+
value: /api
130+
backendRefs:
131+
- name: test-service
132+
port: 8080
133+
`, ownerNamespace)
134+
135+
cmd := exec.Command("kubectl", "apply", "-f", "-")
136+
cmd.Stdin = utils.StringReader(httpRouteYAML)
137+
_, err := utils.Run(cmd)
138+
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTPRoute")
139+
140+
By("creating an AuthPolicy with API key authentication")
141+
authPolicyYAML := fmt.Sprintf(`
142+
apiVersion: kuadrant.io/v1
143+
kind: AuthPolicy
144+
metadata:
145+
name: test-auth-policy-gc
146+
namespace: %s
147+
spec:
148+
targetRef:
149+
group: gateway.networking.k8s.io
150+
kind: HTTPRoute
151+
name: test-route-gc
152+
rules:
153+
authentication:
154+
"api-key":
155+
apiKey:
156+
selector:
157+
matchLabels:
158+
kuadrant.io/apikeys: "true"
159+
credentials:
160+
authorizationHeader:
161+
prefix: "API-KEY"
162+
`, ownerNamespace)
163+
164+
cmd = exec.Command("kubectl", "apply", "-f", "-")
165+
cmd.Stdin = utils.StringReader(authPolicyYAML)
166+
_, err = utils.Run(cmd)
167+
Expect(err).NotTo(HaveOccurred(), "Failed to create AuthPolicy")
168+
169+
By("updating AuthPolicy status to Accepted and Enforced")
170+
authPolicyStatusPatch := `{
171+
"status": {
172+
"conditions": [
173+
{
174+
"type": "Accepted",
175+
"status": "True",
176+
"reason": "Accepted",
177+
"message": "AuthPolicy has been accepted",
178+
"lastTransitionTime": "2024-01-01T00:00:00Z"
179+
},
180+
{
181+
"type": "Enforced",
182+
"status": "True",
183+
"reason": "Enforced",
184+
"message": "AuthPolicy has been successfully enforced",
185+
"lastTransitionTime": "2024-01-01T00:00:00Z"
186+
}
187+
]
188+
}
189+
}`
190+
191+
cmd = exec.Command("kubectl", "patch", "authpolicy", "test-auth-policy-gc",
192+
"-n", ownerNamespace,
193+
"--type=merge",
194+
"--subresource=status",
195+
"-p", authPolicyStatusPatch)
196+
_, err = utils.Run(cmd)
197+
Expect(err).NotTo(HaveOccurred(), "Failed to update AuthPolicy status")
198+
199+
By("creating an APIProduct with automatic approval mode")
200+
apiProductGCName := "gc-test-api"
201+
apiProductYAML := fmt.Sprintf(`
202+
apiVersion: devportal.kuadrant.io/v1alpha1
203+
kind: APIProduct
204+
metadata:
205+
name: %s
206+
namespace: %s
207+
spec:
208+
displayName: "Garbage Collection Test API"
209+
description: "API Product for testing garbage collection"
210+
approvalMode: automatic
211+
publishStatus: Published
212+
targetRef:
213+
group: gateway.networking.k8s.io
214+
kind: HTTPRoute
215+
name: test-route-gc
216+
`, apiProductGCName, ownerNamespace)
217+
218+
cmd = exec.Command("kubectl", "apply", "-f", "-")
219+
cmd.Stdin = utils.StringReader(apiProductYAML)
220+
_, err = utils.Run(cmd)
221+
Expect(err).NotTo(HaveOccurred(), "Failed to create APIProduct")
222+
223+
By("creating a secret with API key in the consumer namespace")
224+
apiKeyGCName := "gc-test-apikey"
225+
secretYAML := fmt.Sprintf(`
226+
apiVersion: v1
227+
kind: Secret
228+
metadata:
229+
name: %s-secret
230+
namespace: %s
231+
type: Opaque
232+
stringData:
233+
api_key: gc-test-key-value-12345
234+
`, apiKeyGCName, consumerNamespace)
235+
236+
cmd = exec.Command("kubectl", "apply", "-f", "-")
237+
cmd.Stdin = utils.StringReader(secretYAML)
238+
_, err = utils.Run(cmd)
239+
Expect(err).NotTo(HaveOccurred(), "Failed to create secret")
240+
241+
By("creating an APIKey in the consumer namespace")
242+
apiKeyYAML := fmt.Sprintf(`
243+
apiVersion: devportal.kuadrant.io/v1alpha1
244+
kind: APIKey
245+
metadata:
246+
name: %s
247+
namespace: %s
248+
spec:
249+
apiProductRef:
250+
name: %s
251+
namespace: %s
252+
secretRef:
253+
name: %s-secret
254+
planTier: premium
255+
useCase: "Testing garbage collection"
256+
requestedBy:
257+
userId: gc-test-user-123
258+
email: gctest@example.com
259+
`, apiKeyGCName, consumerNamespace, apiProductGCName, ownerNamespace, apiKeyGCName)
260+
261+
cmd = exec.Command("kubectl", "apply", "-f", "-")
262+
cmd.Stdin = utils.StringReader(apiKeyYAML)
263+
_, err = utils.Run(cmd)
264+
Expect(err).NotTo(HaveOccurred(), "Failed to create APIKey")
265+
266+
By("verifying APIKeyRequest was created in the owner namespace")
267+
var apiKeyRequestName string
268+
verifyAPIKeyRequestCreated := func(g Gomega) {
269+
cmd := exec.Command("kubectl", "get", "apikeyrequest",
270+
"-n", ownerNamespace,
271+
"-o", "jsonpath={.items[?(@.spec.apiKeyRef.name=='"+apiKeyGCName+"')].metadata.name}")
272+
output, err := utils.Run(cmd)
273+
g.Expect(err).NotTo(HaveOccurred())
274+
g.Expect(output).NotTo(BeEmpty(), "APIKeyRequest should be created")
275+
apiKeyRequestName = output
276+
}
277+
Eventually(verifyAPIKeyRequestCreated).Should(Succeed())
278+
279+
By("verifying APIKeyApproval was automatically created")
280+
apiKeyApprovalName := fmt.Sprintf("%s-auto", apiKeyRequestName)
281+
verifyApprovalCreated := func(g Gomega) {
282+
cmd := exec.Command("kubectl", "get", "apikeyapproval", apiKeyApprovalName,
283+
"-n", ownerNamespace, "-o", "jsonpath={.metadata.name}")
284+
output, err := utils.Run(cmd)
285+
g.Expect(err).NotTo(HaveOccurred())
286+
g.Expect(output).To(Equal(apiKeyApprovalName), "APIKeyApproval should be created")
287+
}
288+
Eventually(verifyApprovalCreated).Should(Succeed())
289+
290+
By("verifying APIKeyApproval has owner reference to APIKeyRequest")
291+
verifyOwnerReference := func(g Gomega) {
292+
cmd := exec.Command("kubectl", "get", "apikeyapproval", apiKeyApprovalName,
293+
"-n", ownerNamespace,
294+
"-o", "jsonpath={.metadata.ownerReferences[0].name}")
295+
output, err := utils.Run(cmd)
296+
g.Expect(err).NotTo(HaveOccurred())
297+
g.Expect(output).To(Equal(apiKeyRequestName), "APIKeyApproval should have owner reference to APIKeyRequest")
298+
}
299+
Eventually(verifyOwnerReference).Should(Succeed())
300+
301+
By("verifying owner reference kind is APIKeyRequest")
302+
verifyOwnerKind := func(g Gomega) {
303+
cmd := exec.Command("kubectl", "get", "apikeyapproval", apiKeyApprovalName,
304+
"-n", ownerNamespace,
305+
"-o", "jsonpath={.metadata.ownerReferences[0].kind}")
306+
output, err := utils.Run(cmd)
307+
g.Expect(err).NotTo(HaveOccurred())
308+
g.Expect(output).To(Equal("APIKeyRequest"), "Owner reference kind should be APIKeyRequest")
309+
}
310+
Eventually(verifyOwnerKind).Should(Succeed())
311+
312+
By("verifying owner reference controller is set to true")
313+
verifyOwnerController := func(g Gomega) {
314+
cmd := exec.Command("kubectl", "get", "apikeyapproval", apiKeyApprovalName,
315+
"-n", ownerNamespace,
316+
"-o", "jsonpath={.metadata.ownerReferences[0].controller}")
317+
output, err := utils.Run(cmd)
318+
g.Expect(err).NotTo(HaveOccurred())
319+
g.Expect(output).To(Equal("true"), "Owner reference controller should be true")
320+
}
321+
Eventually(verifyOwnerController).Should(Succeed())
322+
323+
By("deleting the APIKey")
324+
cmd = exec.Command("kubectl", "delete", "apikey", apiKeyGCName, "-n", consumerNamespace)
325+
_, err = utils.Run(cmd)
326+
Expect(err).NotTo(HaveOccurred(), "Failed to delete APIKey")
327+
328+
By("verifying APIKeyRequest is deleted")
329+
verifyAPIKeyRequestDeleted := func(g Gomega) {
330+
cmd := exec.Command("kubectl", "get", "apikeyrequest", apiKeyRequestName,
331+
"-n", ownerNamespace)
332+
_, err := utils.Run(cmd)
333+
g.Expect(err).To(HaveOccurred(), "APIKeyRequest should be deleted")
334+
}
335+
Eventually(verifyAPIKeyRequestDeleted, 30*time.Second).Should(Succeed())
336+
337+
By("verifying APIKeyApproval is garbage collected")
338+
verifyApprovalDeleted := func(g Gomega) {
339+
cmd := exec.Command("kubectl", "get", "apikeyapproval", apiKeyApprovalName,
340+
"-n", ownerNamespace)
341+
_, err := utils.Run(cmd)
342+
g.Expect(err).To(HaveOccurred(), "APIKeyApproval should be garbage collected")
343+
}
344+
Eventually(verifyApprovalDeleted, 30*time.Second).Should(Succeed())
345+
})
346+
})
347+
})

0 commit comments

Comments
 (0)