diff --git a/multicluster/controllers/multicluster/commonarea/remote_common_area.go b/multicluster/controllers/multicluster/commonarea/remote_common_area.go index 2282f2b9e6a..88bf9e6e67d 100644 --- a/multicluster/controllers/multicluster/commonarea/remote_common_area.go +++ b/multicluster/controllers/multicluster/commonarea/remote_common_area.go @@ -136,7 +136,7 @@ func NewRemoteCommonArea(clusterID common.ClusterID, clusterSetID common.Cluster scheme *runtime.Scheme, localClusterClient client.Client, clusterSetNamespace string, localNamespace string) (RemoteCommonArea, error) { klog.InfoS("Create a RemoteCommonArea", "cluster", clusterID) - crtData, token, err := getSecretCACrtAndToken(secret) + crtData, token, err := GetSecretCACrtAndToken(secret) if err != nil { return nil, err } @@ -189,9 +189,9 @@ func NewRemoteCommonArea(clusterID common.ClusterID, clusterSetID common.Cluster } /** - * getSecretCACrtAndToken returns the access credentials from Secret. + * GetSecretCACrtAndToken returns the access credentials from Secret. */ -func getSecretCACrtAndToken(secretObj *v1.Secret) ([]byte, []byte, error) { +func GetSecretCACrtAndToken(secretObj *v1.Secret) ([]byte, []byte, error) { caData, found := secretObj.Data[v1.ServiceAccountRootCAKey] if !found { return nil, nil, fmt.Errorf("ca.crt data not found in Secret %v", secretObj.GetName()) diff --git a/multicluster/controllers/multicluster/member_clusterset_controller.go b/multicluster/controllers/multicluster/member_clusterset_controller.go index 94f8d5715fa..d466a80f786 100644 --- a/multicluster/controllers/multicluster/member_clusterset_controller.go +++ b/multicluster/controllers/multicluster/member_clusterset_controller.go @@ -352,27 +352,3 @@ func (r *MemberClusterSetReconciler) GetRemoteCommonAreaAndLocalID() (commonarea } return nil, "", errors.New("no connected remote Common Area") } - -func (r *MemberClusterSetReconciler) deleteMemberAnnounce() error { - commonArea, ok := r.remoteCommonAreaManager.GetRemoteCommonAreas()[r.remoteCommonAreaManager.GetElectedLeaderClusterID()] - if !ok { - return fmt.Errorf("no Common Area for ClusetSet %s", r.clusterSetID) - } - - namespace := commonArea.GetNamespace() - name := "member-announce-from-" + string(r.clusterID) - - memberClusterAnnounce := &multiclusterv1alpha1.MemberClusterAnnounce{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - } - - klog.InfoS("Deleting MemberClusterAnnounce", "name", name, "Namespace", namespace) - if err := commonArea.Delete(context.TODO(), memberClusterAnnounce); err != nil { - return fmt.Errorf("failed to delete MemberClusterAnnounce: %v", err) - } - - return nil -} diff --git a/multicluster/controllers/multicluster/memberclusterannounce_controller.go b/multicluster/controllers/multicluster/memberclusterannounce_controller.go index 7cf8ad16fe2..1c4796760bf 100644 --- a/multicluster/controllers/multicluster/memberclusterannounce_controller.go +++ b/multicluster/controllers/multicluster/memberclusterannounce_controller.go @@ -326,10 +326,7 @@ func (r *MemberClusterAnnounceReconciler) addMemberToClusterSet(memberClusterAnn } exist := false -<<<<<<< HEAD // ClusterSet ID of MemberClusterAnnounce cannot change. It is guaranteed by the MemberClusterAnnounce webhook. -======= ->>>>>>> 6aec3fe5 (Auto update ClusterSet in leader cluster) for _, member := range clusterSet.Spec.Members { if member.ClusterID == memberClusterAnnounce.ClusterID { exist = true @@ -366,18 +363,11 @@ func (r *MemberClusterAnnounceReconciler) removeMemberFromClusterSet(memberClust } } if !found { -<<<<<<< HEAD klog.InfoS("Member cluster not found in ClusterSet", "ClusterSet", clusterSetID, "cluster", memberClusterAnnounce.ClusterID) return nil } klog.InfoS("Removing member cluster from the ClusterSet", "cluster", memberClusterAnnounce.ClusterID, "ClusterSet", klog.KObj(clusterSet)) -======= - klog.InfoS("Member cluster not found in ClusterSet", "ClusterSet", clusterSetID, "Member", memberClusterAnnounce.ClusterID) - return nil - } - ->>>>>>> 6aec3fe5 (Auto update ClusterSet in leader cluster) if err := r.Update(context.TODO(), clusterSet); err != nil { klog.ErrorS(err, "Failed to update ClusterSet in leader cluster", "ClusterSet", clusterSetID, "Namespace", clusterSet.Namespace) return err diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index 88038d12eb3..1bece42a8d9 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -598,25 +598,37 @@ $ antctl get podmulticaststats pod -n namespace`, commandGroup: mc, }, { - cobraCommand: multicluster.AddCmd, + cobraCommand: multicluster.CreateCmd, supportAgent: false, supportController: false, commandGroup: mc, }, { - cobraCommand: multicluster.CreateCmd, + cobraCommand: multicluster.DeployCmd, supportAgent: false, supportController: false, commandGroup: mc, }, { - cobraCommand: multicluster.DeleteCmd, + cobraCommand: multicluster.JoinCmd, supportAgent: false, supportController: false, commandGroup: mc, }, { - cobraCommand: multicluster.DeployCmd, + cobraCommand: multicluster.LeaveCmd, + supportAgent: false, + supportController: false, + commandGroup: mc, + }, + { + cobraCommand: multicluster.InitCmd, + supportAgent: false, + supportController: false, + commandGroup: mc, + }, + { + cobraCommand: multicluster.DestroyCmd, supportAgent: false, supportController: false, commandGroup: mc, diff --git a/pkg/antctl/raw/multicluster/add/member_cluster.go b/pkg/antctl/raw/multicluster/add/member_cluster.go deleted file mode 100644 index 666237dcdc2..00000000000 --- a/pkg/antctl/raw/multicluster/add/member_cluster.go +++ /dev/null @@ -1,115 +0,0 @@ -// 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 7367d3cb03a..2d997238599 100644 --- a/pkg/antctl/raw/multicluster/commands.go +++ b/pkg/antctl/raw/multicluster/commands.go @@ -17,9 +17,7 @@ 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" ) @@ -34,32 +32,21 @@ var CreateCmd = &cobra.Command{ 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", } +var JoinCmd = NewJoinCommand() +var LeaveCmd = NewLeaveCommand() +var InitCmd = NewInitCommand() +var DestroyCmd = NewDestroyCommand() + 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/common/common.go b/pkg/antctl/raw/multicluster/common/common.go new file mode 100644 index 00000000000..b0f2d37249a --- /dev/null +++ b/pkg/antctl/raw/multicluster/common/common.go @@ -0,0 +1,210 @@ +// 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 common + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + multiclusterv1alpha2 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha2" + "antrea.io/antrea/pkg/antctl/raw" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func NewClient(cmd *cobra.Command) (client.Client, error) { + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return nil, err + } + restConfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restConfigTmpl) + + k8sClient, err := client.New(restConfigTmpl, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return nil, err + } + + return k8sClient, nil +} + +func CreateClusterClaim(cmd *cobra.Command, k8sClient client.Client, namespace string, clusterset string, clusterID string, createdRes *[]*unstructured.Unstructured) error { + fmt.Fprintf(cmd.OutOrStdout(), "Creating ClusterClaim \"%s\" in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimID, namespace) + var createErr error + if createErr = k8sClient.Create(context.TODO(), newClusterClaim(clusterID, namespace, false)); createErr != nil { + if apierrors.IsAlreadyExists(createErr) { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" already exists in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimID, namespace) + createErr = nil + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ClusterClaim \"%s\": %v\n", multiclusterv1alpha2.WellKnownClusterClaimID, createErr) + return createErr + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" created in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimID, namespace) + *createdRes = append(*createdRes, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "multicluster.crd.antrea.io/v1alpha2", + "kind": "ClusterClaim", + "metadata": map[string]interface{}{ + "name": multiclusterv1alpha2.WellKnownClusterClaimID, + "namespace": namespace, + }, + }, + }) + } + + fmt.Fprintf(cmd.OutOrStdout(), "Creating ClusterClaim \"%s\" in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, namespace) + if createErr = k8sClient.Create(context.TODO(), newClusterClaim(clusterset, namespace, true)); createErr != nil { + if apierrors.IsAlreadyExists(createErr) { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" already exists in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, namespace) + createErr = nil + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ClusterClaim \"%s\": %v\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, createErr) + return createErr + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" created in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, namespace) + *createdRes = append(*createdRes, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "multicluster.crd.antrea.io/v1alpha2", + "kind": "ClusterClaim", + "metadata": map[string]interface{}{ + "name": multiclusterv1alpha2.WellKnownClusterClaimClusterSet, + "namespace": namespace, + }, + }, + }) + } + + return nil +} + +func CreateClusterSet(cmd *cobra.Command, k8sClient client.Client, namespace string, clusterset string, + leaderServer string, secret string, memberClusterID string, leaderClusterID string, leaderClusterNamespace string, createdRes *[]*unstructured.Unstructured) error { + fmt.Fprintf(cmd.OutOrStdout(), "Creating ClusterSet \"%s\" in Namespace %s\n", clusterset, namespace) + clusterSet := newClusterSet(clusterset, namespace, leaderServer, secret, memberClusterID, leaderClusterID, leaderClusterNamespace) + if err := k8sClient.Create(context.TODO(), clusterSet); err != nil { + if apierrors.IsAlreadyExists(err) { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterSet \"%s\" already exists in Namespace %s\n", clusterSet.Name, clusterSet.Namespace) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ClusterSet \"%s\": %v\n", clusterSet.Name, err) + return err + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterSet \"%s\" created in Namespace %s\n", clusterSet.Name, clusterSet.Namespace) + *createdRes = append(*createdRes, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "multicluster.crd.antrea.io/v1alpha1", + "kind": "ClusterSet", + "metadata": map[string]interface{}{ + "name": clusterSet.Name, + "namespace": clusterSet.Namespace, + }, + }, + }) + } + + return nil +} + +func DeleteClusterClaim(cmd *cobra.Command, k8sClient client.Client, namespace string) error { + fmt.Fprintf(cmd.OutOrStdout(), "Deleting ClusterClaim \"%s\" in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimID, namespace) + if err := k8sClient.Delete(context.TODO(), newClusterClaim("", namespace, false)); err != nil && !apierrors.IsNotFound(err) { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ClusterClaim \"%s\": %v\n", multiclusterv1alpha2.WellKnownClusterClaimID, err) + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" deleted in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimID, namespace) + fmt.Fprintf(cmd.OutOrStdout(), "Deleting ClusterClaim \"%s\" in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, namespace) + if err := k8sClient.Delete(context.TODO(), newClusterClaim("", namespace, true)); err != nil && !apierrors.IsNotFound(err) { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ClusterClaim \"%s\": %v\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, err) + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" deleted in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, namespace) + return nil +} + +func DeleteClusterSet(cmd *cobra.Command, k8sClient client.Client, namespace string, clusterSet string) error { + fmt.Fprintf(cmd.OutOrStdout(), "Deleting ClusterSet \"%s\" in Namespace %s\n", clusterSet, namespace) + if err := k8sClient.Delete(context.TODO(), newClusterSet(clusterSet, namespace, "", "", "", "", "")); err != nil && !apierrors.IsNotFound(err) { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ClusterSet \"%s\": %v\n", clusterSet, err) + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "ClusterSet \"%s\" deleted in Namespace %s\n", clusterSet, namespace) + return nil +} + +func newClusterClaim(name string, namespace string, clusterSet bool) *multiclusterv1alpha2.ClusterClaim { + clusterClaim := &multiclusterv1alpha2.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + Value: name, + } + if clusterSet { + clusterClaim.Name = multiclusterv1alpha2.WellKnownClusterClaimClusterSet + } else { + clusterClaim.Name = multiclusterv1alpha2.WellKnownClusterClaimID + } + + return clusterClaim +} + +func newClusterSet(name, namespace, leaderServer, secret, memberClusterID, leaderClusterID, leaderNamespace string) *multiclusterv1alpha1.ClusterSet { + if leaderServer != "" { + return &multiclusterv1alpha1.ClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: multiclusterv1alpha1.ClusterSetSpec{ + Members: []multiclusterv1alpha1.MemberCluster{ + { + ClusterID: memberClusterID, + }, + }, + Leaders: []multiclusterv1alpha1.MemberCluster{ + { + ClusterID: leaderClusterID, + Secret: secret, + Server: leaderServer, + }, + }, + Namespace: leaderNamespace, + }, + } + } + + return &multiclusterv1alpha1.ClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: multiclusterv1alpha1.ClusterSetSpec{ + Leaders: []multiclusterv1alpha1.MemberCluster{ + { + ClusterID: leaderClusterID, + }, + }, + Namespace: namespace, + }, + } +} diff --git a/pkg/antctl/raw/multicluster/common/rolllback.go b/pkg/antctl/raw/multicluster/common/rolllback.go new file mode 100644 index 00000000000..df4f3eb3d0b --- /dev/null +++ b/pkg/antctl/raw/multicluster/common/rolllback.go @@ -0,0 +1,35 @@ +// 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 common + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func Rollback(cmd *cobra.Command, k8sClient client.Client, res []*unstructured.Unstructured) error { + for _, obj := range res { + fmt.Fprintf(cmd.OutOrStdout(), "Deleting %s %s\n", obj.GetKind(), obj.GetName()) + if err := k8sClient.Delete(context.TODO(), obj); err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete %s %s: %v\n", obj.GetKind(), obj.GetName(), err) + return err + } + } + return nil +} diff --git a/pkg/antctl/raw/multicluster/create/access_token.go b/pkg/antctl/raw/multicluster/create/access_token.go index c38c540f3c0..e3a0fbfc243 100644 --- a/pkg/antctl/raw/multicluster/create/access_token.go +++ b/pkg/antctl/raw/multicluster/create/access_token.go @@ -24,11 +24,13 @@ import ( 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" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "antrea.io/antrea/pkg/antctl/raw" - multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" +) + +const ( + createByAntctlAnnotation = "multicluster.antrea.io/created-by-antctl" ) type accessTokenOptions struct { @@ -41,7 +43,7 @@ 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 + $ antctl mc create accesstoken member-access-token -n antrea-multicluster --service-account test-sa --role-binding test-rb `, "\n") func (o *accessTokenOptions) validateAndComplete() error { @@ -83,25 +85,30 @@ func accessTokenRunE(cmd *cobra.Command, args []string) error { 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}) + k8sClient, err := common.NewClient(cmd) if err != nil { return err } + var createErr error + createdRes := []*unstructured.Unstructured{} + defer func() { + if createErr != nil { + if err := common.Rollback(cmd, k8sClient, createdRes); err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to rollback: %v\n", err) + } + } + }() serviceAccount := corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: accessTokenOpts.serviceAccount, Namespace: accessTokenOpts.namespace, + Annotations: map[string]string{ + createByAntctlAnnotation: "true", + }, }, } - var createErr error fmt.Fprintf(cmd.OutOrStdout(), "Creating ServiceAccount \"%s\"\n", accessTokenOpts.serviceAccount) createErr = k8sClient.Create(context.TODO(), &serviceAccount) if createErr != nil { @@ -114,16 +121,16 @@ func accessTokenRunE(cmd *cobra.Command, args []string) error { } } 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) - } - } - }() + createdRes = append(createdRes, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": map[string]interface{}{ + "name": serviceAccount.Name, + "namespace": serviceAccount.Namespace, + }, + }, + }) } fmt.Fprintf(cmd.OutOrStdout(), "Creating RoleBinding \"%s\"\n", accessTokenOpts.roleBinding) @@ -131,6 +138,9 @@ func accessTokenRunE(cmd *cobra.Command, args []string) error { ObjectMeta: metav1.ObjectMeta{ Name: accessTokenOpts.roleBinding, Namespace: accessTokenOpts.namespace, + Annotations: map[string]string{ + createByAntctlAnnotation: "true", + }, }, RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", @@ -157,27 +167,27 @@ func accessTokenRunE(cmd *cobra.Command, args []string) error { } } 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) - } - } - }() + createdRes = append(createdRes, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "RoleBinding", + "metadata": map[string]interface{}{ + "name": roleBinding.Name, + "namespace": roleBinding.Namespace, + }, + }, + }) } secretName := args[0] - fmt.Fprintf(cmd.OutOrStdout(), "Creating Secret \"%s\"\n", secretName) secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName + "A", + Name: secretName, Namespace: accessTokenOpts.namespace, Annotations: map[string]string{ "kubernetes.io/service-account.name": accessTokenOpts.serviceAccount, + createByAntctlAnnotation: "true", }, }, Type: "kubernetes.io/service-account-token", diff --git a/pkg/antctl/raw/multicluster/create/clusterclaim.go b/pkg/antctl/raw/multicluster/create/clusterclaim.go deleted file mode 100644 index 8af72305114..00000000000 --- a/pkg/antctl/raw/multicluster/create/clusterclaim.go +++ /dev/null @@ -1,150 +0,0 @@ -// 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" - - multiclusterv1alpha2 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha2" - "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 := &multiclusterv1alpha2.ClusterClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: multiclusterv1alpha2.WellKnownClusterClaimID, - Namespace: clusterClaimOpt.namespace, - }, - Value: clusterClaimOpt.clusterID, - } - - 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", multiclusterv1alpha2.WellKnownClusterClaimID) - createErr = nil - } else { - fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ClusterClaim \"%s\", error: %s\n", multiclusterv1alpha2.WellKnownClusterClaimID, createErr.Error()) - return createErr - } - } else { - fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" with Value \"%s\" created\n", multiclusterv1alpha2.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", multiclusterv1alpha2.WellKnownClusterClaimID) - } - } - }() - } - - clustersetClaim := &multiclusterv1alpha2.ClusterClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: multiclusterv1alpha2.WellKnownClusterClaimClusterSet, - Namespace: clusterClaimOpt.namespace, - }, - Value: clusterClaimOpt.clusterSetID, - } - - createErr = k8sClient.Create(context.TODO(), clustersetClaim) - if createErr != nil { - if errors.IsAlreadyExists(createErr) { - fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" already exists\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet) - createErr = nil - } else { - fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ClusterClaim \"%s\", start rollback\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet) - return createErr - } - } else { - fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" with Value \"%s\" created\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, clusterClaimOpt.clusterSetID) - } - - return nil -} diff --git a/pkg/antctl/raw/multicluster/create/clusterset.go b/pkg/antctl/raw/multicluster/create/clusterset.go deleted file mode 100644 index a50969a0632..00000000000 --- a/pkg/antctl/raw/multicluster/create/clusterset.go +++ /dev/null @@ -1,142 +0,0 @@ -// 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 deleted file mode 100644 index 174575a4be1..00000000000 --- a/pkg/antctl/raw/multicluster/delete/clusterclaim.go +++ /dev/null @@ -1,124 +0,0 @@ -// 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" - - multiclusterv1alpha2 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha2" - "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(0), - 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 := []*multiclusterv1alpha2.ClusterClaim{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: multiclusterv1alpha2.WellKnownClusterClaimID, - Namespace: clusterClaimOpt.namespace, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: multiclusterv1alpha2.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 *multiclusterv1alpha2.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 deleted file mode 100644 index 35830033b59..00000000000 --- a/pkg/antctl/raw/multicluster/delete/clusterset.go +++ /dev/null @@ -1,101 +0,0 @@ -// 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 deleted file mode 100644 index 565d490e52f..00000000000 --- a/pkg/antctl/raw/multicluster/delete/member_cluster.go +++ /dev/null @@ -1,114 +0,0 @@ -// 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/destroy.go b/pkg/antctl/raw/multicluster/destroy.go new file mode 100644 index 00000000000..4b90fae773b --- /dev/null +++ b/pkg/antctl/raw/multicluster/destroy.go @@ -0,0 +1,83 @@ +// 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 multicluster + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" +) + +type destroyOptions struct { + namespace string + clusterSet string +} + +var destroyOpts *destroyOptions + +func (o *destroyOptions) validate() error { + if o.namespace == "" { + return fmt.Errorf("Namespace is required") + } + if o.clusterSet == "" { + return fmt.Errorf("ClusterSet is required") + } + + return nil +} + +var destroyExamples = strings.Trim(` +# Destroy ClusterSet in the leader cluster + antctl mc destroy --namespace antrea-multicluster --clusterset test-clusterset +`, "\n") + +func NewDestroyCommand() *cobra.Command { + command := &cobra.Command{ + Use: "destroy", + Short: "Destroy ClusterSet in the given Namespace of the leader cluster", + Args: cobra.MaximumNArgs(0), + Example: destroyExamples, + RunE: destroyRunE, + } + + o := destroyOptions{} + destroyOpts = &o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Antrea Multi-cluster Namespace") + command.Flags().StringVarP(&o.clusterSet, "clusterset", "", "", "ClusterSet ID") + + return command +} + +func destroyRunE(cmd *cobra.Command, args []string) error { + if err := destroyOpts.validate(); err != nil { + return err + } + k8sClient, err := common.NewClient(cmd) + if err != nil { + return err + } + if err := common.DeleteClusterClaim(cmd, k8sClient, destroyOpts.namespace); err != nil { + return err + } + if err := common.DeleteClusterSet(cmd, k8sClient, destroyOpts.namespace, destroyOpts.clusterSet); err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Successfully deleted ClusterSet and ClusterClaims in Namespace %s\n", destroyOpts.namespace) + + return nil +} diff --git a/pkg/antctl/raw/multicluster/init.go b/pkg/antctl/raw/multicluster/init.go new file mode 100644 index 00000000000..90eb86abc45 --- /dev/null +++ b/pkg/antctl/raw/multicluster/init.go @@ -0,0 +1,100 @@ +// 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 multicluster + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" +) + +type initOptions struct { + namespace string + clusterSet string + clusterID string +} + +var initOpts *initOptions + +func (o *initOptions) validate() error { + if o.namespace == "" { + return fmt.Errorf("Namespace is required") + } + if o.clusterSet == "" { + return fmt.Errorf("ClusterSet is required") + } + if o.clusterID == "" { + return fmt.Errorf("ClusterID is required") + } + return nil +} + +var initExample = strings.Trim(` +# Initialize ClusterSet in the given Namespace of the leader cluster. One ClusterSet and two ClusterClaims will be +created in the Namespace. + $ antctl mc init --namespace antrea-multicluster --clusterset test-clusterset --clusterid test-cluster-north +`, "\n") + +func NewInitCommand() *cobra.Command { + command := &cobra.Command{ + Use: "init", + Short: "Initialize the leader cluster", + Args: cobra.MaximumNArgs(0), + Example: initExample, + RunE: initRunE, + } + + o := initOptions{} + initOpts = &o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Antrea Multi-cluster Namespace") + command.Flags().StringVarP(&o.clusterSet, "clusterset", "", "", "ClusterSet of the leader cluster") + command.Flags().StringVarP(&o.clusterID, "clusterid", "", "", "ClusterID of the leader cluster") + + return command +} + +func initRunE(cmd *cobra.Command, args []string) error { + if err := initOpts.validate(); err != nil { + return err + } + k8sClient, err := common.NewClient(cmd) + if err != nil { + return err + } + createdRes := []*unstructured.Unstructured{} + var createErr error + defer func() { + if createErr != nil { + if err := common.Rollback(cmd, k8sClient, createdRes); err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to rollback: %v\n", err) + } + } + }() + createErr = common.CreateClusterClaim(cmd, k8sClient, initOpts.namespace, initOpts.clusterSet, initOpts.clusterID, &createdRes) + if createErr != nil { + return createErr + } + createErr = common.CreateClusterSet(cmd, k8sClient, initOpts.namespace, initOpts.clusterSet, "", "", "", initOpts.clusterID, initOpts.namespace, &createdRes) + if createErr != nil { + return createErr + } + fmt.Fprintf(cmd.OutOrStdout(), "Successfully initialized the leader cluster\n") + + return nil +} diff --git a/pkg/antctl/raw/multicluster/join.go b/pkg/antctl/raw/multicluster/join.go new file mode 100644 index 00000000000..f9414d1d7fa --- /dev/null +++ b/pkg/antctl/raw/multicluster/join.go @@ -0,0 +1,284 @@ +// 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 multicluster + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + + multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + "antrea.io/antrea/multicluster/controllers/multicluster/commonarea" + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +type clusterOptions struct { + Namespace string `yaml:"namespace,omitempty"` + ClusterID string `yaml:"clusterid,omitempty"` + Secret string `yaml:"secret,omitempty"` + APIServer string `yaml:"APIServer,omitempty"` + ClusterSetID string `yaml:"clusterSet,omitempty"` +} + +type joinOptions struct { + LeaderClusterOpt clusterOptions `yaml:"leaderCluster"` + MemberClusterOpt clusterOptions `yaml:"memberCluster"` + ConfigFile string +} + +type meta struct { + Kind string `yaml:"kind"` + ApiVersion string `yaml:"apiVersion"` +} + +var joinOpts *joinOptions + +func (o *joinOptions) validateAndComplete() error { + if o.ConfigFile != "" { + raw, err := ioutil.ReadFile(o.ConfigFile) + if err != nil { + return err + } + typeMeta := &meta{} + if err := yamlUnmarshall(raw, typeMeta); err != nil { + return err + } + if typeMeta.Kind != "ClusterSetConfig" || typeMeta.ApiVersion != "multicluster.antrea.io/v1alpha1" { + return fmt.Errorf("unknown apiVersion or kind in config: %s", o.ConfigFile) + } + if err := yamlUnmarshall(raw, &joinOpts); err != nil { + return err + } + } + + if o.LeaderClusterOpt.ClusterID == "" { + return fmt.Errorf("the clusterid cannot be empty") + } + if o.LeaderClusterOpt.APIServer == "" { + return fmt.Errorf("the API server cannot be empty") + } + if o.LeaderClusterOpt.Secret == "" { + return fmt.Errorf("the leader Secret cannot be empty") + } + if o.LeaderClusterOpt.Namespace == "" { + return fmt.Errorf("the leader Namespace cannot be empty") + } + if o.MemberClusterOpt.ClusterSetID == "" { + return fmt.Errorf("the member ClusterSet cannot be empty") + } + if o.MemberClusterOpt.ClusterID == "" { + return fmt.Errorf("the member clusterid cannot be empty") + } + if o.MemberClusterOpt.Namespace == "" { + return fmt.Errorf("the member Namespace cannot be empty") + } + + return nil +} + +func yamlUnmarshall(raw []byte, v interface{}) error { + d := yaml.NewDecoder(bytes.NewReader(raw)) + d.KnownFields(false) + return d.Decode(v) +} + +var joinExamples = strings.Trim(` +# Join the ClusterSet from a member cluster with command line options. + $ antctl mc join --leader-namespace=antrea-multicluster \ + --leader-clusterid=test-cluster-north \ + --leader-clusterset=test-cluster-set \ + --secret=member-access-token \ + --leader-server=https://172.18.0.3:6443 \ + --clusterset=test-clusterset \ + --namespace=kube-system \ + --clusterid=test-cluster-east + +# Join the ClusterSet from a member cluster with a config file. If both command line options and config file are specified, +the arguments in config file will be used. + $ antctl mc join --config-file ./config.yaml + +# Config file example: +apiVersion: multicluster.antrea.io/v1alpha1 +kind: ClusterSetConfig +memberCluster: + namespace: kube-system + clusterid: test-cluster-east + clusterSet: test-clusterset +leaderCluster: + namespace: antrea-multicluster + secret: member-access-token + APIServer: https://172.18.0.3:6443 + clusterid: test-cluster-north + clusterSet: test-clusterset +`, "\n") + +func NewJoinCommand() *cobra.Command { + command := &cobra.Command{ + Use: "join", + Short: "Join the ClusterSet from a member cluster", + Args: cobra.MaximumNArgs(0), + Example: joinExamples, + RunE: joinRunE, + } + + o := joinOptions{} + joinOpts = &o + command.Flags().StringVarP(&joinOpts.LeaderClusterOpt.Namespace, "leader-namespace", "", "", "Namespace of the leader cluster") + command.Flags().StringVarP(&joinOpts.LeaderClusterOpt.ClusterID, "leader-clusterid", "", "", "Cluster ID of the leader cluster") + command.Flags().StringVarP(&joinOpts.LeaderClusterOpt.Secret, "secret", "", "", "Secret of the leader cluster") + command.Flags().StringVarP(&joinOpts.LeaderClusterOpt.ClusterSetID, "leader-clusterset", "", "", "ClusterSet of the leader cluster") + command.Flags().StringVarP(&joinOpts.LeaderClusterOpt.APIServer, "leader-server", "", "", "APIServer endpoint of the leader cluster") + command.Flags().StringVarP(&joinOpts.MemberClusterOpt.ClusterSetID, "clusterset", "", "", "ClusterSet of the member cluster") + command.Flags().StringVarP(&joinOpts.MemberClusterOpt.Namespace, "namespace", "", "", "Namespace of the member cluster") + command.Flags().StringVarP(&joinOpts.MemberClusterOpt.ClusterID, "clusterid", "", "", "Cluster ID of the member cluster") + command.Flags().StringVarP(&joinOpts.ConfigFile, "config-file", "f", "", "Config file that defines all config options") + + return command +} + +func joinRunE(cmd *cobra.Command, args []string) error { + var err error + if err = joinOpts.validateAndComplete(); err != nil { + return err + } + k8sClient, err := common.NewClient(cmd) + + memberClusterNamespace := joinOpts.MemberClusterOpt.Namespace + memberClusterID := joinOpts.MemberClusterOpt.ClusterID + memberClusterSet := joinOpts.MemberClusterOpt.ClusterSetID + createdRes := []*unstructured.Unstructured{} + defer func() { + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to join the ClusterSet. Deleting the created resources.\n") + if err := common.Rollback(cmd, k8sClient, createdRes); err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to rollback: %v\n", err) + } + } + }() + err = common.CreateClusterClaim(cmd, k8sClient, memberClusterNamespace, memberClusterSet, memberClusterID, &createdRes) + if err != nil { + return err + } + err = common.CreateClusterSet(cmd, k8sClient, memberClusterNamespace, memberClusterSet, joinOpts.LeaderClusterOpt.APIServer, joinOpts.LeaderClusterOpt.Secret, + joinOpts.MemberClusterOpt.ClusterID, joinOpts.LeaderClusterOpt.ClusterID, joinOpts.LeaderClusterOpt.Namespace, &createdRes) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Waiting for member cluster ready\n") + if err = waitForMemberClusterReady(cmd, k8sClient); err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to wait for member cluster ready: %v\n", err) + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Member cluster joined successfully\n") + + return nil +} + +func waitForMemberClusterReady(cmd *cobra.Command, k8sClient client.Client) error { + fmt.Fprintf(cmd.OutOrStdout(), "Waiting for ClusterSet \"%s\" to be registered in leader cluster\n", + joinOpts.MemberClusterOpt.ClusterSetID) + secret := &v1.Secret{} + if err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: joinOpts.LeaderClusterOpt.Secret, Namespace: joinOpts.MemberClusterOpt.Namespace}, secret); err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to get secret \"%s\" in Namespace %s: %v\n", joinOpts.LeaderClusterOpt.Secret, joinOpts.MemberClusterOpt.Namespace, err) + return err + } + + crtData, token, err := commonarea.GetSecretCACrtAndToken(secret) + if err != nil { + return err + } + config, err := clientcmd.BuildConfigFromFlags(joinOpts.LeaderClusterOpt.APIServer, "") + if err != nil { + return err + } + config.BearerToken = string(token) + config.CAData = crtData + + remoteClient, err := client.New(config, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return err + } + + memberClusterAnnounce := "member-announce-from-" + joinOpts.MemberClusterOpt.ClusterID + if err := waitForMemberClusterAnnounceReady(remoteClient, memberClusterAnnounce, joinOpts.LeaderClusterOpt.Namespace); err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to wait for MemberClusterAnnounce \"%s\" in Namespace %s in leader cluster: %v\n", memberClusterAnnounce, joinOpts.LeaderClusterOpt.Namespace, err) + return err + } + + if err := waitForLeaderClusterSetReady(k8sClient, joinOpts.MemberClusterOpt.ClusterSetID, joinOpts.MemberClusterOpt.Namespace, joinOpts.LeaderClusterOpt.ClusterID); err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to wait for ClusterSet \"%s\" in Namespace %s in member cluster: %v\n", joinOpts.MemberClusterOpt.ClusterSetID, joinOpts.MemberClusterOpt.Namespace, err) + return err + } + + return nil +} + +func waitForMemberClusterAnnounceReady(client client.Client, memberClusterAnnounce string, namespace string) error { + return wait.PollImmediate( + 1*time.Second, + 30*time.Second, + func() (bool, error) { + announce := &multiclusterv1alpha1.MemberClusterAnnounce{} + if err := client.Get(context.TODO(), types.NamespacedName{Name: memberClusterAnnounce, Namespace: namespace}, announce); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + return announce.DeletionTimestamp == nil, nil + }) +} + +func waitForLeaderClusterSetReady(client client.Client, name string, namespace string, clusterID string) error { + return wait.PollImmediate( + 1*time.Second, + 3*time.Minute, + func() (bool, error) { + clusterSet := &multiclusterv1alpha1.ClusterSet{} + if err := client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, clusterSet); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + + for _, status := range clusterSet.Status.ClusterStatuses { + if status.ClusterID == clusterID { + for _, cond := range status.Conditions { + if cond.Type == multiclusterv1alpha1.ClusterReady { + return cond.Status == "True", nil + } + } + break + } + } + + return false, nil + }) +} diff --git a/pkg/antctl/raw/multicluster/leave.go b/pkg/antctl/raw/multicluster/leave.go new file mode 100644 index 00000000000..f08ebe194ad --- /dev/null +++ b/pkg/antctl/raw/multicluster/leave.go @@ -0,0 +1,81 @@ +// 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 multicluster + +import ( + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" + "fmt" + "github.com/spf13/cobra" + "strings" +) + +type leaveOptions struct { + namespace string + clusterSet string +} + +var leaveOpts *leaveOptions + +func (o *leaveOptions) validate() error { + if o.namespace == "" { + return fmt.Errorf("Namespace is required") + } + if o.clusterSet == "" { + return fmt.Errorf("ClusterSet is required") + } + + return nil +} + +var leaveExamples = strings.Trim(` +# Leave the ClusterSet + antctl mc leave --namespace kube-system --clusterset test-clusterset +`, "\n") + +func NewLeaveCommand() *cobra.Command { + command := &cobra.Command{ + Use: "leave", + Short: "Leave ClusterSet from a member cluster", + Args: cobra.MaximumNArgs(0), + Example: leaveExamples, + RunE: leaveRunE, + } + + o := leaveOptions{} + leaveOpts = &o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Antrea Multi-cluster Namespace") + command.Flags().StringVarP(&o.clusterSet, "clusterset", "", "", "ClusterSet ID of the leader cluster") + + return command +} + +func leaveRunE(cmd *cobra.Command, args []string) error { + if err := leaveOpts.validate(); err != nil { + return err + } + k8sClient, err := common.NewClient(cmd) + if err != nil { + return err + } + if err := common.DeleteClusterClaim(cmd, k8sClient, leaveOpts.namespace); err != nil { + return err + } + if err := common.DeleteClusterSet(cmd, k8sClient, leaveOpts.namespace, leaveOpts.clusterSet); err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Successfully deleted the Antrea member cluster in Namespace %s\n", leaveOpts.namespace) + + return nil +}