diff --git a/cmd/sveltosctl/main.go b/cmd/sveltosctl/main.go index 42d841cd..c6f2f26d 100644 --- a/cmd/sveltosctl/main.go +++ b/cmd/sveltosctl/main.go @@ -48,6 +48,8 @@ func main() { snapshot Displays collected snaphost. Visualize diffs between two collected snapshots. techsupport Displays collected techsupport. register Onboard an existing non CAPI cluster by creating all necessary internal resources. + generate Generates a Kubeconfig that can later be used to register a cluster. + Run this command with sveltosctl pointing to the cluster you want Sveltos to manage. log-level Allows changing the log verbosity. version Display the version of sveltosctl. @@ -105,6 +107,8 @@ Description: err = commands.Techsupport(ctx, args, logger) case "register": err = commands.RegisterCluster(ctx, args, logger) + case "generate": + err = commands.Generate(ctx, args, logger) case "log-level": err = commands.LogLevel(ctx, args, logger) case "version": diff --git a/internal/commands/generate.go b/internal/commands/generate.go new file mode 100644 index 00000000..de09596f --- /dev/null +++ b/internal/commands/generate.go @@ -0,0 +1,78 @@ +/* +Copyright 2024. projectsveltos.io. All rights reserved. + +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 commands + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + + docopt "github.com/docopt/docopt-go" + "github.com/go-logr/logr" + + logs "github.com/projectsveltos/libsveltos/lib/logsettings" + "github.com/projectsveltos/sveltosctl/internal/commands/generate" +) + +// Generate takes care generating a kubeconfig +func Generate(ctx context.Context, args []string, logger logr.Logger) error { + doc := `Usage: + sveltosctl generate [...] + + kubeconfig Generates a Kubeconfig. The generated Kubeconfig can then be used to register a cluster. + Run this command while pointing to the managed cluster. + +Options: + -h --help Show this screen. + +Description: + See 'sveltosctl generate kubeconfig --help' to read about a specific subcommand. + ` + + parser := &docopt.Parser{ + HelpHandler: docopt.PrintHelpAndExit, + OptionsFirst: true, + SkipHelpFlags: false, + } + + opts, err := parser.ParseArgs(doc, nil, "1.0") + if err != nil { + var userError docopt.UserError + if errors.As(err, &userError) { + logger.V(logs.LogInfo).Info(fmt.Sprintf( + "Invalid option: 'sveltosctl %s'. Use flag '--help' to read about a specific subcommand.\n", + strings.Join(os.Args[1:], " "), + )) + } + os.Exit(1) + } + + command := opts[""].(string) + arguments := append([]string{"logLevel", command}, opts[""].([]string)...) + + switch command { + case "kubeconfig": + return generate.GenerateKubeconfig(ctx, arguments, logger) + default: + //nolint: forbidigo // print doc + fmt.Println(doc) + } + + return nil +} diff --git a/internal/commands/generate/export_test.go b/internal/commands/generate/export_test.go new file mode 100644 index 00000000..bc94cb43 --- /dev/null +++ b/internal/commands/generate/export_test.go @@ -0,0 +1,27 @@ +/* +Copyright 2024. projectsveltos.io. All rights reserved. + +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 generate + +const ( + Projectsveltos = projectsveltos +) + +var ( + CreateNamespace = createNamespace + CreateClusterRole = createClusterRole + CreateClusterRoleBinding = createClusterRoleBinding +) diff --git a/internal/commands/generate/generate_kubeconfig.go b/internal/commands/generate/generate_kubeconfig.go new file mode 100644 index 00000000..b9e6406e --- /dev/null +++ b/internal/commands/generate/generate_kubeconfig.go @@ -0,0 +1,326 @@ +/* +Copyright 2024. projectsveltos.io. All rights reserved. + +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 generate + +import ( + "context" + "encoding/base64" + "flag" + "fmt" + "strings" + "time" + + "github.com/docopt/docopt-go" + "github.com/go-logr/logr" + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + + logs "github.com/projectsveltos/libsveltos/lib/logsettings" + "github.com/projectsveltos/sveltosctl/internal/utils" +) + +const ( + projectsveltos = "projectsveltos" + // expirationInSecond is the token expiration time. + saExpirationInSecond = 365 * 24 * 60 * time.Minute +) + +func generateKubeconfigForServiceAccount(ctx context.Context, namespace, serviceAccountName string, + create bool, logger logr.Logger) error { + + if create { + if err := createNamespace(ctx, namespace, logger); err != nil { + return err + } + if err := createServiceAccount(ctx, namespace, serviceAccountName, logger); err != nil { + return err + } + if err := createClusterRole(ctx, projectsveltos, logger); err != nil { + return err + } + if err := createClusterRoleBinding(ctx, projectsveltos, projectsveltos, namespace, serviceAccountName, logger); err != nil { + return err + } + } else { + if err := getNamespace(ctx, namespace); err != nil { + return err + } + if err := getServiceAccount(ctx, namespace, serviceAccountName); err != nil { + return err + } + } + + tokenRequest, err := getServiceAccountTokenRequest(ctx, namespace, serviceAccountName, logger) + if err != nil { + return err + } + + logger.V(logs.LogDebug).Info("Get Kubeconfig from TokenRequest") + data := getKubeconfigFromToken(namespace, serviceAccountName, tokenRequest.Token) + //nolint: forbidigo // print kubeconfig + fmt.Println(data) + + return nil +} + +func getNamespace(ctx context.Context, name string) error { + instance := utils.GetAccessInstance() + currentNs := &corev1.Namespace{} + return instance.GetClient().Get(ctx, types.NamespacedName{Name: name}, currentNs) +} + +func createNamespace(ctx context.Context, name string, logger logr.Logger) error { + logger.V(logs.LogDebug).Info(fmt.Sprintf("Create namespace %s", name)) + currentNs := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + instance := utils.GetAccessInstance() + err := instance.GetClient().Create(ctx, currentNs) + if err != nil && !apierrors.IsAlreadyExists(err) { + logger.V(logs.LogDebug).Info(fmt.Sprintf("Failed to create Namespace %s: %v", + name, err)) + return err + } + + return nil +} + +func getServiceAccount(ctx context.Context, namespace, name string) error { + instance := utils.GetAccessInstance() + currentSA := &corev1.ServiceAccount{} + return instance.GetClient().Get(ctx, + types.NamespacedName{Namespace: namespace, Name: name}, + currentSA) +} + +func createServiceAccount(ctx context.Context, namespace, name string, + logger logr.Logger) error { + + logger.V(logs.LogDebug).Info(fmt.Sprintf("Create serviceAccount %s/%s", namespace, name)) + currentSA := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } + instance := utils.GetAccessInstance() + err := instance.GetClient().Create(ctx, currentSA) + if err != nil && !apierrors.IsAlreadyExists(err) { + logger.V(logs.LogDebug).Info(fmt.Sprintf("Failed to create ServiceAccount %s/%s: %v", + namespace, name, err)) + return err + } + + return nil +} + +func createClusterRole(ctx context.Context, clusterRoleName string, logger logr.Logger) error { + instance := utils.GetAccessInstance() + + logger.V(logs.LogDebug).Info(fmt.Sprintf("Create ClusterRole %s", clusterRoleName)) + // Extends permission in addon-controller-role-extra + clusterrole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterRoleName, + }, + Rules: []rbacv1.PolicyRule{ + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + { + Verbs: []string{"*"}, + NonResourceURLs: []string{"*"}, + }, + }, + } + + err := instance.GetClient().Create(ctx, clusterrole) + if err != nil && !apierrors.IsAlreadyExists(err) { + logger.V(logs.LogDebug).Info(fmt.Sprintf("Failed to create ClusterRole %s: %v", + clusterRoleName, err)) + return err + } + + return nil +} + +func createClusterRoleBinding(ctx context.Context, clusterRoleName, clusterRoleBindingName, serviceAccountNamespace, serviceAccountName string, + logger logr.Logger) error { + + instance := utils.GetAccessInstance() + + logger.V(logs.LogDebug).Info(fmt.Sprintf("Create ClusterRoleBinding %s", clusterRoleBindingName)) + clusterrolebinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterRoleBindingName, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.SchemeGroupVersion.Group, + Kind: "ClusterRole", + Name: clusterRoleName, + }, + Subjects: []rbacv1.Subject{ + { + Namespace: serviceAccountNamespace, + Name: serviceAccountName, + Kind: "ServiceAccount", + APIGroup: corev1.SchemeGroupVersion.Group, + }, + }, + } + err := instance.GetClient().Create(ctx, clusterrolebinding) + if err != nil && !apierrors.IsAlreadyExists(err) { + logger.V(logs.LogDebug).Info(fmt.Sprintf("Failed to create clusterrolebinding %s: %v", + clusterRoleBindingName, err)) + return err + } + + return nil +} + +// getServiceAccountTokenRequest returns token for a serviceaccount +func getServiceAccountTokenRequest(ctx context.Context, serviceAccountNamespace, serviceAccountName string, + logger logr.Logger) (*authenticationv1.TokenRequestStatus, error) { + + instance := utils.GetAccessInstance() + + expiration := int64(saExpirationInSecond.Seconds()) + + treq := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + ExpirationSeconds: &expiration, + }, + } + + clientset, err := kubernetes.NewForConfig(instance.GetConfig()) + if err != nil { + return nil, err + } + + logger.V(logs.LogDebug).Info( + fmt.Sprintf("Create Token for ServiceAccount %s/%s", serviceAccountNamespace, serviceAccountName)) + var tokenRequest *authenticationv1.TokenRequest + tokenRequest, err = clientset.CoreV1().ServiceAccounts(serviceAccountNamespace). + CreateToken(ctx, serviceAccountName, treq, metav1.CreateOptions{}) + if err != nil { + logger.V(logs.LogDebug).Info( + fmt.Sprintf("Failed to create token for ServiceAccount %s/%s: %v", + serviceAccountNamespace, serviceAccountName, err)) + return nil, err + } + + return &tokenRequest.Status, nil +} + +// getKubeconfigFromToken returns Kubeconfig to access management cluster from token. +func getKubeconfigFromToken(namespace, serviceAccountName, token string) string { + template := `apiVersion: v1 +kind: Config +clusters: +- name: local + cluster: + server: %s + certificate-authority-data: "%s" +users: +- name: %s + user: + token: %s +contexts: +- name: sveltos-context + context: + cluster: local + namespace: %s + user: %s +current-context: sveltos-context` + + instance := utils.GetAccessInstance() + + data := fmt.Sprintf(template, instance.GetConfig().Host, + base64.StdEncoding.EncodeToString(instance.GetConfig().CAData), serviceAccountName, token, namespace, serviceAccountName) + + return data +} + +// GenerateKubeconfig creates a TokenRequest and a Kubeconfig associated with it +func GenerateKubeconfig(ctx context.Context, args []string, logger logr.Logger) error { + doc := `Usage: + sveltosctl generate kubeconfig [options] [--namespace=] [--serviceaccount=] [--create] [--verbose] + + --namespace= The namespace of the ServiceAccount. If not specified, projectsveltos namespace will be used. + --serviceaccount= The name of the ServiceAccount. If not specified, projectsveltos will be used. + --create If a ServiceAccount with enough permissions is already present, do not set this flag. + Sveltos will generate a Kubeconfig associated to that ServiceAccount. + If a ServiceAccount with cluster admin permissions needs to be created, use this option. + When this option is set, this command will create necessary resources: + 1. namespace if not existing already + 2. serviceAccount if not existing already + 3. ClusterRole with cluster admin permission + 4. ClusterRoleBinding granting the serviceAccount cluster admin permissions + 5. TokenRequest for the ServiceAccount + +Options: + -h --help Show this screen. + --verbose Verbose mode. Print each step. + +Description: + The generate kubeconfig command will generate a Kubeconfig that can later on be used to register the cluster. +` + parsedArgs, err := docopt.ParseArgs(doc, nil, "1.0") + if err != nil { + logger.V(logs.LogInfo).Error(err, "failed to parse args") + return fmt.Errorf( + "invalid option: 'sveltosctl %s'. Use flag '--help' to read about a specific subcommand. Error: %w", + strings.Join(args, " "), + err, + ) + } + if len(parsedArgs) == 0 { + return nil + } + + _ = flag.Lookup("v").Value.Set(fmt.Sprint(logs.LogInfo)) + verbose := parsedArgs["--verbose"].(bool) + if verbose { + err = flag.Lookup("v").Value.Set(fmt.Sprint(logs.LogDebug)) + if err != nil { + return err + } + } + + namespace := projectsveltos + if passedNamespace := parsedArgs["--namespace"]; passedNamespace != nil { + namespace = passedNamespace.(string) + } + + serviceAccount := projectsveltos + if passedServiceAccount := parsedArgs["--serviceaccount"]; passedServiceAccount != nil { + serviceAccount = passedServiceAccount.(string) + } + + create := parsedArgs["--create"].(bool) + + return generateKubeconfigForServiceAccount(ctx, namespace, serviceAccount, create, logger) +} diff --git a/internal/commands/generate/generate_kubeconfig_test.go b/internal/commands/generate/generate_kubeconfig_test.go new file mode 100644 index 00000000..2be61ba9 --- /dev/null +++ b/internal/commands/generate/generate_kubeconfig_test.go @@ -0,0 +1,95 @@ +/* +Copyright 2023. projectsveltos.io. All rights reserved. + +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 generate_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2/textlogger" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/projectsveltos/sveltosctl/internal/commands/generate" + "github.com/projectsveltos/sveltosctl/internal/utils" +) + +var _ = Describe("Register Mgmt Cluster", func() { + It("createNamespace creates namespace", func() { + scheme, err := utils.GetScheme() + Expect(err).To(BeNil()) + c := fake.NewClientBuilder().WithScheme(scheme).Build() + utils.InitalizeManagementClusterAcces(scheme, nil, nil, c) + + ns := randomString() + Expect(generate.CreateNamespace(context.TODO(), ns, + textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) + + currentNs := &corev1.Namespace{} + Expect(c.Get(context.TODO(), types.NamespacedName{Name: ns}, currentNs)).To(BeNil()) + }) + + It("createClusterRole creates ClusterRole", func() { + scheme, err := utils.GetScheme() + Expect(err).To(BeNil()) + c := fake.NewClientBuilder().WithScheme(scheme).Build() + utils.InitalizeManagementClusterAcces(scheme, nil, nil, c) + + Expect(generate.CreateClusterRole(context.TODO(), generate.Projectsveltos, + textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) + + currentClusterRole := &rbacv1.ClusterRole{} + Expect(c.Get(context.TODO(), types.NamespacedName{Name: generate.Projectsveltos}, + currentClusterRole)).To(Succeed()) + + Expect(generate.CreateClusterRole(context.TODO(), generate.Projectsveltos, + textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) + }) + + It("createClusterRoleBinding creates ClusterRoleBinding", func() { + scheme, err := utils.GetScheme() + Expect(err).To(BeNil()) + c := fake.NewClientBuilder().WithScheme(scheme).Build() + utils.InitalizeManagementClusterAcces(scheme, nil, nil, c) + + saNamespace := randomString() + saName := randomString() + Expect(generate.CreateClusterRoleBinding(context.TODO(), generate.Projectsveltos, + generate.Projectsveltos, saNamespace, saName, + textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) + + currentClusterRoleBinding := &rbacv1.ClusterRoleBinding{} + Expect(c.Get(context.TODO(), types.NamespacedName{Name: generate.Projectsveltos}, + currentClusterRoleBinding)).To(Succeed()) + + Expect(currentClusterRoleBinding.RoleRef.Kind).To(Equal("ClusterRole")) + Expect(currentClusterRoleBinding.RoleRef.Name).To(Equal(generate.Projectsveltos)) + + Expect(len(currentClusterRoleBinding.Subjects)).To(Equal(1)) + Expect(currentClusterRoleBinding.Subjects[0].Name).To(Equal(saName)) + Expect(currentClusterRoleBinding.Subjects[0].Namespace).To(Equal(saNamespace)) + Expect(currentClusterRoleBinding.Subjects[0].Kind).To(Equal("ServiceAccount")) + + Expect(generate.CreateClusterRoleBinding(context.TODO(), generate.Projectsveltos, + generate.Projectsveltos, saNamespace, saName, + textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) + }) +}) diff --git a/internal/commands/generate/generate_suite_test.go b/internal/commands/generate/generate_suite_test.go new file mode 100644 index 00000000..bf75d19b --- /dev/null +++ b/internal/commands/generate/generate_suite_test.go @@ -0,0 +1,35 @@ +/* +Copyright 2024. projectsveltos.io. All rights reserved. + +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 generate_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/cluster-api/util" +) + +func TestShow(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Show Suite") +} + +func randomString() string { + const length = 10 + return util.RandomString(length) +} diff --git a/internal/commands/onboard/export_test.go b/internal/commands/onboard/export_test.go index eef9ae15..6218c21b 100644 --- a/internal/commands/onboard/export_test.go +++ b/internal/commands/onboard/export_test.go @@ -25,16 +25,5 @@ const ( ) var ( - CreateSveltosCluster = createSveltosCluster - CreateNamespace = createNamespace - CreateClusterRole = createClusterRole - CreateClusterRoleBinding = createClusterRoleBinding -) - -const ( - ClusterRoleName = clusterRoleName - ClusterRoleBindingName = clusterRoleBindingName - SaName = saName - SaNamespace = saNamespace - DefaultNamespace = defaultNamespace + CreateSveltosCluster = createSveltosCluster ) diff --git a/internal/commands/onboard/mgmt_cluster.go b/internal/commands/onboard/mgmt_cluster.go deleted file mode 100644 index 5f3bcf22..00000000 --- a/internal/commands/onboard/mgmt_cluster.go +++ /dev/null @@ -1,392 +0,0 @@ -/* -Copyright 2022. projectsveltos.io. All rights reserved. - -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 onboard - -import ( - "context" - "encoding/base64" - "flag" - "fmt" - "os" - "strings" - "time" - - "github.com/docopt/docopt-go" - "github.com/go-logr/logr" - authenticationv1 "k8s.io/api/authentication/v1" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" - - libsveltosv1alpha1 "github.com/projectsveltos/libsveltos/api/v1alpha1" - "github.com/projectsveltos/libsveltos/lib/clusterproxy" - logs "github.com/projectsveltos/libsveltos/lib/logsettings" - "github.com/projectsveltos/sveltosctl/internal/utils" -) - -const ( - defaultNamespace = "mgmt" - defaultName = "mgmt" - clusterRoleName = "mgmt-role" - clusterRoleBindingName = "mgmt-role-binding" - saName = "sveltos" - saNamespace = "projectsveltos" - // expirationInSecond is the token expiration time. - saExpirationInSecond = 365 * 24 * 60 * time.Minute -) - -func createNamespace(ctx context.Context, clusterNamespace string, logger logr.Logger) error { - if clusterNamespace != defaultNamespace { - return nil // only if management cluster needs to be registered in the defaultNamespace - // namespace will be created - } - - instance := utils.GetAccessInstance() - - currentNs := &corev1.Namespace{} - err := instance.GetClient().Get(ctx, types.NamespacedName{Name: clusterNamespace}, currentNs) - if err == nil { - return nil - } - - if apierrors.IsNotFound(err) { - logger.V(logs.LogDebug).Info(fmt.Sprintf("Create namespace %s", clusterNamespace)) - // If namespace defaultNamespace does not exist, create it - currentNs = &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: defaultNamespace, - }, - } - - return instance.GetClient().Create(ctx, currentNs) - } - - return err -} - -func createSecretWithKubeconfig(ctx context.Context, clusterNamespace, clusterName, kubeconfigPath string, - logger logr.Logger) error { - - instance := utils.GetAccessInstance() - - secretName := clusterName + sveltosKubeconfigSecretNamePostfix - logger.V(logs.LogDebug).Info( - fmt.Sprintf("Verifying Secret %s/%s does not exist already", clusterNamespace, secretName)) - - secret := &corev1.Secret{} - err := instance.GetResource(ctx, - types.NamespacedName{Namespace: clusterNamespace, Name: secretName}, secret) - if err != nil && !apierrors.IsNotFound(err) { - return err - } - - // Read file - _, err = os.ReadFile(kubeconfigPath) - if err != nil { - return err - } - - return createSecret(ctx, clusterNamespace, secretName, kubeconfigPath, logger) -} - -// getServiceAccountToken returns token for a serviceaccount -func getServiceAccountToken(ctx context.Context, - logger logr.Logger) (*authenticationv1.TokenRequestStatus, error) { - - instance := utils.GetAccessInstance() - - expiration := int64(saExpirationInSecond.Seconds()) - - treq := &authenticationv1.TokenRequest{ - Spec: authenticationv1.TokenRequestSpec{ - ExpirationSeconds: &expiration, - }, - } - - clientset, err := kubernetes.NewForConfig(instance.GetConfig()) - if err != nil { - return nil, err - } - - logger.V(logs.LogDebug).Info( - fmt.Sprintf("Create Token for ServiceAccount %s/%s", saNamespace, saName)) - var tokenRequest *authenticationv1.TokenRequest - tokenRequest, err = clientset.CoreV1().ServiceAccounts(saNamespace). - CreateToken(ctx, saName, treq, metav1.CreateOptions{}) - if err != nil { - logger.V(logs.LogDebug).Info( - fmt.Sprintf("Failed to create token for ServiceAccount %s/%s: %v", - saNamespace, saName, err)) - return nil, err - } - - return &tokenRequest.Status, nil -} - -// getKubeconfigFromToken returns Kubeconfig to access management cluster from token. -func getKubeconfigFromToken(token string) string { - template := `apiVersion: v1 -kind: Config -clusters: -- name: local - cluster: - server: %s - certificate-authority-data: "%s" -users: -- name: sveltos - user: - token: %s -contexts: -- name: sveltos-context - context: - cluster: local - user: %s -current-context: sveltos-context` - - instance := utils.GetAccessInstance() - - data := fmt.Sprintf(template, instance.GetConfig().Host, - base64.StdEncoding.EncodeToString(instance.GetConfig().CAData), token, saName) - - return data -} - -func createClusterRole(ctx context.Context, logger logr.Logger) error { - instance := utils.GetAccessInstance() - - logger.V(logs.LogDebug).Info(fmt.Sprintf("Create ClusterRole %s", clusterRoleName)) - // Extends permission in addon-controller-role-extra - clusterrole := &rbacv1.ClusterRole{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterRoleName, - }, - Rules: []rbacv1.PolicyRule{ - { - Verbs: []string{"*"}, - APIGroups: []string{"*"}, - Resources: []string{"*"}, - }, - { - Verbs: []string{"*"}, - NonResourceURLs: []string{"*"}, - }, - }, - } - - err := instance.GetClient().Create(ctx, clusterrole) - if err != nil && !apierrors.IsAlreadyExists(err) { - logger.V(logs.LogDebug).Info(fmt.Sprintf("Failed to create ClusterRole %s: %v", - clusterRoleName, err)) - return err - } - - return nil -} - -func createClusterRoleBinding(ctx context.Context, logger logr.Logger) error { - instance := utils.GetAccessInstance() - - logger.V(logs.LogDebug).Info(fmt.Sprintf("Create ClusterRoleBinding %s", clusterRoleBindingName)) - clusterrolebinding := &rbacv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterRoleBindingName, - }, - RoleRef: rbacv1.RoleRef{ - APIGroup: rbacv1.SchemeGroupVersion.Group, - Kind: "ClusterRole", - Name: clusterRoleName, - }, - Subjects: []rbacv1.Subject{ - { - Namespace: saNamespace, - Name: saName, - Kind: "ServiceAccount", - APIGroup: corev1.SchemeGroupVersion.Group, - }, - }, - } - err := instance.GetClient().Create(ctx, clusterrolebinding) - if err != nil && !apierrors.IsAlreadyExists(err) { - logger.V(logs.LogDebug).Info(fmt.Sprintf("Failed to create clusterrolebinding %s: %v", - clusterRoleBindingName, err)) - return err - } - - return nil -} - -func createServiceAccount(ctx context.Context, logger logr.Logger) error { - instance := utils.GetAccessInstance() - - logger.V(logs.LogDebug).Info(fmt.Sprintf("Create ServiceAccount %s/%s", saNamespace, saName)) - sa := &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: saNamespace, - Name: saName, - }, - } - - // Create ServiceAccount - err := instance.GetClient().Create(ctx, sa) - if err != nil && !apierrors.IsAlreadyExists(err) { - logger.V(logs.LogDebug).Info(fmt.Sprintf("Failed to created ServiceAccount %s/%s: %v", - saNamespace, saName, err)) - return err - } - - return nil -} - -// grantSveltosClusterAdminRole grants Sveltos * permissions. -// Sveltos addon-controller serviceAccount is tied to addon-controller-role-extra -// ClusterRole. -// This command extends permission in such cluster. Then takes the kubeconfig associated -// with the Sveltos addon-controller serviceAccount and store in the Secret for the -// SveltosCluster corresponding to the management cluster. -func grantSveltosClusterAdminRole(ctx context.Context, clusterNamespace, clusterName string, - logger logr.Logger) error { - - err := createClusterRole(ctx, logger) - if err != nil { - return err - } - - err = createClusterRoleBinding(ctx, logger) - if err != nil { - return err - } - - err = createServiceAccount(ctx, logger) - if err != nil { - return err - } - - token, err := getServiceAccountToken(ctx, logger) - if err != nil { - return err - } - - logger.V(logs.LogDebug).Info("Get Kubeconfig from Token") - data := getKubeconfigFromToken(token.Token) - kubeconfig, err := clusterproxy.CreateKubeconfig(logger, []byte(data)) - if err != nil { - return err - } - defer os.Remove(kubeconfig) - - logger.V(logs.LogDebug).Info("Create secret with Kubeconfig") - err = createSecretWithKubeconfig(ctx, clusterNamespace, clusterName, kubeconfig, logger) - if err != nil { - return err - } - - logger.V(logs.LogDebug).Info("Create SveltosCluster") - err = createSveltosCluster(ctx, clusterNamespace, clusterName, logger) - if err != nil { - return err - } - - return nil -} - -func onboardMgmtCluster(ctx context.Context, clusterNamespace, clusterName, kubeconfigPath string, - logger logr.Logger) error { - - err := createNamespace(ctx, clusterNamespace, logger) - if err != nil { - return err - } - - instance := utils.GetAccessInstance() - - logger.V(logs.LogDebug).Info(fmt.Sprintf("Verifying SveltosCluster %s/%s does not exist already", clusterNamespace, clusterName)) - sveltosCluster := &libsveltosv1alpha1.SveltosCluster{} - err = instance.GetResource(ctx, types.NamespacedName{Namespace: clusterNamespace, Name: clusterName}, sveltosCluster) - if err != nil && !apierrors.IsNotFound(err) { - return err - } - - // if Kubeconfig is provided, use it - if kubeconfigPath != "" { - return createSecretWithKubeconfig(ctx, clusterNamespace, clusterName, kubeconfigPath, logger) - } - - // No Kubeconfig provided. Sveltos will be granted cluster-admin role - return grantSveltosClusterAdminRole(ctx, clusterNamespace, clusterName, logger) -} - -// RegisterManagementCluster takes care of creating all necessary internal resources to import a cluster -func RegisterManagementCluster(ctx context.Context, args []string, logger logr.Logger) error { - doc := `Usage: - sveltosctl register mgmt-cluster [options] [--namespace=] [--cluster=] [--kubeconfig=] [--verbose] - - --namespace= The namespace where SveltosCluster will be created. By default "mgmt" will be used. - --cluster= The name of the SveltosCluster. By default "mgmt" will be used. - --kubeconfig= Path of the file containing the cluster kubeconfig. If not provided, Sveltos - will be given cluster-admin access to the management cluster. - If kubeconfig is not passed, run this command only while using sveltosctl as binary. - Sveltosctl as pod does not have enough permission to execute necessary code. - -Options: - -h --help Show this screen. - --verbose Verbose mode. Print each step. - -Description: - The register mgmt-cluster command registers the management cluster as a cluster to be managed by for Sveltos. -` - parsedArgs, err := docopt.ParseArgs(doc, nil, "1.0") - if err != nil { - logger.V(logs.LogInfo).Error(err, "failed to parse args") - return fmt.Errorf( - "invalid option: 'sveltosctl %s'. Use flag '--help' to read about a specific subcommand. Error: %w", - strings.Join(args, " "), - err, - ) - } - if len(parsedArgs) == 0 { - return nil - } - - _ = flag.Lookup("v").Value.Set(fmt.Sprint(logs.LogInfo)) - verbose := parsedArgs["--verbose"].(bool) - if verbose { - err = flag.Lookup("v").Value.Set(fmt.Sprint(logs.LogDebug)) - if err != nil { - return err - } - } - - namespace := defaultNamespace - if passedNamespace := parsedArgs["--namespace"]; passedNamespace != nil { - namespace = passedNamespace.(string) - } - - cluster := defaultName - if passedCluster := parsedArgs["--cluster"]; passedCluster != nil { - cluster = passedCluster.(string) - } - - kubeconfig := "" - if passedKubeconfig := parsedArgs["--kubeconfig"]; passedKubeconfig != nil { - kubeconfig = passedKubeconfig.(string) - } - - return onboardMgmtCluster(ctx, namespace, cluster, kubeconfig, logger) -} diff --git a/internal/commands/onboard/mgmt_cluster_test.go b/internal/commands/onboard/mgmt_cluster_test.go deleted file mode 100644 index 26df5349..00000000 --- a/internal/commands/onboard/mgmt_cluster_test.go +++ /dev/null @@ -1,135 +0,0 @@ -/* -Copyright 2022. projectsveltos.io. All rights reserved. - -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 onboard_test - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/klog/v2/textlogger" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - libsveltosv1alpha1 "github.com/projectsveltos/libsveltos/api/v1alpha1" - "github.com/projectsveltos/sveltosctl/internal/commands/onboard" - "github.com/projectsveltos/sveltosctl/internal/utils" -) - -var _ = Describe("Register Mgmt Cluster", func() { - It("createSveltosCluster creates SveltosCluster", func() { - scheme, err := utils.GetScheme() - Expect(err).To(BeNil()) - - clusterNamespace := randomString() - clusterName := randomString() - - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterNamespace, - }, - } - - initObjects := []client.Object{ns} - c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjects...).Build() - utils.InitalizeManagementClusterAcces(scheme, nil, nil, c) - - Expect(onboard.CreateSveltosCluster(context.TODO(), clusterNamespace, clusterName, - textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) - - currentSveltosCluster := &libsveltosv1alpha1.SveltosCluster{} - Expect(c.Get(context.TODO(), - types.NamespacedName{Namespace: clusterNamespace, Name: clusterName}, - currentSveltosCluster)).To(Succeed()) - - Expect(onboard.CreateSveltosCluster(context.TODO(), clusterNamespace, clusterName, - textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) - }) - - It("createNamespace creates namespace", func() { - scheme, err := utils.GetScheme() - Expect(err).To(BeNil()) - c := fake.NewClientBuilder().WithScheme(scheme).Build() - utils.InitalizeManagementClusterAcces(scheme, nil, nil, c) - - ns := randomString() - Expect(onboard.CreateNamespace(context.TODO(), ns, - textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) - - currentNs := &corev1.Namespace{} - err = c.Get(context.TODO(), types.NamespacedName{Name: ns}, currentNs) - // Only defaultNamespace is created. Any other namespace is expected to be created - // by admin as per any other cluster registration - Expect(apierrors.IsNotFound(err)).To(BeTrue()) - - Expect(onboard.CreateNamespace(context.TODO(), onboard.DefaultNamespace, - textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) - Expect(c.Get(context.TODO(), - types.NamespacedName{Name: onboard.DefaultNamespace}, currentNs)).To(Succeed()) - - Expect(onboard.CreateNamespace(context.TODO(), onboard.DefaultNamespace, - textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) - }) - - It("createClusterRole creates ClusterRole", func() { - scheme, err := utils.GetScheme() - Expect(err).To(BeNil()) - c := fake.NewClientBuilder().WithScheme(scheme).Build() - utils.InitalizeManagementClusterAcces(scheme, nil, nil, c) - - Expect(onboard.CreateClusterRole(context.TODO(), - textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) - - currentClusterRole := &rbacv1.ClusterRole{} - Expect(c.Get(context.TODO(), types.NamespacedName{Name: onboard.ClusterRoleName}, - currentClusterRole)).To(Succeed()) - - Expect(onboard.CreateClusterRole(context.TODO(), - textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) - }) - - It("createClusterRoleBinding creates ClusterRoleBinding", func() { - scheme, err := utils.GetScheme() - Expect(err).To(BeNil()) - c := fake.NewClientBuilder().WithScheme(scheme).Build() - utils.InitalizeManagementClusterAcces(scheme, nil, nil, c) - - Expect(onboard.CreateClusterRoleBinding(context.TODO(), - textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) - - currentClusterRoleBinding := &rbacv1.ClusterRoleBinding{} - Expect(c.Get(context.TODO(), types.NamespacedName{Name: onboard.ClusterRoleBindingName}, - currentClusterRoleBinding)).To(Succeed()) - - Expect(currentClusterRoleBinding.RoleRef.Kind).To(Equal("ClusterRole")) - Expect(currentClusterRoleBinding.RoleRef.Name).To(Equal(onboard.ClusterRoleName)) - - Expect(len(currentClusterRoleBinding.Subjects)).To(Equal(1)) - Expect(currentClusterRoleBinding.Subjects[0].Name).To(Equal(onboard.SaName)) - Expect(currentClusterRoleBinding.Subjects[0].Namespace).To(Equal(onboard.SaNamespace)) - Expect(currentClusterRoleBinding.Subjects[0].Kind).To(Equal("ServiceAccount")) - - Expect(onboard.CreateClusterRoleBinding(context.TODO(), - textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) - }) -}) diff --git a/internal/commands/register_cluster.go b/internal/commands/register_cluster.go index 33b0d124..c3e2fe05 100644 --- a/internal/commands/register_cluster.go +++ b/internal/commands/register_cluster.go @@ -36,7 +36,6 @@ func RegisterCluster(ctx context.Context, args []string, logger logr.Logger) err sveltosctl register [...] cluster Imports a non CAPI cluster to be managed by Sveltos. - mgmt-cluster Registers management cluster to be managed by Sveltos. Options: -h --help Show this screen. @@ -69,8 +68,6 @@ Description: switch command { case "cluster": return onboard.RegisterCluster(ctx, arguments, logger) - case "mgmt-cluster": - return onboard.RegisterManagementCluster(ctx, arguments, logger) default: //nolint: forbidigo // print doc fmt.Println(doc)