Skip to content

Commit 37ed1f7

Browse files
authored
feat: Allow creation of fleetworkspace for existing namespace when annotation is present and set to true. (#938)
1 parent 050c821 commit 37ed1f7

File tree

2 files changed

+140
-2
lines changed

2 files changed

+140
-2
lines changed

pkg/resources/management.cattle.io/v3/fleetworkspace/mutator.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ import (
2121
"k8s.io/kubernetes/pkg/registry/rbac/validation"
2222
)
2323

24-
const k8sManagedLabel = "app.kubernetes.io/managed-by"
24+
const (
25+
k8sManagedLabel = "app.kubernetes.io/managed-by"
26+
27+
// AllowFleetWorkspaceCreationForExistingNamespaceAnn is an annotation key to indicate that a fleet workspace may be created
28+
// for a namespace that already exists.
29+
AllowFleetWorkspaceCreationForExistingNamespaceAnn = "field.cattle.io/allow-fleetworkspace-creation-for-existing-namespace"
30+
)
2531

2632
var (
2733
fleetAdminRole = "fleetworkspace-admin"
@@ -101,7 +107,7 @@ func (m *Mutator) Admit(request *admission.Request) (*admissionv1.AdmissionRespo
101107
if err != nil {
102108
return nil, fmt.Errorf("failed to get fleetworkspace namespace '%s': %w", fw.Name, err)
103109
}
104-
if ns.Labels[k8sManagedLabel] != "rancher" {
110+
if !(ns.Labels[k8sManagedLabel] == "rancher" || (ns.Annotations != nil && ns.Annotations[AllowFleetWorkspaceCreationForExistingNamespaceAnn] == "true")) {
105111
return admission.ResponseBadRequest(fmt.Sprintf("namespace '%s' already exists", fw.Name)), nil
106112
}
107113
cr, err := m.clusterroles.Cache().Get(fleetAdminRole)

pkg/resources/management.cattle.io/v3/fleetworkspace/mutator_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,23 @@ func TestAdmit(t *testing.T) {
9090
req: req,
9191
expectedErr: errors.NewServerTimeout(schema.GroupResource{}, "", 2),
9292
},
93+
"existing namespace with the 'field.cattle.io/allow-fleetworkspace-creation-for-existing-namespace' annotation": {
94+
m: existingNsWithAnnotationMutator,
95+
req: req,
96+
expectAllowed: true,
97+
expectResultStatus: nil,
98+
},
99+
"existing namespace with the 'field.cattle.io/allow-fleetworkspace-creation-for-existing-namespace' annotation set to false": {
100+
m: existingNsWithAnnotationSetToFalseMutator,
101+
req: req,
102+
expectAllowed: false,
103+
expectResultStatus: &metav1.Status{
104+
Status: "Failure",
105+
Message: "namespace 'test' already exists",
106+
Reason: metav1.StatusReasonBadRequest,
107+
Code: http.StatusBadRequest,
108+
},
109+
},
93110
}
94111

95112
for name, test := range tests {
@@ -263,3 +280,118 @@ func newNsErrorMutator(t *testing.T) Mutator {
263280
namespaces: mockNamespaceController,
264281
}
265282
}
283+
284+
func existingNsWithAnnotationMutator(t *testing.T) Mutator {
285+
ctrl := gomock.NewController(t)
286+
287+
mockNamespaceController := fake.NewMockNonNamespacedControllerInterface[*v1.Namespace, *v1.NamespaceList](ctrl)
288+
mockClusterRoleCache := fake.NewMockNonNamespacedCacheInterface[*rbacv1.ClusterRole](ctrl)
289+
mockClusterRoleController := fake.NewMockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList](ctrl)
290+
mockRoleBindingController := fake.NewMockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList](ctrl)
291+
mockClusterRoleBindingController := fake.NewMockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList](ctrl)
292+
293+
// Namespace already exists
294+
mockNamespaceController.EXPECT().Create(gomock.Any()).Return(nil, errors.NewAlreadyExists(gvr.GroupResource(), nsName))
295+
// Get returns existing namespace with the annotation
296+
mockNamespaceController.EXPECT().Get(gomock.Any(), gomock.Any()).Return(&v1.Namespace{
297+
ObjectMeta: metav1.ObjectMeta{
298+
Name: nsName,
299+
Annotations: map[string]string{"field.cattle.io/allow-fleetworkspace-creation-for-existing-namespace": "true"},
300+
},
301+
}, nil)
302+
// Get returns the `fleetworkspace-admin` cluster role
303+
mockClusterRoleCache.EXPECT().Get(fleetAdminRole).Return(&rbacv1.ClusterRole{}, nil)
304+
mockClusterRoleController.EXPECT().Cache().Return(mockClusterRoleCache)
305+
// Create a role binding for the current user with the `fleetworkspace-admin` role
306+
roleBinding := &rbacv1.RoleBinding{
307+
ObjectMeta: metav1.ObjectMeta{
308+
Name: "fleetworkspace-admin-binding-" + nsName,
309+
Namespace: nsName,
310+
},
311+
Subjects: []rbacv1.Subject{
312+
{
313+
Kind: "User",
314+
APIGroup: "rbac.authorization.k8s.io",
315+
Name: user,
316+
},
317+
},
318+
RoleRef: rbacv1.RoleRef{
319+
APIGroup: "rbac.authorization.k8s.io",
320+
Kind: "ClusterRole",
321+
Name: fleetAdminRole,
322+
},
323+
}
324+
mockRoleBindingController.EXPECT().Create(roleBinding)
325+
// Create a cluster role and cluster role binding
326+
clusterRole := &rbacv1.ClusterRole{
327+
ObjectMeta: metav1.ObjectMeta{
328+
Name: "fleetworkspace-own-test",
329+
},
330+
Rules: []rbacv1.PolicyRule{
331+
{
332+
APIGroups: []string{
333+
management.GroupName,
334+
},
335+
Verbs: []string{
336+
"*",
337+
},
338+
Resources: []string{
339+
"fleetworkspaces",
340+
},
341+
ResourceNames: []string{
342+
nsName,
343+
},
344+
},
345+
},
346+
}
347+
348+
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
349+
ObjectMeta: metav1.ObjectMeta{
350+
Name: "fleetworkspace-own-binding-test",
351+
},
352+
Subjects: []rbacv1.Subject{
353+
{
354+
Kind: "User",
355+
APIGroup: "rbac.authorization.k8s.io",
356+
Name: user,
357+
},
358+
},
359+
RoleRef: rbacv1.RoleRef{
360+
APIGroup: "rbac.authorization.k8s.io",
361+
Kind: "ClusterRole",
362+
Name: "fleetworkspace-own-test",
363+
},
364+
}
365+
mockClusterRoleController.EXPECT().Create(gomock.Any()).Return(clusterRole, nil)
366+
mockClusterRoleBindingController.EXPECT().Create(clusterRoleBinding).Return(clusterRoleBinding, nil)
367+
368+
resolver, _ := validation.NewTestRuleResolver(nil, nil, []*rbacv1.ClusterRole{clusterRole}, []*rbacv1.ClusterRoleBinding{clusterRoleBinding})
369+
370+
return Mutator{
371+
namespaces: mockNamespaceController,
372+
clusterroles: mockClusterRoleController,
373+
rolebindings: mockRoleBindingController,
374+
clusterrolebindings: mockClusterRoleBindingController,
375+
resolver: resolver,
376+
}
377+
}
378+
379+
func existingNsWithAnnotationSetToFalseMutator(t *testing.T) Mutator {
380+
ctrl := gomock.NewController(t)
381+
382+
mockNamespaceController := fake.NewMockNonNamespacedControllerInterface[*v1.Namespace, *v1.NamespaceList](ctrl)
383+
384+
// Namespace already exists
385+
mockNamespaceController.EXPECT().Create(gomock.Any()).Return(nil, errors.NewAlreadyExists(gvr.GroupResource(), nsName))
386+
// Get returns existing namespace with the annotation
387+
mockNamespaceController.EXPECT().Get(gomock.Any(), gomock.Any()).Return(&v1.Namespace{
388+
ObjectMeta: metav1.ObjectMeta{
389+
Name: nsName,
390+
Annotations: map[string]string{"field.cattle.io/allow-fleetworkspace-creation-for-existing-namespace": "false"},
391+
},
392+
}, nil)
393+
394+
return Mutator{
395+
namespaces: mockNamespaceController,
396+
}
397+
}

0 commit comments

Comments
 (0)