From 30b3c1e53d1e88f105f53f85bdafd9f022153539 Mon Sep 17 00:00:00 2001 From: mgianluc Date: Wed, 17 Apr 2024 10:19:58 +0200 Subject: [PATCH] Allow to register a cluster with one command The sveltosctl register command is used to register a cluster. It requires a Kubeconfig to be passed. If user has a kubeconfig with multiple contexts, the option __fleet-cluster-context__ allows to specify the context for the cluster to be managed. So with default context pointing to the management cluster, following command will: 1. create a ServiceAccount in the managed cluster (using cluster-1 context) 2. grant this ServiceAccount cluster-admin permission 3. create a TokenRequest for such account and a Kubeconfig with bearer token from the TokenRequest 4. create a SveltosCluster in the management cluster (so using default context) and a Secret with kubeconfig generated in the step above ``` sveltosctl register cluster --namespace=gcp --cluster=cluster-1 --fleet-cluster-context=cluster-1 --labels=k1=v1,k2=v2 ``` --- README.md | 16 ++ go.mod | 8 +- go.sum | 8 +- internal/commands/generate/export_test.go | 4 - .../commands/generate/generate_kubeconfig.go | 41 ++--- internal/commands/onboard/cluster.go | 155 +++++++++++++++--- internal/commands/onboard/cluster_test.go | 18 +- 7 files changed, 183 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index d24f15b4..8cae428d 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,22 @@ Usage: --profile= Show addons deployed because of this clusterprofile/profile. If not specified all clusterprofiles/profiles are considered. ``` +## Register a cluster + +If there is kubeconfig with multiple contexts, the option __fleet-cluster-context__ +allows to specify the context for the cluster to be managed. + +So with default context pointing to the management cluster, following command will: +1. create a ServiceAccount in the managed cluster (using cluster-1 context) +2. grant this ServiceAccount cluster-admin permission +3. create a TokenRequest for such account and a Kubeconfig with bearer token from the TokenRequest +4. create a SveltosCluster in the management cluster (so using default context) and a Secret +with kubeconfig generated in the step above + +``` +sveltosctl register cluster --namespace=gcp --cluster=cluster-1 --fleet-cluster-context=cluster-1 --labels=k1=v1,k2=v2 +``` + ## Display information about resources in managed cluster **show resources** looks at all the HealthCheckReport instances and display information about those. diff --git a/go.mod b/go.mod index dbb8bb35..3aa6a662 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/projectsveltos/sveltosctl -go 1.21 +go 1.22.0 + +toolchain go1.22.2 require ( github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 @@ -14,7 +16,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/projectsveltos/addon-controller v0.27.1-0.20240405170833-7fa049caf6a3 github.com/projectsveltos/event-manager v0.26.1-0.20240315124018-bf7536defbe2 - github.com/projectsveltos/libsveltos v0.27.1-0.20240405132615-9e1a36ca5c8f + github.com/projectsveltos/libsveltos v0.27.1-0.20240414121914-1eb0b89fc6c9 github.com/robfig/cron/v3 v3.0.1 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.29.3 @@ -24,7 +26,7 @@ require ( k8s.io/klog/v2 v2.120.1 k8s.io/kubectl v0.29.3 sigs.k8s.io/cluster-api v1.6.3 - sigs.k8s.io/controller-runtime v0.17.2 + sigs.k8s.io/controller-runtime v0.17.3 sigs.k8s.io/yaml v1.4.0 ) diff --git a/go.sum b/go.sum index 0bb36976..3104809a 100644 --- a/go.sum +++ b/go.sum @@ -336,8 +336,8 @@ github.com/projectsveltos/addon-controller v0.27.1-0.20240405170833-7fa049caf6a3 github.com/projectsveltos/addon-controller v0.27.1-0.20240405170833-7fa049caf6a3/go.mod h1:i9dFWaOrVzQSPXyACKxBkcgq3eH8TmlGw0DIFvWizVI= github.com/projectsveltos/event-manager v0.26.1-0.20240315124018-bf7536defbe2 h1:vbwFY8ag5X+yJ42q88Q/dyWfodpRtExhdSrNRYtmMVs= github.com/projectsveltos/event-manager v0.26.1-0.20240315124018-bf7536defbe2/go.mod h1:jsdBg7hvxOqX0dwtgffrodJWKDKVttX/GYp2+fWsL+U= -github.com/projectsveltos/libsveltos v0.27.1-0.20240405132615-9e1a36ca5c8f h1:FCmjCYxO/5irmocHXcuVfg9fT74NoN1G4k64/SwWdMo= -github.com/projectsveltos/libsveltos v0.27.1-0.20240405132615-9e1a36ca5c8f/go.mod h1:Uq3KYj5LKQYttA3yVb0O/V5Uvi2Qy7B23tnB5fBAMFg= +github.com/projectsveltos/libsveltos v0.27.1-0.20240414121914-1eb0b89fc6c9 h1:RYqAVU6cXVWniqDJGuWRFOzA5xgH3zasf2O11W7hYNw= +github.com/projectsveltos/libsveltos v0.27.1-0.20240414121914-1eb0b89fc6c9/go.mod h1:DVMv0DUlZ2x21jq7Z+LJvs7HesSe/pWsHZgQLV3Mhjw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= @@ -587,8 +587,8 @@ oras.land/oras-go v1.2.4 h1:djpBY2/2Cs1PV87GSJlxv4voajVOMZxqqtq9AB8YNvY= oras.land/oras-go v1.2.4/go.mod h1:DYcGfb3YF1nKjcezfX2SNlDAeQFKSXmf+qrFmrh4324= sigs.k8s.io/cluster-api v1.6.3 h1:VOlPNg92PQLlhBVLc5pg+cbAuPvGOOBujeFLk9zgnoo= sigs.k8s.io/cluster-api v1.6.3/go.mod h1:4FzfgPPiYaFq8X9F9j2SvmggH/4OOLEDgVJuWDqKLig= -sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= -sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/controller-runtime v0.17.3 h1:65QmN7r3FWgTxDMz9fvGnO1kbf2nu+acg9p2R9oYYYk= +sigs.k8s.io/controller-runtime v0.17.3/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.16.0 h1:/zAR4FOQDCkgSDmVzV2uiFbuy9bhu3jEzthrHCuvm1g= diff --git a/internal/commands/generate/export_test.go b/internal/commands/generate/export_test.go index bc94cb43..87c42a38 100644 --- a/internal/commands/generate/export_test.go +++ b/internal/commands/generate/export_test.go @@ -16,10 +16,6 @@ limitations under the License. package generate -const ( - Projectsveltos = projectsveltos -) - var ( CreateNamespace = createNamespace CreateClusterRole = createClusterRole diff --git a/internal/commands/generate/generate_kubeconfig.go b/internal/commands/generate/generate_kubeconfig.go index af7d978c..a4899eab 100644 --- a/internal/commands/generate/generate_kubeconfig.go +++ b/internal/commands/generate/generate_kubeconfig.go @@ -39,45 +39,47 @@ import ( ) const ( - projectsveltos = "projectsveltos" + Projectsveltos = "projectsveltos" ) -func generateKubeconfigForServiceAccount(ctx context.Context, namespace, serviceAccountName string, - expirationSeconds int, create bool, logger logr.Logger) error { +func GenerateKubeconfigForServiceAccount(ctx context.Context, namespace, serviceAccountName string, + expirationSeconds int, create, display bool, logger logr.Logger) (string, error) { if create { if err := createNamespace(ctx, namespace, logger); err != nil { - return err + return "", err } if err := createServiceAccount(ctx, namespace, serviceAccountName, logger); err != nil { - return err + return "", err } - if err := createClusterRole(ctx, projectsveltos, 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 + if err := createClusterRoleBinding(ctx, Projectsveltos, Projectsveltos, namespace, serviceAccountName, logger); err != nil { + return "", err } } else { if err := getNamespace(ctx, namespace); err != nil { - return err + return "", err } if err := getServiceAccount(ctx, namespace, serviceAccountName); err != nil { - return err + return "", err } } tokenRequest, err := getServiceAccountTokenRequest(ctx, namespace, serviceAccountName, expirationSeconds, logger) if err != nil { - return err + return "", err } logger.V(logs.LogDebug).Info("Get Kubeconfig from TokenRequest") data := getKubeconfigFromToken(namespace, serviceAccountName, tokenRequest.Token) - //nolint: forbidigo // print kubeconfig - fmt.Println(data) + if display { + //nolint: forbidigo // print kubeconfig + fmt.Println(data) + } - return nil + return data, nil } func getNamespace(ctx context.Context, name string) error { @@ -325,12 +327,12 @@ or create a new one with the necessary permissions. } } - namespace := projectsveltos + namespace := Projectsveltos if passedNamespace := parsedArgs["--namespace"]; passedNamespace != nil { namespace = passedNamespace.(string) } - serviceAccount := projectsveltos + serviceAccount := Projectsveltos if passedServiceAccount := parsedArgs["--serviceaccount"]; passedServiceAccount != nil { serviceAccount = passedServiceAccount.(string) } @@ -345,6 +347,7 @@ or create a new one with the necessary permissions. create := parsedArgs["--create"].(bool) - return generateKubeconfigForServiceAccount(ctx, namespace, serviceAccount, expirationSeconds, - create, logger) + _, err = GenerateKubeconfigForServiceAccount(ctx, namespace, serviceAccount, expirationSeconds, + create, true, logger) + return err } diff --git a/internal/commands/onboard/cluster.go b/internal/commands/onboard/cluster.go index 09d7b92d..1ffd8e95 100644 --- a/internal/commands/onboard/cluster.go +++ b/internal/commands/onboard/cluster.go @@ -22,15 +22,19 @@ import ( "fmt" "os" "strings" + "time" "github.com/docopt/docopt-go" "github.com/go-logr/logr" corev1 "k8s.io/api/core/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/tools/clientcmd" libsveltosv1alpha1 "github.com/projectsveltos/libsveltos/api/v1alpha1" logs "github.com/projectsveltos/libsveltos/lib/logsettings" + "github.com/projectsveltos/sveltosctl/internal/commands/generate" "github.com/projectsveltos/sveltosctl/internal/utils" ) @@ -40,8 +44,8 @@ const ( kubeconfig = "kubeconfig" ) -func onboardSveltosCluster(ctx context.Context, clusterNamespace, clusterName, kubeconfigPath string, - labels map[string]string, logger logr.Logger) error { +func onboardSveltosCluster(ctx context.Context, clusterNamespace, clusterName string, kubeconfigData []byte, + labels map[string]string, renew bool, logger logr.Logger) error { instance := utils.GetAccessInstance() @@ -53,22 +57,16 @@ func onboardSveltosCluster(ctx context.Context, clusterNamespace, clusterName, k return err } - // Read file - _, err = os.ReadFile(kubeconfigPath) + err = patchSveltosCluster(ctx, clusterNamespace, clusterName, labels, renew, logger) if err != nil { return err } - err = patchSveltosCluster(ctx, clusterNamespace, clusterName, labels, logger) - if err != nil { - return err - } - - return patchSecret(ctx, clusterNamespace, secretName, kubeconfigPath, logger) + return patchSecret(ctx, clusterNamespace, secretName, kubeconfigData, logger) } func patchSveltosCluster(ctx context.Context, clusterNamespace, clusterName string, - labels map[string]string, logger logr.Logger) error { + labels map[string]string, renew bool, logger logr.Logger) error { instance := utils.GetAccessInstance() @@ -81,6 +79,12 @@ func patchSveltosCluster(ctx context.Context, clusterNamespace, clusterName stri currentSveltosCluster.Namespace = clusterNamespace currentSveltosCluster.Name = clusterName currentSveltosCluster.Labels = labels + if renew { + currentSveltosCluster.Spec.TokenRequestRenewalOption = &libsveltosv1alpha1.TokenRequestRenewalOption{ + RenewTokenRequestInterval: metav1.Duration{Duration: 24 * time.Hour}, + } + } + return instance.CreateResource(ctx, currentSveltosCluster) } return err @@ -91,23 +95,17 @@ func patchSveltosCluster(ctx context.Context, clusterNamespace, clusterName stri return instance.UpdateResource(ctx, currentSveltosCluster) } -func patchSecret(ctx context.Context, clusterNamespace, secretName, kubeconfigPath string, logger logr.Logger) error { +func patchSecret(ctx context.Context, clusterNamespace, secretName string, kubeconfigData []byte, logger logr.Logger) error { instance := utils.GetAccessInstance() - var data []byte - data, err := os.ReadFile(kubeconfigPath) - if err != nil { - return err - } - currentSecret := &corev1.Secret{} - err = instance.GetResource(ctx, types.NamespacedName{Namespace: clusterNamespace, Name: secretName}, currentSecret) + err := instance.GetResource(ctx, types.NamespacedName{Namespace: clusterNamespace, Name: secretName}, currentSecret) if err != nil { if apierrors.IsNotFound(err) { logger.V(logs.LogDebug).Info(fmt.Sprintf("Creating Secret %s/%s", clusterNamespace, secretName)) currentSecret.Namespace = clusterNamespace currentSecret.Name = secretName - currentSecret.Data = map[string][]byte{kubeconfig: data} + currentSecret.Data = map[string][]byte{kubeconfig: kubeconfigData} return instance.CreateResource(ctx, currentSecret) } return err @@ -115,7 +113,7 @@ func patchSecret(ctx context.Context, clusterNamespace, secretName, kubeconfigPa logger.V(logs.LogDebug).Info(fmt.Sprintf("Updating Secret %s/%s", clusterNamespace, secretName)) currentSecret.Data = map[string][]byte{ - kubeconfig: data, + kubeconfig: kubeconfigData, } return instance.UpdateResource(ctx, currentSecret) @@ -124,14 +122,23 @@ func patchSecret(ctx context.Context, clusterNamespace, secretName, kubeconfigPa // RegisterCluster takes care of creating all necessary internal resources to import a cluster func RegisterCluster(ctx context.Context, args []string, logger logr.Logger) error { doc := `Usage: - sveltosctl register cluster [options] --namespace= --cluster= --kubeconfig= [--labels=] [--verbose] + sveltosctl register cluster [options] --namespace= --cluster= [--kubeconfig=] [--fleet-cluster-context=] [--labels=] + [--verbose] --namespace= Specifies the namespace where Sveltos will create a resource (SveltosCluster) to represent the registered cluster. --cluster= Defines a name for the registered cluster within Sveltos. - --kubeconfig= Provides the path to a file containing the kubeconfig for the Kubernetes cluster you want to register. + --kubeconfig= (Optional) Provides the path to a file containing the kubeconfig for the Kubernetes cluster you want to register. If you don't have a kubeconfig file yet, you can use the "sveltosctl generate kubeconfig" command. Be sure to point that command to the specific cluster you want to manage. This will help you create the necessary kubeconfig file before registering the cluster with Sveltos. + Either --kubeconfig or --fleet-cluster-context must be provided. + --fleet-cluster-context= (Optional) If your kubeconfig has multiple contexts: + - One context points to the management cluster (default one) + - Another context points to the cluster you actually want to manage; + In this case, you can specify the context name with the --fleet-cluster-context flag. This tells + the command to use the specific context to generate a Kubeconfig Sveltos can use and then create + a SveltosCluster with it so you don't have to provide kubeconfig + Either --kubeconfig or --fleet-cluster-context must be provided. --labels= (Optional) This option allows you to specify labels for the SveltosCluster resource being created. The format for labels is , where each key-value pair is separated by a comma (,) and the key and value are separated by an equal sign (=). You can define multiple labels by adding more key-value pairs @@ -190,7 +197,42 @@ Description: kubeconfig = passedKubeconfig.(string) } - return onboardSveltosCluster(ctx, namespace, cluster, kubeconfig, labels, logger) + renew := false + fleetClusterContext := "" + if passedContext := parsedArgs["--fleet-cluster-context"]; passedContext != nil { + fleetClusterContext = passedContext.(string) + renew = true + } + + if kubeconfig == "" && fleetClusterContext == "" { + return fmt.Errorf("either kubeconfig or fleet-cluster-context must be specified") + } + + data, err := getKubeconfigData(ctx, kubeconfig, fleetClusterContext, logger) + if err != nil { + return err + } + + return onboardSveltosCluster(ctx, namespace, cluster, data, labels, renew, logger) +} + +func getKubeconfigData(ctx context.Context, kubeconfigFile, fleetClusterContext string, logger logr.Logger) ([]byte, error) { + var data []byte + if fleetClusterContext != "" { + kubeconfigData, err := createKubeconfig(ctx, fleetClusterContext, logger) + if err != nil { + return nil, err + } + data = []byte(kubeconfigData) + } else { + var err error + data, err = os.ReadFile(kubeconfigFile) + if err != nil { + return nil, err + } + } + + return data, nil } func stringToMap(data string) (map[string]string, error) { @@ -207,3 +249,68 @@ func stringToMap(data string) (map[string]string, error) { } return result, nil } + +func createKubeconfig(ctx context.Context, fleetClusterContext string, logger logr.Logger) (string, error) { + logger.V(logs.LogDebug).Info("Get current context") + currentContext, err := getCurrentContext() + if err != nil { + return "", err + } + logger.V(logs.LogDebug).Info(fmt.Sprintf("current context %s", currentContext)) + + logger.V(logs.LogDebug).Info(fmt.Sprintf("Switch context to %s", fleetClusterContext)) + err = switchCurrentContext(fleetClusterContext) + if err != nil { + return "", err + } + logger.V(logs.LogDebug).Info(fmt.Sprintf("switched to context %s", fleetClusterContext)) + + logger.V(logs.LogDebug).Info("Generate Kubeconfig") + var data string + data, err = generate.GenerateKubeconfigForServiceAccount(ctx, generate.Projectsveltos, generate.Projectsveltos, 0, true, false, logger) + if err != nil { + return "", err + } + + logger.V(logs.LogDebug).Info(fmt.Sprintf("Reset context to %s", currentContext)) + err = switchCurrentContext(currentContext) + if err != nil { + return "", err + } + logger.V(logs.LogDebug).Info(fmt.Sprintf("switched to context %s", currentContext)) + + return data, nil +} + +func getCurrentContext() (string, error) { + kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}) + config, err := kubeconfig.RawConfig() + if err != nil { + return "", err + } + + return config.CurrentContext, nil +} + +func switchCurrentContext(fleetClusterContext string) error { + kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}) + config, err := kubeconfig.RawConfig() + + if err != nil { + return err + } + + for contextName := range config.Contexts { + if contextName == fleetClusterContext { + config.CurrentContext = fleetClusterContext + err = clientcmd.ModifyConfig(clientcmd.NewDefaultPathOptions(), config, true) + if err != nil { + return fmt.Errorf("error ModifyConfig: %w", err) + } + + return nil + } + } + + return fmt.Errorf("error context %s not found", fleetClusterContext) +} diff --git a/internal/commands/onboard/cluster_test.go b/internal/commands/onboard/cluster_test.go index 92448e3f..5496ae26 100644 --- a/internal/commands/onboard/cluster_test.go +++ b/internal/commands/onboard/cluster_test.go @@ -18,7 +18,6 @@ package onboard_test import ( "context" - "os" "reflect" . "github.com/onsi/ginkgo/v2" @@ -40,14 +39,7 @@ var _ = Describe("OnboardCluster", func() { It("onboardSveltosCluster creates SveltosCluster and Secret", func() { data := randomString() - // Create temp file - kubeconfigFile, err := os.CreateTemp("", "kubeconfig") - Expect(err).To(BeNil()) - - defer os.Remove(kubeconfigFile.Name()) - - _, err = kubeconfigFile.WriteString(data) - Expect(err).To(BeNil()) + kubeconfigData := []byte(data) clusterNamespace := randomString() clusterName := randomString() @@ -64,8 +56,8 @@ var _ = Describe("OnboardCluster", func() { randomString(): randomString(), } - Expect(onboard.OnboardSveltosCluster(context.TODO(), clusterNamespace, clusterName, kubeconfigFile.Name(), - labels, textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) + Expect(onboard.OnboardSveltosCluster(context.TODO(), clusterNamespace, clusterName, kubeconfigData, + labels, false, textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) instance := utils.GetAccessInstance() @@ -89,7 +81,7 @@ var _ = Describe("OnboardCluster", func() { randomString(): randomString(), } - Expect(onboard.OnboardSveltosCluster(context.TODO(), clusterNamespace, clusterName, kubeconfigFile.Name(), - labels, textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) + Expect(onboard.OnboardSveltosCluster(context.TODO(), clusterNamespace, clusterName, kubeconfigData, + labels, false, textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))))).To(Succeed()) }) })