Skip to content

Commit 2fb408b

Browse files
committed
Move service and secret cleanup to ownerReferences to simplify the
deletion of teams (and reduce permissions requried)
1 parent 78a6c79 commit 2fb408b

9 files changed

Lines changed: 65 additions & 187 deletions

File tree

balancer/routes/adminDeleteInstance.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,14 @@ func handleAdminDeleteInstance(bundle *bundle.Bundle) http.Handler {
1818
return
1919
}
2020

21+
// Only the deployment needs to be deleted explicitly.
22+
// The service and secret are owned by the deployment via OwnerReferences and will be garbage collected by Kubernetes.
2123
err := bundle.ClientSet.AppsV1().Deployments(bundle.RuntimeEnvironment.Namespace).Delete(req.Context(), fmt.Sprintf("juiceshop-%s", teamToDelete), metav1.DeleteOptions{})
2224
if err != nil && !errors.IsNotFound(err) {
2325
bundle.Log.Printf("Failed to delete deployment for team '%s': %s", teamToDelete, err)
2426
http.Error(responseWriter, "", http.StatusInternalServerError)
2527
return
2628
}
27-
err = bundle.ClientSet.CoreV1().Services(bundle.RuntimeEnvironment.Namespace).Delete(req.Context(), fmt.Sprintf("juiceshop-%s", teamToDelete), metav1.DeleteOptions{})
28-
if err != nil && !errors.IsNotFound(err) {
29-
bundle.Log.Printf("Failed to delete service for team '%s': %s", teamToDelete, err)
30-
http.Error(responseWriter, "", http.StatusInternalServerError)
31-
return
32-
}
33-
34-
err = bundle.ClientSet.CoreV1().Secrets(bundle.RuntimeEnvironment.Namespace).Delete(req.Context(), fmt.Sprintf("juiceshop-%s", teamToDelete), metav1.DeleteOptions{})
35-
if err != nil && !errors.IsNotFound(err) {
36-
bundle.Log.Printf("Failed to delete secret for team '%s': %s", teamToDelete, err)
37-
}
3829

3930
responseWriter.WriteHeader(http.StatusOK)
4031
responseWriter.Write([]byte{}) // nosemgrep: go.lang.security.audit.xss.no-direct-write-to-responsewriter.no-direct-write-to-responsewriter

