From 2096532f5dcd9fd7d1e162d50a82f64db654f9d8 Mon Sep 17 00:00:00 2001
From: stefanprodan <stefan.prodan@gmail.com>
Date: Tue, 15 Sep 2020 16:36:21 +0300
Subject: [PATCH] Add create tenant command

---
 cmd/gotk/create_tenant.go      | 201 +++++++++++++++++++++++++++++++++
 cmd/gotk/utils.go              |   2 +
 docs/cmd/gotk_create.md        |   1 +
 docs/cmd/gotk_create_tenant.md |  36 ++++++
 mkdocs.yml                     |   1 +
 5 files changed, 241 insertions(+)
 create mode 100644 cmd/gotk/create_tenant.go
 create mode 100644 docs/cmd/gotk_create_tenant.md

diff --git a/cmd/gotk/create_tenant.go b/cmd/gotk/create_tenant.go
new file mode 100644
index 00000000..7345e006
--- /dev/null
+++ b/cmd/gotk/create_tenant.go
@@ -0,0 +1,201 @@
+/*
+Copyright 2020 The Flux CD contributors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+
+	"github.com/spf13/cobra"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apimachinery/pkg/api/equality"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/yaml"
+)
+
+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.`,
+	RunE: createTenantCmdRun,
+}
+
+func init() {
+	createCmd.AddCommand(createTenantCmd)
+}
+
+func createTenantCmdRun(cmd *cobra.Command, args []string) error {
+	if len(args) < 1 {
+		return fmt.Errorf("tenant name is required")
+	}
+	tenant := args[0]
+
+	objLabels, err := parseLabels()
+	if err != nil {
+		return err
+	}
+
+	namespace := corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:   tenant,
+			Labels: objLabels,
+		},
+	}
+
+	roleBinding := rbacv1.RoleBinding{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "gotk-reconciler",
+			Namespace: tenant,
+			Labels:    objLabels,
+		},
+		Subjects: []rbacv1.Subject{
+			{
+				APIGroup: "rbac.authorization.k8s.io",
+				Kind:     "User",
+				Name:     fmt.Sprintf("gotk:%s:reconciler", tenant),
+			},
+		},
+		RoleRef: rbacv1.RoleRef{
+			APIGroup: "rbac.authorization.k8s.io",
+			Kind:     "ClusterRole",
+			Name:     "cluster-admin",
+		},
+	}
+
+	if export {
+		return exportTenant(namespace, roleBinding)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+
+	kubeClient, err := utils.kubeClient(kubeconfig)
+	if err != nil {
+		return err
+	}
+
+	logger.Actionf("applying namespace %s", namespace.Name)
+	if err := upsertNamespace(ctx, kubeClient, namespace); err != nil {
+		return err
+	}
+
+	logger.Actionf("applying role binding %s", roleBinding.Name)
+	if err := upsertRoleBinding(ctx, kubeClient, roleBinding); err != nil {
+		return err
+	}
+
+	logger.Successf("tenant setup completed")
+	return nil
+}
+
+func upsertNamespace(ctx context.Context, kubeClient client.Client, namespace corev1.Namespace) error {
+	namespacedName := types.NamespacedName{
+		Namespace: namespace.GetNamespace(),
+		Name:      namespace.GetName(),
+	}
+
+	var existing corev1.Namespace
+	err := kubeClient.Get(ctx, namespacedName, &existing)
+	if err != nil {
+		if errors.IsNotFound(err) {
+			if err := kubeClient.Create(ctx, &namespace); err != nil {
+				return err
+			} else {
+				return nil
+			}
+		}
+		return err
+	}
+
+	if !equality.Semantic.DeepDerivative(namespace.Labels, existing.Labels) {
+		existing.Labels = namespace.Labels
+		if err := kubeClient.Update(ctx, &existing); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func upsertRoleBinding(ctx context.Context, kubeClient client.Client, roleBinding rbacv1.RoleBinding) error {
+	namespacedName := types.NamespacedName{
+		Namespace: roleBinding.GetNamespace(),
+		Name:      roleBinding.GetName(),
+	}
+
+	var existing rbacv1.RoleBinding
+	err := kubeClient.Get(ctx, namespacedName, &existing)
+	if err != nil {
+		if errors.IsNotFound(err) {
+			if err := kubeClient.Create(ctx, &roleBinding); err != nil {
+				return err
+			} else {
+				return nil
+			}
+		}
+		return err
+	}
+
+	if !equality.Semantic.DeepDerivative(roleBinding.Subjects, existing.Subjects) ||
+		!equality.Semantic.DeepDerivative(roleBinding.RoleRef, existing.RoleRef) ||
+		!equality.Semantic.DeepDerivative(roleBinding.Labels, existing.Labels) {
+		if err := kubeClient.Delete(ctx, &existing); err != nil {
+			return err
+		}
+		if err := kubeClient.Create(ctx, &roleBinding); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func exportTenant(namespace corev1.Namespace, roleBinding rbacv1.RoleBinding) error {
+	namespace.TypeMeta = metav1.TypeMeta{
+		APIVersion: "v1",
+		Kind:       "Namespace",
+	}
+	data, err := yaml.Marshal(namespace)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println("---")
+	data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
+	fmt.Println(resourceToString(data))
+
+	roleBinding.TypeMeta = metav1.TypeMeta{
+		APIVersion: "rbac.authorization.k8s.io/v1",
+		Kind:       "RoleBinding",
+	}
+	data, err = yaml.Marshal(roleBinding)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println("---")
+	fmt.Println(resourceToString(data))
+
+	return nil
+}
diff --git a/cmd/gotk/utils.go b/cmd/gotk/utils.go
index e1dae097..16d0d6bf 100644
--- a/cmd/gotk/utils.go
+++ b/cmd/gotk/utils.go
@@ -27,6 +27,7 @@ import (
 	"text/template"
 
 	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/tools/clientcmd"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -118,6 +119,7 @@ func (*Utils) kubeClient(config string) (client.Client, error) {
 
 	scheme := runtime.NewScheme()
 	_ = corev1.AddToScheme(scheme)
+	_ = rbacv1.AddToScheme(scheme)
 	_ = sourcev1.AddToScheme(scheme)
 	_ = kustomizev1.AddToScheme(scheme)
 	_ = helmv2.AddToScheme(scheme)
diff --git a/docs/cmd/gotk_create.md b/docs/cmd/gotk_create.md
index f16fe7d0..c96baf9a 100644
--- a/docs/cmd/gotk_create.md
+++ b/docs/cmd/gotk_create.md
@@ -30,4 +30,5 @@ The create sub-commands generate sources and resources.
 * [gotk create helmrelease](gotk_create_helmrelease.md)	 - Create or update a HelmRelease resource
 * [gotk create kustomization](gotk_create_kustomization.md)	 - Create or update a Kustomization resource
 * [gotk create source](gotk_create_source.md)	 - Create or update sources
+* [gotk create tenant](gotk_create_tenant.md)	 - Create or update a tenant
 
diff --git a/docs/cmd/gotk_create_tenant.md b/docs/cmd/gotk_create_tenant.md
new file mode 100644
index 00000000..82d75550
--- /dev/null
+++ b/docs/cmd/gotk_create_tenant.md
@@ -0,0 +1,36 @@
+## gotk create tenant
+
+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.
+
+```
+gotk create tenant [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for tenant
+```
+
+### Options inherited from parent commands
+
+```
+      --export              export in YAML format to stdout
+      --interval duration   source sync interval (default 1m0s)
+      --kubeconfig string   path to the kubeconfig file (default "~/.kube/config")
+      --label strings       set labels on the resource (can specify multiple labels with commas: label1=value1,label2=value2)
+      --namespace string    the namespace scope for this operation (default "gitops-system")
+      --timeout duration    timeout for this operation (default 5m0s)
+      --verbose             print generated objects
+```
+
+### SEE ALSO
+
+* [gotk create](gotk_create.md)	 - Create or update sources and resources
+
diff --git a/mkdocs.yml b/mkdocs.yml
index ef9c36cf..6053cdc9 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -85,6 +85,7 @@ nav:
     - Create source: cmd/gotk_create_source.md
     - Create source git: cmd/gotk_create_source_git.md
     - Create source helm: cmd/gotk_create_source_helm.md
+    - Create tenant: cmd/gotk_create_tenant.md
     - Delete: cmd/gotk_delete.md
     - Delete kustomization: cmd/gotk_delete_kustomization.md
     - Delete helmrelease: cmd/gotk_delete_helmrelease.md
-- 
GitLab