From 393be92632867584fb10c84ef4adb5d4355d649a Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 26 Sep 2020 16:36:16 +0300 Subject: [PATCH] Add support for multi-namespace tenant ownership --- cmd/gotk/create_tenant.go | 123 ++++++++++++++++++++++++--------- docs/cmd/gotk_create_tenant.md | 24 ++++++- 2 files changed, 110 insertions(+), 37 deletions(-) diff --git a/cmd/gotk/create_tenant.go b/cmd/gotk/create_tenant.go index 7345e006..a970bab1 100644 --- a/cmd/gotk/create_tenant.go +++ b/cmd/gotk/create_tenant.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" ) @@ -36,12 +37,35 @@ var createTenantCmd = &cobra.Command{ Use: "tenant", Short: "Create or update a tenant", Long: ` -The create tenant command generates a namespace and a role binding to limit the -reconcilers scope to the tenant namespace.`, +The create tenant command generates namespaces and role bindings to limit the +reconcilers scope to the tenant namespaces.`, + Example: ` # Create a tenant with access to a namespace + gotk create tenant dev-team \ + --with-namespace=frontend \ + --label=environment=dev + + # Generate tenant namespaces and role bindings in YAML format + gotk create tenant dev-team \ + --with-namespace=frontend \ + --with-namespace=backend \ + --export > dev-team.yaml +`, RunE: createTenantCmdRun, } +const ( + tenantLabel = "toolkit.fluxcd.io/tenant" + tenantRoleBinding = "gotk-reconciler" +) + +var ( + tenantNamespaces []string + tenantClusterRole string +) + func init() { + createTenantCmd.Flags().StringSliceVar(&tenantNamespaces, "with-namespace", nil, "namespace belonging to this tenant") + createTenantCmd.Flags().StringVar(&tenantClusterRole, "cluster-role", "cluster-admin", "cluster role of the tenant role binding") createCmd.AddCommand(createTenantCmd) } @@ -50,41 +74,70 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("tenant name is required") } tenant := args[0] + if err := validation.IsQualifiedName(tenant); len(err) > 0 { + return fmt.Errorf("invalid tenant name '%s': %v", tenant, err) + } - objLabels, err := parseLabels() - if err != nil { - return err + if tenantClusterRole == "" { + return fmt.Errorf("cluster-role is required") } - namespace := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: tenant, - Labels: objLabels, - }, + if tenantNamespaces == nil { + return fmt.Errorf("with-namespace is required") } - roleBinding := rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gotk-reconciler", - Namespace: tenant, - Labels: objLabels, - }, - Subjects: []rbacv1.Subject{ - { + var namespaces []corev1.Namespace + var roleBindings []rbacv1.RoleBinding + + for _, ns := range tenantNamespaces { + if err := validation.IsQualifiedName(ns); len(err) > 0 { + return fmt.Errorf("invalid namespace '%s': %v", ns, err) + } + + objLabels, err := parseLabels() + if err != nil { + return err + } + + objLabels[tenantLabel] = tenant + + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: ns, + Labels: objLabels, + }, + } + namespaces = append(namespaces, namespace) + + roleBinding := rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: tenantRoleBinding, + Namespace: ns, + Labels: objLabels, + }, + Subjects: []rbacv1.Subject{ + { + APIGroup: "rbac.authorization.k8s.io", + Kind: "User", + Name: fmt.Sprintf("gotk:%s:reconciler", ns), + }, + }, + RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", - Kind: "User", - Name: fmt.Sprintf("gotk:%s:reconciler", tenant), + Kind: "ClusterRole", + Name: tenantClusterRole, }, - }, - RoleRef: rbacv1.RoleRef{ - APIGroup: "rbac.authorization.k8s.io", - Kind: "ClusterRole", - Name: "cluster-admin", - }, + } + roleBindings = append(roleBindings, roleBinding) } if export { - return exportTenant(namespace, roleBinding) + for i, _ := range tenantNamespaces { + if err := exportTenant(namespaces[i], roleBindings[1]); err != nil { + return err + } + } + return nil } ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -95,14 +148,16 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error { return err } - logger.Actionf("applying namespace %s", namespace.Name) - if err := upsertNamespace(ctx, kubeClient, namespace); err != nil { - return err - } + for i, _ := range tenantNamespaces { + logger.Actionf("applying namespace %s", namespaces[i].Name) + if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil { + return err + } - logger.Actionf("applying role binding %s", roleBinding.Name) - if err := upsertRoleBinding(ctx, kubeClient, roleBinding); err != nil { - return err + logger.Actionf("applying role binding %s", roleBindings[i].Name) + if err := upsertRoleBinding(ctx, kubeClient, roleBindings[i]); err != nil { + return err + } } logger.Successf("tenant setup completed") diff --git a/docs/cmd/gotk_create_tenant.md b/docs/cmd/gotk_create_tenant.md index f79a7bc8..d15bc773 100644 --- a/docs/cmd/gotk_create_tenant.md +++ b/docs/cmd/gotk_create_tenant.md @@ -5,17 +5,35 @@ Create or update a tenant ### Synopsis -The create tenant command generates a namespace and a role binding to limit the -reconcilers scope to the tenant namespace. +The create tenant command generates namespaces and role bindings to limit the +reconcilers scope to the tenant namespaces. ``` gotk create tenant [flags] ``` +### Examples + +``` + # Create a tenant with access to a namespace + gotk create tenant dev-team \ + --with-namespace=frontend \ + --label=environment=dev + + # Generate tenant namespaces and role bindings in YAML format + gotk create tenant dev-team \ + --with-namespace=frontend \ + --with-namespace=backend \ + --export > dev-team.yaml + +``` + ### Options ``` - -h, --help help for tenant + --cluster-role string cluster role of the tenant role binding (default "cluster-admin") + -h, --help help for tenant + --with-namespace strings namespace belonging to this tenant ``` ### Options inherited from parent commands