balancer/routes/adminDeleteInstance_test.go

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/juice-shop/multi-juicer/balancer/pkg/testutil"
1111
"github.com/stretchr/testify/assert"
1212
appsv1 "k8s.io/api/apps/v1"
13-
corev1 "k8s.io/api/core/v1"
1413
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1514
"k8s.io/apimachinery/pkg/runtime/schema"
1615
"k8s.io/client-go/kubernetes/fake"
@@ -40,35 +39,14 @@ func TestAdminDeleteInstanceHandler(t *testing.T) {
4039
},
4140
}
4241
}
43-
createServiceForTeam := func(team string) *corev1.Service {
44-
return &corev1.Service{
45-
ObjectMeta: metav1.ObjectMeta{
46-
Name: fmt.Sprintf("juiceshop-%s", team),
47-
Namespace: "test-namespace",
48-
Labels: map[string]string{
49-
"app.kubernetes.io/name": "juice-shop",
50-
"app.kubernetes.io/part-of": "multi-juicer",
51-
"team": team,
52-
},
53-
},
54-
Spec: corev1.ServiceSpec{
55-
Ports: []corev1.ServicePort{
56-
{
57-
Port: 3000,
58-
},
59-
},
60-
},
61-
}
62-
}
63-
6442
t.Run("deleting instances requires admin login", func(t *testing.T) {
6543
req, _ := http.NewRequest("DELETE", "/balancer/api/admin/teams/foobar/delete", nil)
6644
req.Header.Set("Cookie", fmt.Sprintf("team=%s", testutil.SignTestTeamname("some team")))
6745
rr := httptest.NewRecorder()
6846

6947
server := http.NewServeMux()
7048

71-
clientset := fake.NewClientset(createDeploymentForTeam("foobar"), createServiceForTeam("foobar"))
49+
clientset := fake.NewClientset(createDeploymentForTeam("foobar"))
7250
bundle := testutil.NewTestBundleWithCustomFakeClient(clientset)
7351
AddRoutes(server, bundle)
7452

@@ -85,7 +63,7 @@ func TestAdminDeleteInstanceHandler(t *testing.T) {
8563

8664
server := http.NewServeMux()
8765

88-
clientset := fake.NewClientset(createDeploymentForTeam("foobar"), createServiceForTeam("foobar"))
66+
clientset := fake.NewClientset(createDeploymentForTeam("foobar"))
8967
bundle := testutil.NewTestBundleWithCustomFakeClient(clientset)
9068
AddRoutes(server, bundle)
9169

@@ -94,7 +72,7 @@ func TestAdminDeleteInstanceHandler(t *testing.T) {
9472
assert.Equal(t, http.StatusBadRequest, rr.Code)
9573
})
9674

97-
t.Run("deletes both deployments and services of teams", func(t *testing.T) {
75+
t.Run("deletes deployment of team", func(t *testing.T) {
9876
req, _ := http.NewRequest("DELETE", "/balancer/api/admin/teams/foobar/delete", nil)
9977
req.Header.Set("Cookie", fmt.Sprintf("team=%s", testutil.SignTestTeamname("admin")))
10078
rr := httptest.NewRecorder()
@@ -103,9 +81,7 @@ func TestAdminDeleteInstanceHandler(t *testing.T) {
10381

10482
clientset := fake.NewClientset(
10583
createDeploymentForTeam("foobar"),
106-
createServiceForTeam("foobar"),
10784
createDeploymentForTeam("other-team"),
108-
createServiceForTeam("other-team"),
10985
)
11086
bundle := testutil.NewTestBundleWithCustomFakeClient(clientset)
11187
AddRoutes(server, bundle)
@@ -119,16 +95,10 @@ func TestAdminDeleteInstanceHandler(t *testing.T) {
11995

12096
assert.Equal(t, "delete", actions[0].GetVerb())
12197
assert.Equal(t, schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, actions[0].GetResource())
122-
assert.Equal(t, "delete", actions[1].GetVerb())
123-
assert.Equal(t, schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}, actions[1].GetResource())
12498

12599
deployments, err := clientset.AppsV1().Deployments("test-namespace").List(context.Background(), metav1.ListOptions{})
126100
assert.Nil(t, err)
127101
assert.Len(t, deployments.Items, 1)
128-
129-
services, err := clientset.CoreV1().Services("test-namespace").List(context.Background(), metav1.ListOptions{})
130-
assert.Nil(t, err)
131-
assert.Len(t, services.Items, 1)
132102
})
133103

134104
}

balancer/routes/join.go

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -145,23 +145,23 @@ func createANewTeam(context context.Context, bundle *bundle.Bundle, team string,
145145
return
146146
}
147147

148+
deployment, err := createDeploymentForTeam(context, bundle, team, passcodeHash)
149+
if err != nil {
150+
bundle.Log.Printf("Failed to create deployment: %s", err)
151+
http.Error(w, "failed to create deployment", http.StatusInternalServerError)
152+
return
153+
}
154+
148155
if bundle.Config.JuiceShopConfig.LLM.Enabled {
149-
err = createLLMTokenSecretForTeam(context, bundle, team)
156+
err = createLLMTokenSecretForTeam(context, bundle, team, deployment)
150157
if err != nil {
151158
bundle.Log.Printf("Failed to create LLM token secret: %s", err)
152159
http.Error(w, "failed to create LLM token secret", http.StatusInternalServerError)
153160
return
154161
}
155162
}
156163

157-
err = createDeploymentForTeam(context, bundle, team, passcodeHash)
158-
if err != nil {
159-
bundle.Log.Printf("Failed to create deployment: %s", err)
160-
http.Error(w, "failed to create deployment", http.StatusInternalServerError)
161-
return
162-
}
163-
164-
err = createServiceForTeam(context, bundle, team)
164+
err = createServiceForTeam(context, bundle, team, deployment)
165165
if err != nil {
166166
bundle.Log.Printf("Failed to create service: %s", err)
167167
http.Error(w, "failed to create service", http.StatusInternalServerError)
@@ -272,7 +272,21 @@ func writeUnauthorizedResponse(responseWriter http.ResponseWriter) {
272272
responseWriter.Write(errorResponseBody) // nosemgrep: go.lang.security.audit.xss.no-direct-write-to-responsewriter.no-direct-write-to-responsewriter
273273
}
274274

275-
// uid of the balancer kubernetes deployment resource. used to "attach" created juice shop deployments and services to the balancer deployment so that they get deleted when the balancer gets deleted
275+
func getDeploymentOwnerReferences(deployment *appsv1.Deployment) []metav1.OwnerReference {
276+
truePointer := true
277+
return []metav1.OwnerReference{
278+
{
279+
APIVersion: "apps/v1",
280+
Kind: "Deployment",
281+
Name: deployment.Name,
282+
UID: deployment.UID,
283+
Controller: &truePointer,
284+
BlockOwnerDeletion: &truePointer,
285+
},
286+
}
287+
}
288+
289+
// uid of the balancer kubernetes deployment resource. used to "attach" created juice shop deployments to the balancer deployment so that they get deleted when the balancer gets deleted
276290
var deploymentUid types.UID
277291

278292
func getOwnerReferences(context context.Context, bundle *bundle.Bundle) ([]metav1.OwnerReference, error) {
@@ -302,10 +316,10 @@ func getOwnerReferences(context context.Context, bundle *bundle.Bundle) ([]metav
302316
return ownerReferences, nil
303317
}
304318

305-
func createDeploymentForTeam(context context.Context, bundle *bundle.Bundle, team string, passcodeHash string) error {
319+
func createDeploymentForTeam(context context.Context, bundle *bundle.Bundle, team string, passcodeHash string) (*appsv1.Deployment, error) {
306320
ownerReferences, err := getOwnerReferences(context, bundle)
307321
if err != nil {
308-
return err
322+
return nil, err
309323
}
310324

311325
podLabels := map[string]string{}
@@ -433,16 +447,11 @@ func createDeploymentForTeam(context context.Context, bundle *bundle.Bundle, tea
433447
},
434448
}
435449

436-
_, err = bundle.ClientSet.AppsV1().Deployments(bundle.RuntimeEnvironment.Namespace).Create(context, deployment, metav1.CreateOptions{})
437-
return err
450+
created, err := bundle.ClientSet.AppsV1().Deployments(bundle.RuntimeEnvironment.Namespace).Create(context, deployment, metav1.CreateOptions{})
451+
return created, err
438452
}
439453

440-
func createServiceForTeam(context context.Context, bundle *bundle.Bundle, team string) error {
441-
ownerReferences, err := getOwnerReferences(context, bundle)
442-
if err != nil {
443-
return err
444-
}
445-
454+
func createServiceForTeam(context context.Context, bundle *bundle.Bundle, team string, ownerDeployment *appsv1.Deployment) error {
446455
service := &corev1.Service{
447456
ObjectMeta: metav1.ObjectMeta{
448457
Name: fmt.Sprintf("juiceshop-%s", team),
@@ -454,7 +463,7 @@ func createServiceForTeam(context context.Context, bundle *bundle.Bundle, team s
454463
"app.kubernetes.io/instance": fmt.Sprintf("juice-shop-%s", team),
455464
"app.kubernetes.io/part-of": "multi-juicer",
456465
},
457-
OwnerReferences: ownerReferences,
466+
OwnerReferences: getDeploymentOwnerReferences(ownerDeployment),
458467
},
459468
Spec: corev1.ServiceSpec{
460469
Selector: map[string]string{
@@ -469,7 +478,7 @@ func createServiceForTeam(context context.Context, bundle *bundle.Bundle, team s
469478
},
470479
}
471480

472-
_, err = bundle.ClientSet.CoreV1().Services(bundle.RuntimeEnvironment.Namespace).Create(context, service, metav1.CreateOptions{})
481+
_, err := bundle.ClientSet.CoreV1().Services(bundle.RuntimeEnvironment.Namespace).Create(context, service, metav1.CreateOptions{})
473482
return err
474483
}
475484

@@ -507,17 +516,12 @@ func buildJuiceShopEnv(bundle *bundle.Bundle, team string) []corev1.EnvVar {
507516
return envVars
508517
}
509518

510-
func createLLMTokenSecretForTeam(ctx context.Context, bundle *bundle.Bundle, team string) error {
519+
func createLLMTokenSecretForTeam(ctx context.Context, bundle *bundle.Bundle, team string, ownerDeployment *appsv1.Deployment) error {
511520
token, err := signutil.Sign(team, bundle.Config.CookieConfig.SigningKey)
512521
if err != nil {
513522
return fmt.Errorf("failed to sign LLM token: %w", err)
514523
}
515524

516-
ownerReferences, err := getOwnerReferences(ctx, bundle)
517-
if err != nil {
518-
return err
519-
}
520-
521525
secret := &corev1.Secret{
522526
ObjectMeta: metav1.ObjectMeta{
523527
Name: fmt.Sprintf("juiceshop-%s", team),
@@ -526,7 +530,7 @@ func createLLMTokenSecretForTeam(ctx context.Context, bundle *bundle.Bundle, tea
526530
"app.kubernetes.io/component": "llm-token",
527531
"app.kubernetes.io/part-of": "multi-juicer",
528532
},
529-
OwnerReferences: ownerReferences,
533+
OwnerReferences: getDeploymentOwnerReferences(ownerDeployment),
530534
},
531535
Data: map[string][]byte{
532536
"token": []byte(token),

balancer/routes/join_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ func TestJoinHandler(t *testing.T) {
119119
{
120120
APIVersion: "apps/v1",
121121
Kind: "Deployment",
122-
Name: "balancer",
123-
UID: "34c0bb8a-240b-4f2a-84ae-2eb2258298f9",
122+
Name: deployment.Name,
123+
UID: deployment.UID,
124124
Controller: &truePointer,
125125
BlockOwnerDeletion: &truePointer,
126126
},

cleaner/main.go

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,15 @@ func main() {
4040
cleanupSummary := runCleanup(clientset, currentTime, maxInactiveTime)
4141

4242
logger.Println("Finished cleaning up JuiceShop deployments.")
43-
logger.Printf("Deleted %d deployment(s) and %d service(s) successfully", cleanupSummary.SuccessfulDeploymentDeletions, cleanupSummary.SuccessfulServiceDeletions)
44-
if (cleanupSummary.FailedDeploymentDeletions + cleanupSummary.FailedServiceDeletions) > 0 {
45-
logger.Printf("Failed to delete %d deployment(s) and %d service(s)", cleanupSummary.FailedDeploymentDeletions, cleanupSummary.FailedServiceDeletions)
43+
logger.Printf("Deleted %d deployment(s) successfully", cleanupSummary.SuccessfulDeletions)
44+
if cleanupSummary.FailedDeletions > 0 {
45+
logger.Printf("Failed to delete %d deployment(s)", cleanupSummary.FailedDeletions)
4646
}
4747
}
4848

4949
type CleanupSummary struct {
50-
SuccessfulDeploymentDeletions int
51-
SuccessfulServiceDeletions int
52-
FailedDeploymentDeletions int
53-
FailedServiceDeletions int
50+
SuccessfulDeletions int
51+
FailedDeletions int
5452
}
5553

5654
func runCleanup(clientset kubernetes.Interface, currentTime time.Time, maxInactive time.Duration) CleanupSummary {
@@ -66,12 +64,7 @@ func runCleanup(clientset kubernetes.Interface, currentTime time.Time, maxInacti
6664
logger.Println("No JuiceShop deployments found. Nothing to do.")
6765
}
6866

69-
summary := CleanupSummary{
70-
SuccessfulDeploymentDeletions: 0,
71-
SuccessfulServiceDeletions: 0,
72-
FailedDeploymentDeletions: 0,
73-
FailedServiceDeletions: 0,
74-
}
67+
summary := CleanupSummary{}
7568

7669
for _, deployment := range deployments.Items {
7770
lastConnectedTimestampString, hasAnnotation := deployment.Annotations["multi-juicer.owasp-juice.shop/lastRequest"]
@@ -88,26 +81,15 @@ func runCleanup(clientset kubernetes.Interface, currentTime time.Time, maxInacti
8881
name := deployment.Name
8982
if currentTime.Sub(time.UnixMilli(lastConnectedTimestamp)) > maxInactive {
9083
logger.Printf("Deleting instance '%s' as it has been inactive for longer than %s", name, maxInactive.String())
84+
// Only the deployment needs to be deleted explicitly.
85+
// The service and secret are owned by the deployment via OwnerReferences and will be garbage collected by Kubernetes.
9186
err = clientset.AppsV1().Deployments(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
9287
if err != nil && !errors.IsNotFound(err) {
9388
logger.Printf("Failed to delete deployment %s: %v", name, err)
94-
summary.FailedDeploymentDeletions++
95-
continue
96-
}
97-
summary.SuccessfulDeploymentDeletions++
98-
err = clientset.CoreV1().Services(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
99-
if err != nil && !errors.IsNotFound(err) {
100-
logger.Printf("Failed to delete service %s: %v", name, err)
101-
summary.FailedServiceDeletions++
89+
summary.FailedDeletions++
10290
continue
10391
}
104-
summary.SuccessfulServiceDeletions++
105-
106-
err = clientset.CoreV1().Secrets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
107-
if err != nil && !errors.IsNotFound(err) {
108-
logger.Printf("Failed to delete kubernetes secret for %s: %v", name, err)
109-
}
110-
92+
summary.SuccessfulDeletions++
11193
logger.Printf("Successfully deleted instance %s", name)
11294
} else {
11395
logger.Printf("Skipping deployment %s as it has been active recently", name)

0 commit comments

Comments
 (0)