From 3e1b254c3e16f6b721edf4cde5af60d07f42d56e Mon Sep 17 00:00:00 2001 From: hjiajing Date: Thu, 9 Jun 2022 02:06:43 +0800 Subject: [PATCH] multi-cluster bootstrap in antctl (#3474) Add new subcommands to Create or Delete multi-cluster Resources. Signed-off-by: hjiajing --- docs/multicluster/antctl.md | 70 +++++- pkg/antctl/antctl.go | 24 +++ .../raw/multicluster/add/member_cluster.go | 115 ++++++++++ pkg/antctl/raw/multicluster/commands.go | 33 +++ .../raw/multicluster/create/access_token.go | 200 ++++++++++++++++++ .../raw/multicluster/create/clusterclaim.go | 152 +++++++++++++ .../raw/multicluster/create/clusterset.go | 142 +++++++++++++ .../raw/multicluster/delete/clusterclaim.go | 124 +++++++++++ .../raw/multicluster/delete/clusterset.go | 101 +++++++++ .../raw/multicluster/delete/member_cluster.go | 114 ++++++++++ .../raw/multicluster/deploy/deploy_helper.go | 187 ++++++++++++++++ .../raw/multicluster/deploy/leader_cluster.go | 88 ++++++++ .../raw/multicluster/deploy/member_cluster.go | 87 ++++++++ 13 files changed, 1433 insertions(+), 4 deletions(-) create mode 100644 pkg/antctl/raw/multicluster/add/member_cluster.go create mode 100644 pkg/antctl/raw/multicluster/create/access_token.go create mode 100644 pkg/antctl/raw/multicluster/create/clusterclaim.go create mode 100644 pkg/antctl/raw/multicluster/create/clusterset.go create mode 100644 pkg/antctl/raw/multicluster/delete/clusterclaim.go create mode 100644 pkg/antctl/raw/multicluster/delete/clusterset.go create mode 100644 pkg/antctl/raw/multicluster/delete/member_cluster.go create mode 100644 pkg/antctl/raw/multicluster/deploy/deploy_helper.go create mode 100644 pkg/antctl/raw/multicluster/deploy/leader_cluster.go create mode 100644 pkg/antctl/raw/multicluster/deploy/member_cluster.go diff --git a/docs/multicluster/antctl.md b/docs/multicluster/antctl.md index 604961d1c74..d03881086af 100644 --- a/docs/multicluster/antctl.md +++ b/docs/multicluster/antctl.md @@ -1,11 +1,16 @@ # Antctl Multi-cluster commands Starting from version 1.6.0, Antrea supports the `antctl mc` commands, which can -collect information from a leader cluster in a ClusterSet, for troubleshooting -issues in an Antrea Multi-cluster ClusterSet. +collect information from a leader cluster in a ClusterSet for troubleshooting +issues in an Antrea Multi-cluster ClusterSet, create and delete resources in an +Antrea Multi-cluster ClusterSet, and so on. The command `antctl mc get` is supported +since Antrea version 1.6.0 and other commands are supported from 1.7.0. These commands +cannot run inside the `antrea-controller`, `antrea-agent` and `antrea-mc-controller` +Pods. The antctl will look for your kubeconfig file at `$HOME/.kube/config` by default. +You can select a different one by setting the `KUBECONFIG` environment variable or with +`--kubeconfig`. -All antctl Multi-cluster commands can only run correctly after [deploying Antrea -Multi-cluster](./user-guide.md) successfully. +## antctl mc get - `antctl mc get clusterset` (or `get clustersets`) command can print all ClusterSets, a specified Clusterset, or the ClusterSet in a specified leader cluster @@ -28,3 +33,60 @@ antctl mc get resourceexport [NAME] [-n NAMESPACE] [-clusterid CLUSTERID] [-o js ``` To see the usage examples of these commands, you may also run `antctl mc get [subcommand] --help`. + +## antctl mc create + +`antctl mc create` command can create access-token and other resources like ClusterSet, ClusterClaims for +Antrea Multi-cluster setup + ++ `antctl mc create accesstoken` command can create accesstoken for member clusters. ++ `antctl mc create clusterclaims` command can create two ClusterClaims in a leader or member cluster. One for the leader or member cluster, and another for the ClusterSet. ++ `antctl mc create clusterset` command can create a ClusterSet in a leader or member cluster. + +```bash +anctcl mc create accesstoken [NAME] [-n NAMESPACE] [--serviceaccount SERVICE_ACCOUNT] [--role-binding ROLE_BINDING] +antctl mc create clusterclaims [-n NAMESPACE] [--clusterset-id CLUSTERSET_ID] [--cluster-id CLUSTER_ID] +antctl mc create clusterset [NAME] [-n NAMESPACE] [--leader-server LEADER_SERVER] [--service-account SERVICE_ACCOUNT] [--secret SECRET] [--leader-cluster LEADER_CLUSTER_ID] +``` + +To see the usage examples of these commands, you may also run `antctl mc create [subcommand] --help`. + +## antctl mc add + +`antctl mc add` command can add a new member cluster to a ClusterSet. + +```bash +antctl mc add membercluster [CLUSTER_ID] [-n NAMESPACE] [--clusterset CLUSTERSET] [--service-account SERVICE_ACCOUNT] +``` + +To see the usage examples of these commands, you may also run `antctl mc add [subcommand] --help`. + +## antctl mc delete + +`antctl mc delete` command can delete resources in an Antrea Multi-cluster ClusterSet. + ++ `antctl mc delete clusterclaims` command can delete the two ClusterClaims in a specified Namespace. One for the leader or member cluster, and another for the ClusterSet. ++ `antctl mc delete clusterset` command can delete a ClusterSet in a leader or member cluster. ++ `antctl mc delete member-cluster` command can delete a member cluster in a specified Antrea Multi-cluster ClusterSet. + +```bash +antctl mc delete clusterclaims [-n NAMESPACE] +antctl mc delete clusterset [NAME] [-n NAMESPACE] +antctl mc delete membercluster [MEMBER_CLUSTER_ID] [-n NAMESPACE] [--clusterset CLUSTERSET] +``` + +To see the usage examples of these commands, you may also run `antctl mc delete [subcommand] --help`. + +## antctl mc deploy + +`antctl mc deploy` command can deploy Antrea Multi-cluster Controller to a leader or member cluster. + ++ `antctl mc deploy leadercluster` command can deploy Antrea Multi-cluster Controller to a leader cluster, and define all the CRDs the leader cluster needed. ++ `antctl mc deploy membercluster` command can deploy Antrea Multi-cluster Controller to a member cluster, and define all the CRDs the member cluster needed. + +```bash +antctl mc deploy leadercluster [--antrea-version ANTREA_VERSION] [-n NAMESPACE] [-f PATH_TO_MANIFEST] +antctl mc deploy membercluster [--antrea-version ANTREA_VERSION] [-n NAMESPACE] [-f PATH_TO_MANIFEST] +``` + +To see the usage examples of these commands, you may also run `antctl mc deploy [subcommand] --help`. diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index cfe9605e132..a3efda0b66d 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -565,6 +565,30 @@ var CommandList = &commandList{ supportController: false, commandGroup: mc, }, + { + cobraCommand: multicluster.AddCmd, + supportAgent: false, + supportController: false, + commandGroup: mc, + }, + { + cobraCommand: multicluster.CreateCmd, + supportAgent: false, + supportController: false, + commandGroup: mc, + }, + { + cobraCommand: multicluster.DeleteCmd, + supportAgent: false, + supportController: false, + commandGroup: mc, + }, + { + cobraCommand: multicluster.DeployCmd, + supportAgent: false, + supportController: false, + commandGroup: mc, + }, }, codec: scheme.Codecs, } diff --git a/pkg/antctl/raw/multicluster/add/member_cluster.go b/pkg/antctl/raw/multicluster/add/member_cluster.go new file mode 100644 index 00000000000..666237dcdc2 --- /dev/null +++ b/pkg/antctl/raw/multicluster/add/member_cluster.go @@ -0,0 +1,115 @@ +// Copyright 2022 Antrea Authors +// +// 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 add + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + "antrea.io/antrea/pkg/antctl/raw" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +type memberClusterOptions struct { + namespace string + clusterSet string + serviceAccount string +} + +var memberClusterOpt *memberClusterOptions + +var memberClusterExamples = strings.Trim(` +# Add a new member cluster to a ClusterSet + $ antctl mc add membercluster -n --clusterset --service-account +`, "\n") + +func (o *memberClusterOptions) validateAndComplete() error { + if o.namespace == "" { + return fmt.Errorf("the Namespace cannot be empty") + } + if o.clusterSet == "" { + return fmt.Errorf("the ClusterSet cannot be empty") + } + if o.serviceAccount == "" { + return fmt.Errorf("the ServiceAccount cannot be empty") + } + + return nil +} + +func NewMemberClusterCmd() *cobra.Command { + command := &cobra.Command{ + Use: "membercluster", + Args: cobra.MaximumNArgs(1), + Short: "Add a new member cluster to a ClusterSet", + Long: "Add a new member cluster to a ClusterSet", + Example: memberClusterExamples, + RunE: memberClusterRunE, + } + + o := &memberClusterOptions{} + memberClusterOpt = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of member cluster") + command.Flags().StringVarP(&o.clusterSet, "clusterset", "", "", "The name of target ClusterSet to add a new member cluster") + command.Flags().StringVarP(&o.serviceAccount, "service-account", "", "", "ServiceAccount of the member cluster") + + return command +} + +func memberClusterRunE(cmd *cobra.Command, args []string) error { + if err := memberClusterOpt.validateAndComplete(); err != nil { + return err + } + if len(args) != 1 { + return fmt.Errorf("exactly one NAME is required, got %d", len(args)) + } + + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + restconfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restconfigTmpl) + + k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return err + } + + memberClusterID := args[0] + clusterSet := &multiclusterv1alpha1.ClusterSet{} + if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: memberClusterOpt.clusterSet, Namespace: memberClusterOpt.namespace}, clusterSet); err != nil { + return err + } + for _, member := range clusterSet.Spec.Members { + if member.ClusterID == memberClusterID { + return fmt.Errorf(`the member cluster "%s" was already added to the ClusterSet "%s"`, memberClusterID, memberClusterOpt.clusterSet) + } + } + clusterSet.Spec.Members = append(clusterSet.Spec.Members, multiclusterv1alpha1.MemberCluster{ClusterID: memberClusterID, ServiceAccount: memberClusterOpt.serviceAccount}) + if err := k8sClient.Update(context.TODO(), clusterSet); err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "The member cluster \"%s\" is added to the ClusterSet \"%s\" successfully\n", memberClusterID, memberClusterOpt.clusterSet) + return nil +} diff --git a/pkg/antctl/raw/multicluster/commands.go b/pkg/antctl/raw/multicluster/commands.go index 12714ef73e2..7367d3cb03a 100644 --- a/pkg/antctl/raw/multicluster/commands.go +++ b/pkg/antctl/raw/multicluster/commands.go @@ -17,6 +17,10 @@ package multicluster import ( "github.com/spf13/cobra" + "antrea.io/antrea/pkg/antctl/raw/multicluster/add" + "antrea.io/antrea/pkg/antctl/raw/multicluster/create" + deleteCmd "antrea.io/antrea/pkg/antctl/raw/multicluster/delete" + "antrea.io/antrea/pkg/antctl/raw/multicluster/deploy" "antrea.io/antrea/pkg/antctl/raw/multicluster/get" ) @@ -25,8 +29,37 @@ var GetCmd = &cobra.Command{ Short: "Display one or many resources in a ClusterSet", } +var CreateCmd = &cobra.Command{ + Use: "create", + Short: "Create multi-cluster resources", +} + +var AddCmd = &cobra.Command{ + Use: "add", + Short: "Add a new member cluster to a ClusterSet", +} + +var DeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete multi-cluster resources", +} + +var DeployCmd = &cobra.Command{ + Use: "deploy", + Short: "Deploy Antrea Multi-cluster Controller to a leader or member cluster", +} + func init() { GetCmd.AddCommand(get.NewClusterSetCommand()) GetCmd.AddCommand(get.NewResourceImportCommand()) GetCmd.AddCommand(get.NewResourceExportCommand()) + CreateCmd.AddCommand(create.NewClusterClaimCmd()) + CreateCmd.AddCommand(create.NewAccessTokenCmd()) + CreateCmd.AddCommand(create.NewClusterSetCmd()) + DeleteCmd.AddCommand(deleteCmd.NewMemberClusterCmd()) + DeleteCmd.AddCommand(deleteCmd.NewClusterSetCmd()) + DeleteCmd.AddCommand(deleteCmd.NewClusterClaimCmd()) + AddCmd.AddCommand(add.NewMemberClusterCmd()) + DeployCmd.AddCommand(deploy.NewLeaderClusterCmd()) + DeployCmd.AddCommand(deploy.NewMemberClusterCmd()) } diff --git a/pkg/antctl/raw/multicluster/create/access_token.go b/pkg/antctl/raw/multicluster/create/access_token.go new file mode 100644 index 00000000000..c38c540f3c0 --- /dev/null +++ b/pkg/antctl/raw/multicluster/create/access_token.go @@ -0,0 +1,200 @@ +// Copyright 2022 Antrea Authors +// +// 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 create + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + "antrea.io/antrea/pkg/antctl/raw" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +type accessTokenOptions struct { + namespace string + serviceAccount string + roleBinding string +} + +var accessTokenOpts *accessTokenOptions + +var accessTokenExamples = strings.Trim(` +# Create an access token in a leader cluster for one or more member clusters. If the ServiceAccount or RoleBinding does not exist, the command will create them too + $ antctl mc create accesstoken -n --service-account --role-binding +`, "\n") + +func (o *accessTokenOptions) validateAndComplete() error { + if o.namespace == "" { + return fmt.Errorf("the Namespace cannot be empty") + } + if o.serviceAccount == "" { + return fmt.Errorf("the ServiceAccount cannot be empty") + } + if o.roleBinding == "" { + return fmt.Errorf("the RoleBinding cannot be empty") + } + return nil +} + +func NewAccessTokenCmd() *cobra.Command { + command := &cobra.Command{ + Use: "accesstoken", + Args: cobra.MaximumNArgs(1), + Short: "Create an accesstoken in a leader cluster", + Long: "Create an accesstoken in a leader cluster", + Example: accessTokenExamples, + RunE: accessTokenRunE, + } + + o := &accessTokenOptions{} + accessTokenOpts = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of the ClusterClaim") + command.Flags().StringVarP(&o.serviceAccount, "service-account", "", "", "ServiceAccount of the access token") + command.Flags().StringVarP(&o.roleBinding, "role-binding", "", "", "RoleBinding of the ServiceAccount") + + return command +} + +func accessTokenRunE(cmd *cobra.Command, args []string) error { + if err := accessTokenOpts.validateAndComplete(); err != nil { + return err + } + if len(args) != 1 { + return fmt.Errorf("exactly one NAME is required, got %d", len(args)) + } + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + restconfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restconfigTmpl) + k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return err + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: accessTokenOpts.serviceAccount, + Namespace: accessTokenOpts.namespace, + }, + } + + var createErr error + fmt.Fprintf(cmd.OutOrStdout(), "Creating ServiceAccount \"%s\"\n", accessTokenOpts.serviceAccount) + createErr = k8sClient.Create(context.TODO(), &serviceAccount) + if createErr != nil { + if errors.IsAlreadyExists(createErr) { + fmt.Fprintf(cmd.OutOrStderr(), "ServiceAccount \"%s\" already exists\n", accessTokenOpts.serviceAccount) + createErr = nil + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ServiceAccount \"%s\", error: %s\n", accessTokenOpts.serviceAccount, createErr.Error()) + return createErr + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ServiceAccount \"%s\" created\n", serviceAccount.Name) + defer func() { + if createErr != nil { + err := k8sClient.Delete(context.TODO(), &serviceAccount) + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ServiceAccount \"%s\", error: %s\n", serviceAccount.Name, err.Error()) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ServiceAccount \"%s\" deleted\n", serviceAccount.Name) + } + } + }() + } + + fmt.Fprintf(cmd.OutOrStdout(), "Creating RoleBinding \"%s\"\n", accessTokenOpts.roleBinding) + roleBinding := rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: accessTokenOpts.roleBinding, + Namespace: accessTokenOpts.namespace, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "antrea-mc-member-cluster-role", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: accessTokenOpts.serviceAccount, + Namespace: accessTokenOpts.namespace, + }, + }, + } + + createErr = k8sClient.Create(context.TODO(), &roleBinding) + if createErr != nil { + if errors.IsAlreadyExists(createErr) { + fmt.Fprintf(cmd.OutOrStderr(), "RoleBinding \"%s\" already exists\n", accessTokenOpts.roleBinding) + createErr = nil + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to create RoleBingding \"%s\", error: %s\n", accessTokenOpts.roleBinding, createErr.Error()) + return createErr + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "RoleBinding \"%s\" created\n", roleBinding.Name) + defer func() { + if createErr != nil { + err := k8sClient.Delete(context.TODO(), &roleBinding) + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete RoleBinding \"%s\", error: %s\n", roleBinding.Name, err.Error()) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "RoleBinding \"%s\" deleted\n", roleBinding.Name) + } + } + }() + } + + secretName := args[0] + + fmt.Fprintf(cmd.OutOrStdout(), "Creating Secret \"%s\"\n", secretName) + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName + "A", + Namespace: accessTokenOpts.namespace, + Annotations: map[string]string{ + "kubernetes.io/service-account.name": accessTokenOpts.serviceAccount, + }, + }, + Type: "kubernetes.io/service-account-token", + } + + createErr = k8sClient.Create(context.TODO(), &secret) + if createErr != nil { + if errors.IsAlreadyExists(createErr) { + fmt.Fprintf(cmd.OutOrStderr(), "Secret \"%s\" already exists\n", secretName) + createErr = nil + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to create Secret \"%s\", start rollback\n", secretName) + return createErr + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Secret \"%s\" with access token created\n", secretName) + } + + return nil +} diff --git a/pkg/antctl/raw/multicluster/create/clusterclaim.go b/pkg/antctl/raw/multicluster/create/clusterclaim.go new file mode 100644 index 00000000000..734ee934206 --- /dev/null +++ b/pkg/antctl/raw/multicluster/create/clusterclaim.go @@ -0,0 +1,152 @@ +// Copyright 2022 Antrea Authors +// +// 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 create + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + "antrea.io/antrea/pkg/antctl/raw" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +type clusterClaimOptions struct { + namespace string + clusterSetID string + clusterID string +} + +var clusterClaimOpt *clusterClaimOptions + +var clusterClaimExamples = strings.Trim(` +# Create two ClusterClaims, one for the leader or member cluster and another for the ClusterSet + $ antctl mc create clusterclaims --cluster-id --clusterset -n +`, "\n") + +func (o *clusterClaimOptions) validateAndComplete() error { + if o.namespace == "" { + return fmt.Errorf("the Namespace cannot be empty") + } + if o.clusterSetID == "" { + return fmt.Errorf("the ClusterSet ID cannot be empty") + } + if o.clusterID == "" { + return fmt.Errorf("the cluster ID cannot be empty") + } + + return nil +} + +func NewClusterClaimCmd() *cobra.Command { + command := &cobra.Command{ + Use: "clusterclaims", + Args: cobra.MaximumNArgs(0), + Short: "Create two ClusterClaims in a leader or member cluster", + Long: "Create two ClusterClaims in a leader or member cluster. One for ClusterSet and another for the leader or member cluster", + Example: clusterClaimExamples, + RunE: clusterClaimRunE, + } + + o := &clusterClaimOptions{} + clusterClaimOpt = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of the ClusterClaim") + command.Flags().StringVarP(&o.clusterSetID, "clusterset-id", "", "", "ClusterSet ID of the ClusterClaim for the ClusterSet") + command.Flags().StringVarP(&o.clusterID, "cluster-id", "", "", "cluster ID of the ClusterClaim for the leader or member cluster") + + return command +} + +func clusterClaimRunE(cmd *cobra.Command, args []string) error { + if err := clusterClaimOpt.validateAndComplete(); err != nil { + return err + } + + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + restconfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restconfigTmpl) + + k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return err + } + + clusterClaim := &multiclusterv1alpha1.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: multiclusterv1alpha1.WellKnownClusterClaimID, + Namespace: clusterClaimOpt.namespace, + }, + Value: clusterClaimOpt.clusterID, + Name: multiclusterv1alpha1.WellKnownClusterClaimID, + } + + var createErr error + createErr = k8sClient.Create(context.TODO(), clusterClaim) + if createErr != nil { + if errors.IsAlreadyExists(createErr) { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" already exists\n", multiclusterv1alpha1.WellKnownClusterClaimID) + createErr = nil + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ClusterClaim \"%s\", error: %s\n", multiclusterv1alpha1.WellKnownClusterClaimID, createErr.Error()) + return createErr + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" with Value \"%s\" created\n", multiclusterv1alpha1.WellKnownClusterClaimID, clusterClaimOpt.clusterID) + defer func() { + if createErr != nil { + err := k8sClient.Delete(context.TODO(), clusterClaim) + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ClusterClaim \"%s\", error: \n", err.Error()) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" deleted\n", multiclusterv1alpha1.WellKnownClusterClaimID) + } + } + }() + } + + clustersetClaim := &multiclusterv1alpha1.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: multiclusterv1alpha1.WellKnownClusterClaimClusterSet, + Namespace: clusterClaimOpt.namespace, + }, + Value: clusterClaimOpt.clusterSetID, + Name: multiclusterv1alpha1.WellKnownClusterClaimClusterSet, + } + + createErr = k8sClient.Create(context.TODO(), clustersetClaim) + if createErr != nil { + if errors.IsAlreadyExists(createErr) { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" already exists\n", multiclusterv1alpha1.WellKnownClusterClaimClusterSet) + createErr = nil + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ClusterClaim \"%s\", start rollback\n", multiclusterv1alpha1.WellKnownClusterClaimClusterSet) + return createErr + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" with Value \"%s\" created\n", multiclusterv1alpha1.WellKnownClusterClaimClusterSet, clusterClaimOpt.clusterSetID) + } + + return nil +} diff --git a/pkg/antctl/raw/multicluster/create/clusterset.go b/pkg/antctl/raw/multicluster/create/clusterset.go new file mode 100644 index 00000000000..a50969a0632 --- /dev/null +++ b/pkg/antctl/raw/multicluster/create/clusterset.go @@ -0,0 +1,142 @@ +// Copyright 2022 Antrea Authors +// +// 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 create + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + "antrea.io/antrea/pkg/antctl/raw" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +type clusterSetOptions struct { + leaderCluster string + leaderClusterServer string + leaderClusterNamespace string + memberClusters map[string]string + namespace string + secret string +} + +var clusterSetOpt *clusterSetOptions + +var clusterSetExamples = strings.Trim(` +# Create a ClusterSet in a leader cluster + $ antctl mc create clusterset -n --service-account --leader-cluster +# Create a ClusterSet in a member cluster + $ antctl mc create clusterset -n --leader-apiserver --secret --leader-cluster +`, "\n") + +func (o *clusterSetOptions) validateAndComplete() error { + if o.namespace == "" { + return fmt.Errorf("the Namespace cannot be empty") + } + if o.leaderClusterNamespace == "" { + o.leaderClusterNamespace = metav1.NamespaceDefault + } + if o.leaderCluster == "" { + return fmt.Errorf("the leader-cluster-id cannot be empty") + } + if o.secret == "" && o.memberClusters == nil { + return fmt.Errorf("the ServiceAccounts list is required in leader cluster, the secret is required in member cluster") + } + if o.secret != "" && o.leaderClusterServer == "" { + return fmt.Errorf("the leader cluster apiserver is required in member cluster") + } + + return nil +} + +func NewClusterSetCmd() *cobra.Command { + command := &cobra.Command{ + Use: "clusterset", + Args: cobra.MaximumNArgs(1), + Short: "Create a ClusterSet in a leader or member cluster", + Long: "Create a ClusterSet in a leader or member cluster", + Example: clusterSetExamples, + RunE: clusterSetRunE, + } + + o := &clusterSetOptions{} + clusterSetOpt = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of the ClusterSet") + command.Flags().StringVarP(&o.leaderCluster, "leader-cluster", "", "", "leader cluster ID of the ClusterSet") + command.Flags().StringVarP(&o.leaderClusterServer, "leader-apiserver", "", "", "leader cluster apiserver address of the ClusterSet. It is required only for a member cluster") + command.Flags().StringVarP(&o.leaderClusterNamespace, "leader-namespace", "", "", "the Namespace where Antrea Multi-cluster Controller is running in leader cluster") + command.Flags().StringToStringVarP(&o.memberClusters, "member-clusters", "", nil, "a map from cluster ID to ServiceAccount of the member clusters(e.g. --member-clusters member1=sa1,member2=sa2). It is required only for a leader cluster") + command.Flags().StringVarP(&o.secret, "secret", "", "", "Secret to access the leader cluster. It is required only when creating ClusterSet in a member cluster") + + return command +} + +func clusterSetRunE(cmd *cobra.Command, args []string) error { + if err := clusterSetOpt.validateAndComplete(); err != nil { + return err + } + if len(args) != 1 { + return fmt.Errorf("exactly one NAME is required, got %d", len(args)) + } + + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + restconfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restconfigTmpl) + + k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return err + } + clusterSetName := args[0] + clusterSet := &multiclusterv1alpha1.ClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterSetName, + Namespace: clusterSetOpt.namespace, + }, + Spec: multiclusterv1alpha1.ClusterSetSpec{ + Leaders: []multiclusterv1alpha1.MemberCluster{ + { + ClusterID: clusterSetOpt.leaderCluster, + Secret: clusterSetOpt.secret, + Server: fmt.Sprintf("https://%s", strings.Replace(clusterSetOpt.leaderClusterServer, "https://", "", 1)), + }, + }, + Namespace: clusterSetOpt.leaderClusterNamespace, + }, + } + + for memberCluster, serviceAccount := range clusterSetOpt.memberClusters { + clusterSet.Spec.Members = append(clusterSet.Spec.Members, multiclusterv1alpha1.MemberCluster{ + ClusterID: memberCluster, + ServiceAccount: serviceAccount, + }) + } + + if err := k8sClient.Create(context.TODO(), clusterSet); err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "ClusterSet \"%s\" created\n", clusterSetName) + + return nil +} diff --git a/pkg/antctl/raw/multicluster/delete/clusterclaim.go b/pkg/antctl/raw/multicluster/delete/clusterclaim.go new file mode 100644 index 00000000000..83418b37183 --- /dev/null +++ b/pkg/antctl/raw/multicluster/delete/clusterclaim.go @@ -0,0 +1,124 @@ +// Copyright 2022 Antrea Authors +// +// 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 delete + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + "antrea.io/antrea/pkg/antctl/raw" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +type clusterClaimOptions struct { + namespace string +} + +var clusterClaimOpt *clusterClaimOptions + +var clusterClaimExamples = strings.Trim(` +# Delete the two ClusterClaims in a specified Namespace. One for the leader or member cluster, and another for the ClusterSet + $ antctl mc delete clusterclaims -n +`, "\n") + +func (o *clusterClaimOptions) validateAndComplete() error { + if o.namespace == "" { + return fmt.Errorf("the Namespace cannot be empty") + } + + return nil +} + +func NewClusterClaimCmd() *cobra.Command { + command := &cobra.Command{ + Use: "clusterclaims", + Args: cobra.MaximumNArgs(1), + Short: "Delete the ClusterClaims in a specified Namespace", + Long: "Delete the ClusterClaims in a specified Namespace. One for the leader or member cluster, and another for the ClusterSet", + Example: clusterClaimExamples, + RunE: clusterClaimRunE, + } + + o := &clusterClaimOptions{} + clusterClaimOpt = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of ClusterClaim") + + return command +} + +func clusterClaimRunE(cmd *cobra.Command, args []string) error { + if err := clusterClaimOpt.validateAndComplete(); err != nil { + return err + } + + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + restconfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restconfigTmpl) + + k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return err + } + + clusterClaims := []*multiclusterv1alpha1.ClusterClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: multiclusterv1alpha1.WellKnownClusterClaimID, + Namespace: clusterClaimOpt.namespace, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: multiclusterv1alpha1.WellKnownClusterClaimClusterSet, + Namespace: clusterClaimOpt.namespace, + }, + }, + } + + for _, clusterClaim := range clusterClaims { + if err := deleteClusterClaim(cmd, k8sClient, clusterClaim); err != nil { + return err + } + } + + return nil +} + +func deleteClusterClaim(cmd *cobra.Command, k8sClient client.Client, clusterClaim *multiclusterv1alpha1.ClusterClaim) error { + err := k8sClient.Delete(context.TODO(), clusterClaim) + if err != nil { + if !errors.IsNotFound(err) { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ClusterClaim \"%s\", error: %s\n", clusterClaim.ObjectMeta.Name, err.Error()) + return err + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" not found\n", clusterClaim.ObjectMeta.Name) + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" deleted\n", clusterClaim.ObjectMeta.Name) + } + + return nil +} diff --git a/pkg/antctl/raw/multicluster/delete/clusterset.go b/pkg/antctl/raw/multicluster/delete/clusterset.go new file mode 100644 index 00000000000..35830033b59 --- /dev/null +++ b/pkg/antctl/raw/multicluster/delete/clusterset.go @@ -0,0 +1,101 @@ +// Copyright 2022 Antrea Authors +// +// 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 delete + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + "antrea.io/antrea/pkg/antctl/raw" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +type clusterSetOptions struct { + namespace string +} + +var clusterSetOpt *clusterSetOptions + +var clusterSetExamples = strings.Trim(` +# Delete a ClusterSet in a specified Namespace in a leader or member cluster + $ antctl mc delete clusterset -n +`, "\n") + +func (o *clusterSetOptions) validateAndComplete() error { + if o.namespace == "" { + return fmt.Errorf("the Namespace cannot be empty") + } + + return nil +} + +func NewClusterSetCmd() *cobra.Command { + command := &cobra.Command{ + Use: "clusterset", + Args: cobra.MaximumNArgs(1), + Short: "Delete a ClusterSet in a leader or member cluster", + Long: "Delete a ClusterSet in a leader or member cluster", + Example: clusterSetExamples, + RunE: clusterSetRunE, + } + + o := &clusterSetOptions{} + clusterSetOpt = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of ClusterSet") + + return command +} + +func clusterSetRunE(cmd *cobra.Command, args []string) error { + if err := clusterSetOpt.validateAndComplete(); err != nil { + return err + } + if len(args) != 1 { + return fmt.Errorf("exactly one NAME is required, got %d", len(args)) + } + + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + restconfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restconfigTmpl) + + k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return err + } + + clusterSetName := args[0] + clusterSet := &multiclusterv1alpha1.ClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterSetName, + Namespace: clusterSetOpt.namespace, + }, + } + if err := k8sClient.Delete(context.TODO(), clusterSet); err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "ClusterSet \"%s\" deleted\n", clusterSetName) + return nil +} diff --git a/pkg/antctl/raw/multicluster/delete/member_cluster.go b/pkg/antctl/raw/multicluster/delete/member_cluster.go new file mode 100644 index 00000000000..565d490e52f --- /dev/null +++ b/pkg/antctl/raw/multicluster/delete/member_cluster.go @@ -0,0 +1,114 @@ +// Copyright 2022 Antrea Authors +// +// 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 delete + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + "antrea.io/antrea/pkg/antctl/raw" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +type memberClusterOptions struct { + namespace string + clusterSet string +} + +var memberClusterOpts *memberClusterOptions + +var memberClusterExamples = strings.Trim(` +# Delete a member cluster in a ClusterSet + $ antctl mc delete membercluster -n --clusterset +`, "\n") + +func (o *memberClusterOptions) validateAndComplete() error { + if o.namespace == "" { + return fmt.Errorf("the Namespace cannot be empty") + } + if o.clusterSet == "" { + return fmt.Errorf("the ClusterSet cannot be empty") + } + + return nil +} + +func NewMemberClusterCmd() *cobra.Command { + command := &cobra.Command{ + Use: "membercluster", + Args: cobra.MaximumNArgs(1), + Short: "Delete a member cluster in a ClusterSet", + Long: "Delete a member cluster in a ClusterSet", + Example: memberClusterExamples, + RunE: memberClusterRunE, + } + + o := &memberClusterOptions{} + memberClusterOpts = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of member Cluster") + command.Flags().StringVarP(&o.clusterSet, "clusterset", "", "", "ClusterSet ID of the member Cluster") + + return command +} + +func memberClusterRunE(cmd *cobra.Command, args []string) error { + if err := memberClusterOpts.validateAndComplete(); err != nil { + return err + } + if len(args) != 1 { + return fmt.Errorf("exactly one ClusterID is required, got %d", len(args)) + } + + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + restconfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restconfigTmpl) + + k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return err + } + memberClusterID := args[0] + clusterSet := &multiclusterv1alpha1.ClusterSet{} + if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: memberClusterOpts.clusterSet, Namespace: memberClusterOpts.namespace}, clusterSet); err != nil { + return err + } + + var memberClusters []multiclusterv1alpha1.MemberCluster + for _, m := range clusterSet.Spec.Members { + if m.ClusterID != memberClusterID { + memberClusters = append(memberClusters, m) + } + } + if len(memberClusters) == len(clusterSet.Spec.Members) { + return fmt.Errorf(`member cluster "%s" not found in ClusterSet "%s"`, memberClusterID, memberClusterOpts.clusterSet) + } + clusterSet.Spec.Members = memberClusters + if err := k8sClient.Update(context.TODO(), clusterSet); err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "Member cluster \"%s\" deleted\n", memberClusterID) + return nil +} diff --git a/pkg/antctl/raw/multicluster/deploy/deploy_helper.go b/pkg/antctl/raw/multicluster/deploy/deploy_helper.go new file mode 100644 index 00000000000..e8311b8f0ec --- /dev/null +++ b/pkg/antctl/raw/multicluster/deploy/deploy_helper.go @@ -0,0 +1,187 @@ +// Copyright 2022 Antrea Authors +// +// 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 deploy + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/spf13/cobra" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + yamlutil "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" + + "antrea.io/antrea/pkg/antctl/raw" +) + +const ( + leaderRole = "leader" + memberRole = "member" + + latestVersionURL = "https://raw.githubusercontent.com/antrea-io/antrea/main/multicluster/build/yamls" + downloadURL = "https://github.com/antrea-io/antrea/releases/download" + leaderGlobalYAML = "antrea-multicluster-leader-global.yml" + leaderNamespacedYAML = "antrea-multicluster-leader-namespaced.yml" + memberYAML = "antrea-multicluster-member.yml" +) + +func generateManifests(role string, version string) ([]string, error) { + var manifests []string + switch role { + case leaderRole: + manifests = []string{ + fmt.Sprintf("%s/%s", latestVersionURL, leaderGlobalYAML), + fmt.Sprintf("%s/%s", latestVersionURL, leaderNamespacedYAML), + } + if version != "latest" { + manifests = []string{ + fmt.Sprintf("%s/%s/%s", downloadURL, version, leaderGlobalYAML), + fmt.Sprintf("%s/%s/%s", downloadURL, version, leaderNamespacedYAML), + } + } + case memberRole: + manifests = []string{ + fmt.Sprintf("%s/%s", latestVersionURL, memberYAML), + } + if version != "latest" { + manifests = []string{ + fmt.Sprintf("%s/%s/%s", downloadURL, version, memberYAML), + } + } + default: + return manifests, fmt.Errorf("invalid role %s", role) + } + + return manifests, nil +} + +func createResources(cmd *cobra.Command, content []byte) error { + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + restconfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restconfigTmpl) + + k8sClient, err := kubernetes.NewForConfig(kubeconfig) + if err != nil { + return err + } + dynamicClient, err := dynamic.NewForConfig(kubeconfig) + if err != nil { + return err + } + + decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(content)), 100) + for { + var rawObj runtime.RawExtension + if err = decoder.Decode(&rawObj); err != nil { + break + } + + obj, gvk, err := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).Decode(rawObj.Raw, nil, nil) + if err != nil { + return err + } + unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return err + } + + unstructuredObj := &unstructured.Unstructured{Object: unstructuredMap} + + gr, err := restmapper.GetAPIGroupResources(k8sClient.Discovery()) + if err != nil { + return err + } + + mapper := restmapper.NewDiscoveryRESTMapper(gr) + mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return err + } + + var dri dynamic.ResourceInterface + if mapping.Scope.Name() == meta.RESTScopeNameNamespace { + dri = dynamicClient.Resource(mapping.Resource).Namespace(unstructuredObj.GetNamespace()) + } else { + dri = dynamicClient.Resource(mapping.Resource) + } + + if _, err := dri.Create(context.TODO(), unstructuredObj, metav1.CreateOptions{}); err != nil { + if !kerrors.IsAlreadyExists(err) { + return err + } + } + fmt.Fprintf(cmd.OutOrStdout(), "%s/%s created\n", unstructuredObj.GetKind(), unstructuredObj.GetName()) + } + + return nil +} + +func deploy(cmd *cobra.Command, role string, version string, namespace string, filename string) error { + if filename != "" { + content, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + if err := createResources(cmd, content); err != nil { + return err + } + } else { + manifests, err := generateManifests(role, version) + if err != nil { + return err + } + for _, manifest := range manifests { + // #nosec G107 + resp, err := http.Get(manifest) + if err != nil { + return err + } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + content := string(b) + if role == leaderRole && strings.Contains(manifest, "namespaced") { + content = strings.ReplaceAll(content, "changeme", namespace) + } + if role == memberRole && strings.Contains(manifest, "member") { + content = strings.ReplaceAll(content, "kube-system", namespace) + } + + if err := createResources(cmd, []byte(content)); err != nil { + return err + } + } + } + fmt.Fprintf(cmd.OutOrStdout(), "The %s cluster resources are deployed\n", role) + + return nil +} diff --git a/pkg/antctl/raw/multicluster/deploy/leader_cluster.go b/pkg/antctl/raw/multicluster/deploy/leader_cluster.go new file mode 100644 index 00000000000..9a47ced9cf4 --- /dev/null +++ b/pkg/antctl/raw/multicluster/deploy/leader_cluster.go @@ -0,0 +1,88 @@ +// Copyright 2022 Antrea Authors +// +// 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 deploy + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" +) + +type leaderClusterOptions struct { + namespace string + filename string + antreaVersion string +} + +var leaderClusterOpts *leaderClusterOptions + +var leaderClusterExamples = strings.Trim(` + +# Define the leader cluster CRDs and deploy the "antrea-mc-controller" Deployment in a specified Namespace + $ antctl mc deploy leadercluster --antrea-version -n +# Define the leader cluster CRDs and deploy the "antrea-mc-controller" Deployment using pre-downloaded manifest + $ antctl mc deploy leadercluster -f + +The following CRDs will be defined: +- CRDs: ClusterClaim, ClusterSet, MemberClusterAnnounce, ResourceExport, ResourceImport +`, "\n") + +func (o *leaderClusterOptions) validateAndComplete() error { + if o.filename != "" { + if _, err := os.Stat(o.filename); err != nil { + return err + } + return nil + } + if o.namespace == "" { + return fmt.Errorf("the Namespace cannot be empty") + } + if o.antreaVersion == "" { + o.antreaVersion = "latest" + } + + return nil +} + +func NewLeaderClusterCmd() *cobra.Command { + command := &cobra.Command{ + Use: "leadercluster", + Args: cobra.MaximumNArgs(0), + Short: "Deploy Antrea Multi-cluster to a leader cluster", + Long: "Deploy Antrea Multi-cluster to a leader cluster in a Namespace", + Example: leaderClusterExamples, + RunE: leaderClusterRunE, + } + o := &leaderClusterOptions{} + leaderClusterOpts = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace to deploy Antrea Multi-cluster") + command.Flags().StringVarP(&o.antreaVersion, "antrea-version", "", "", + "version of Antrea Multi-cluster to deploy. If not set, the latest version from Antrea main branch will be used. "+ + "When manifest-file is not provided, the Antrea Multi-cluster deployment manifest of the specified version will be downloaded and applied; "+ + "when manifest-file is provided, this option will be ignored") + command.Flags().StringVarP(&o.filename, "manifest-file", "f", "", "path to the Antrea Multi-cluster deployment manifest file for leader cluster") + + return command +} + +func leaderClusterRunE(cmd *cobra.Command, _ []string) error { + if err := leaderClusterOpts.validateAndComplete(); err != nil { + return err + } + + return deploy(cmd, leaderRole, leaderClusterOpts.antreaVersion, leaderClusterOpts.namespace, leaderClusterOpts.filename) +} diff --git a/pkg/antctl/raw/multicluster/deploy/member_cluster.go b/pkg/antctl/raw/multicluster/deploy/member_cluster.go new file mode 100644 index 00000000000..aeb8a440ef7 --- /dev/null +++ b/pkg/antctl/raw/multicluster/deploy/member_cluster.go @@ -0,0 +1,87 @@ +// Copyright 2022 Antrea Authors +// +// 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 deploy + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" +) + +type memberClusterOptions struct { + namespace string + antreaVersion string + filename string +} + +var memberClusterOpts *memberClusterOptions + +var memberClusterExamples = strings.Trim(` +# Define the member cluster CRDs and deploy the "antrea-mc-controller" Deployment in a specified Namespace + $ antctl mc deploy membercluster --antrea-version -n +# Define the member cluster CRDs and deploy the "antrea-mc-controller" Deployment using pre-downloaded manifest + $ antctl mc deploy membercluster -f + +The following CRDs will be defined: +- CRDs: ClusterClaim, ClusterSet, MemberClusterAnnounce, ResourceExport, ResourceImport, ServiceExport, ServiceImport +`, "\n") + +func (o *memberClusterOptions) validateAndComplete() error { + if o.filename != "" { + if _, err := os.Stat(o.filename); err != nil { + return err + } + return nil + } + if o.namespace == "" { + return fmt.Errorf("the Namespace cannot be empty") + } + if o.antreaVersion == "" { + o.antreaVersion = "latest" + } + + return nil +} + +func NewMemberClusterCmd() *cobra.Command { + command := &cobra.Command{ + Use: "membercluster", + Args: cobra.MaximumNArgs(0), + Short: "Deploy Antrea Multi-cluster to a member cluster", + Long: "Deploy Antrea Multi-cluster to a member cluster in a Namespace", + Example: memberClusterExamples, + RunE: memberClusterRunE, + } + o := &memberClusterOptions{} + memberClusterOpts = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace to deploy Antrea Multi-cluster") + command.Flags().StringVarP(&o.antreaVersion, "antrea-version", "", "", + "version of Antrea Multi-cluster to deploy. If not set, the latest version from Antrea main branch will be used. "+ + "When manifest-file is not provided, the Antrea Multi-cluster deployment manifest of the specified version will be downloaded and applied; "+ + "when manifest-file is provided, this option will be ignored") + command.Flags().StringVarP(&o.filename, "manifest-file", "f", "", "path to the Antrea Multi-cluster deployment manifest file for member cluster") + + return command +} + +func memberClusterRunE(cmd *cobra.Command, _ []string) error { + if err := memberClusterOpts.validateAndComplete(); err != nil { + return err + } + + return deploy(cmd, memberRole, memberClusterOpts.antreaVersion, memberClusterOpts.namespace, memberClusterOpts.filename) +}