From ffff951e0cadcb3a48b3f56a3b10819850ab1337 Mon Sep 17 00:00:00 2001 From: Bangqi Zhu Date: Wed, 19 Oct 2022 11:56:06 -0700 Subject: [PATCH] fetch and delete token command line Signed-off-by: Bangqi Zhu --- docs/multicluster/antctl.md | 15 ++ pkg/antctl/antctl.go | 6 + pkg/antctl/raw/multicluster/commands.go | 12 +- pkg/antctl/raw/multicluster/common/common.go | 108 +++++++++-- .../raw/multicluster/common/common_test.go | 181 ++++++++++++++++++ .../{access_token.go => member_token.go} | 14 +- ...ess_token_test.go => member_token_test.go} | 57 +++--- .../raw/multicluster/delete/member_token.go | 84 ++++++++ .../multicluster/delete/member_token_test.go | 117 +++++++++++ pkg/antctl/raw/multicluster/get/clusterset.go | 4 +- .../raw/multicluster/get/clusterset_test.go | 4 +- .../raw/multicluster/get/member_token.go | 149 ++++++++++++++ .../raw/multicluster/get/member_token_test.go | 162 ++++++++++++++++ pkg/antctl/transform/clusterset/transform.go | 2 +- pkg/antctl/transform/membertoken/transform.go | 69 +++++++ 15 files changed, 931 insertions(+), 53 deletions(-) rename pkg/antctl/raw/multicluster/create/{access_token.go => member_token.go} (90%) rename pkg/antctl/raw/multicluster/create/{access_token_test.go => member_token_test.go} (75%) create mode 100644 pkg/antctl/raw/multicluster/delete/member_token.go create mode 100644 pkg/antctl/raw/multicluster/delete/member_token_test.go create mode 100644 pkg/antctl/raw/multicluster/get/member_token.go create mode 100644 pkg/antctl/raw/multicluster/get/member_token_test.go create mode 100644 pkg/antctl/transform/membertoken/transform.go diff --git a/docs/multicluster/antctl.md b/docs/multicluster/antctl.md index d37a9a238ae..ddf5363c442 100644 --- a/docs/multicluster/antctl.md +++ b/docs/multicluster/antctl.md @@ -23,6 +23,9 @@ prints all ResourceExports, a specified ResourceExport, or ResourceExports in a specified Namespace. - `antctl mc get joinconfig` command prints member cluster join parameters of the ClusterSet in a specified leader cluster Namespace. +- `antctl mc get membertoken` (or `get membertokens`) command prints all member tokens, +a specified token, or member tokens in a specified Namespace. The command is supported +only on a leader cluster. Using the `json` or `yaml` antctl output format can print more information of ClusterSet, ResourceImport, and ResourceExport than using the default table @@ -33,6 +36,7 @@ antctl mc get clusterset [NAME] [-n NAMESPACE] [-o json|yaml] [-A] antctl mc get resourceimport [NAME] [-n NAMESPACE] [-o json|yaml] [-A] antctl mc get resourceexport [NAME] [-n NAMESPACE] [-clusterid CLUSTERID] [-o json|yaml] [-A] antctl mc get joinconfig [-n NAMESPACE] +antctl mc get membertoken [NAME] [-n NAMESPACE] [-o json|yaml] [-A] ``` To see the usage examples of these commands, you may also run `antctl mc get [subcommand] --help`. @@ -49,6 +53,17 @@ anctcl mc create membertoken NAME -n NAMESPACE [-o OUTPUT_FILE] To see the usage examples of these commands, you may also run `antctl mc create [subcommand] --help`. +## antctl mc delete + +`antctl mc delete` command deletes a member token of a ClusterSet. The command will delete the +corresponding Secret, ServiceAccount and RoleBinding if they exist. + +```bash +anctcl mc delete membertoken NAME -n NAMESPACE +``` + +To see the usage examples of these commands, you may also run `antctl mc delete [subcommand] --help`. + ## antctl mc deploy `antctl mc deploy` command deploys Antrea Multi-cluster Controller to a leader or member cluster. diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index 1bece42a8d9..089f60d5198 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -633,6 +633,12 @@ $ antctl get podmulticaststats pod -n namespace`, supportController: false, commandGroup: mc, }, + { + cobraCommand: multicluster.DeleteCmd, + supportAgent: false, + supportController: false, + commandGroup: mc, + }, { cobraCommand: set.SetCmd, supportAgent: false, diff --git a/pkg/antctl/raw/multicluster/commands.go b/pkg/antctl/raw/multicluster/commands.go index 4f3864bd2ea..f29992d3614 100644 --- a/pkg/antctl/raw/multicluster/commands.go +++ b/pkg/antctl/raw/multicluster/commands.go @@ -18,6 +18,7 @@ import ( "github.com/spf13/cobra" "antrea.io/antrea/pkg/antctl/raw/multicluster/create" + "antrea.io/antrea/pkg/antctl/raw/multicluster/delete" "antrea.io/antrea/pkg/antctl/raw/multicluster/deploy" "antrea.io/antrea/pkg/antctl/raw/multicluster/get" ) @@ -29,7 +30,7 @@ var GetCmd = &cobra.Command{ var CreateCmd = &cobra.Command{ Use: "create", - Short: "Create multi-cluster resources", + Short: "Create a member token for a ClusterSet", } var DeployCmd = &cobra.Command{ @@ -37,6 +38,11 @@ var DeployCmd = &cobra.Command{ Short: "Deploy Antrea Multi-cluster Controller to a leader or member cluster", } +var DeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a member token", +} + var JoinCmd = NewJoinCommand() var LeaveCmd = NewLeaveCommand() var InitCmd = NewInitCommand() @@ -47,7 +53,9 @@ func init() { GetCmd.AddCommand(get.NewResourceImportCommand()) GetCmd.AddCommand(get.NewResourceExportCommand()) GetCmd.AddCommand(get.NewJoinConfigCommand()) - CreateCmd.AddCommand(create.NewAccessTokenCmd()) + GetCmd.AddCommand(get.NewMemberTokenCommand()) + CreateCmd.AddCommand(create.NewMemberTokenCmd()) DeployCmd.AddCommand(deploy.NewLeaderClusterCmd()) DeployCmd.AddCommand(deploy.NewMemberClusterCmd()) + DeleteCmd.AddCommand(delete.NewMemberTokenCmd()) } diff --git a/pkg/antctl/raw/multicluster/common/common.go b/pkg/antctl/raw/multicluster/common/common.go index bdb4e1fe575..d965a85a19d 100644 --- a/pkg/antctl/raw/multicluster/common/common.go +++ b/pkg/antctl/raw/multicluster/common/common.go @@ -91,7 +91,6 @@ func CreateClusterClaim(cmd *cobra.Command, k8sClient client.Client, namespace s if !apierrors.IsAlreadyExists(createErr) { fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ClusterClaim \"%s\": %v\n", multiclusterv1alpha2.WellKnownClusterClaimID, createErr) return createErr - } fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" already exists in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimID, namespace) createErr = nil @@ -231,6 +230,23 @@ func deleteServiceAccounts(cmd *cobra.Command, k8sClient client.Client, namespac } } +// ConvertMemberTokenSecret generates a token Secret manifest for creating the input Secret in a member cluster. +func ConvertMemberTokenSecret(secret corev1.Secret) corev1.Secret { + s := corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: secret.Name, + Namespace: secret.Namespace, + }, + Data: secret.Data, + Type: corev1.SecretTypeOpaque, + } + return s +} + func CreateMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, namespace string, file *os.File, createdRes *[]map[string]interface{}) error { var createErr error serviceAccount := newServiceAccount(name, namespace) @@ -291,17 +307,8 @@ func CreateMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, secret); err != nil { return err } - s := &corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Data: secret.Data, - Type: corev1.SecretTypeOpaque, - } + + s := ConvertMemberTokenSecret(*secret) b, err := k8syaml.Marshal(s) if err != nil { @@ -321,6 +328,83 @@ func CreateMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, return nil } +func DeleteMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, namespace string) error { + errFunc := func(kind string, act string, err error) error { + if apierrors.IsNotFound(err) { + fmt.Fprintf(cmd.OutOrStdout(), "%s %s not found in Namespace %s\n", kind, name, namespace) + return nil + } + if err != nil { + return err + } + if act == "delete" { + fmt.Fprintf(cmd.OutOrStdout(), "%s %s deleted\n", kind, name) + } + return nil + } + + secret := &corev1.Secret{} + getErr := k8sClient.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: name}, secret) + err := errFunc("Secret", "get", getErr) + if err != nil { + return err + } + if secret.Annotations[CreateByAntctlAnnotation] == "true" { + deleteErr := k8sClient.Delete(context.TODO(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }}, &client.DeleteOptions{}) + err = errFunc("Secret", "delete", deleteErr) + if err != nil { + return err + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Secret %s is not created by antctl, ignoring it", name) + } + + roleBinding := &rbacv1.RoleBinding{} + getErr = k8sClient.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: name}, roleBinding) + err = errFunc("RoleBinding", "get", getErr) + if err != nil { + return err + } + if roleBinding.Annotations[CreateByAntctlAnnotation] == "true" { + deleteErr := k8sClient.Delete(context.TODO(), &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }}, &client.DeleteOptions{}) + err = errFunc("RoleBinding", "delete", deleteErr) + if err != nil { + return err + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "RoleBinding %s is not created by antctl , ignoring it", name) + } + + serviceAccount := &corev1.ServiceAccount{} + getErr = k8sClient.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: name}, serviceAccount) + err = errFunc("ServiceAccount", "get", getErr) + if err != nil { + return err + } + if serviceAccount.Annotations[CreateByAntctlAnnotation] == "true" { + deleteErr := k8sClient.Delete(context.TODO(), &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }}, &client.DeleteOptions{}) + err = errFunc("ServiceAccount", "delete", deleteErr) + if err != nil { + return err + } + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ServiceAccount %s is not created by antctl, ignoring it", name) + } + return nil +} + func waitForSecretReady(client client.Client, secretName string, namespace string) error { return wait.PollImmediate( 1*time.Second, diff --git a/pkg/antctl/raw/multicluster/common/common_test.go b/pkg/antctl/raw/multicluster/common/common_test.go index 8031e3cc885..79b62e7b6ac 100644 --- a/pkg/antctl/raw/multicluster/common/common_test.go +++ b/pkg/antctl/raw/multicluster/common/common_test.go @@ -429,3 +429,184 @@ func TestCreateMemberToken(t *testing.T) { }) } } + +func TestDeleteMemberToken(t *testing.T) { + secretContent := []byte(`apiVersion: v1 +kind: Secret +metadata: + name: default-member-token +data: + ca.crt: YWJjZAo= + namespace: ZGVmYXVsdAo= + token: YWJjZAo= +type: Opaque`) + + existingSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + Annotations: map[string]string{ + CreateByAntctlAnnotation: "true", + }, + }, + Data: map[string][]byte{"token": secretContent}, + } + + existingSecretNoAnnotation := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + }, + Data: map[string][]byte{"token": secretContent}, + } + + existingSecret1 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token-1", + Annotations: map[string]string{ + CreateByAntctlAnnotation: "true", + }, + }, + Data: map[string][]byte{"token": secretContent}, + } + + existingRolebinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + Annotations: map[string]string{ + CreateByAntctlAnnotation: "true", + }, + }, + } + + existingRolebinding1 := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token-notexist", + Annotations: map[string]string{ + CreateByAntctlAnnotation: "true", + }, + }, + } + + existingServiceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + Annotations: map[string]string{ + CreateByAntctlAnnotation: "true", + }, + }, + } + + existingServiceAccount1 := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token-notexist", + Annotations: map[string]string{ + CreateByAntctlAnnotation: "true", + }, + }, + } + + tests := []struct { + name string + namespace string + tokenName string + serviceAccount *corev1.ServiceAccount + rolebinding *rbacv1.RoleBinding + secret *corev1.Secret + numsOfServiceAccount int + numsOfRolebinding int + numsOfSecret int + expectedOutput string + }{ + { + name: "delete successfully", + tokenName: "default-member-token", + namespace: "default", + secret: existingSecret, + rolebinding: existingRolebinding, + serviceAccount: existingServiceAccount, + numsOfServiceAccount: 0, + numsOfRolebinding: 0, + numsOfSecret: 0, + expectedOutput: "", + }, + { + name: "failed to delete because of wrong secret name", + tokenName: "default-member-token", + namespace: "default", + secret: existingSecret1, + rolebinding: existingRolebinding, + serviceAccount: existingServiceAccount, + numsOfSecret: 1, + numsOfRolebinding: 0, + numsOfServiceAccount: 0, + expectedOutput: "Secret default-member-token not found in Namespace default", + }, + { + name: "failed to delete because of wrong rolebinding name", + tokenName: "default-member-token", + namespace: "default", + secret: existingSecret, + rolebinding: existingRolebinding1, + serviceAccount: existingServiceAccount, + numsOfSecret: 0, + numsOfRolebinding: 1, + numsOfServiceAccount: 0, + expectedOutput: "RoleBinding default-member-token not found in Namespace default", + }, + { + name: "failed to delete because of wrong serviceaccount name", + tokenName: "default-member-token", + namespace: "default", + secret: existingSecret, + rolebinding: existingRolebinding, + serviceAccount: existingServiceAccount1, + numsOfSecret: 0, + numsOfRolebinding: 0, + numsOfServiceAccount: 1, + expectedOutput: "ServiceAccount default-member-token not found in Namespace default", + }, + { + name: "the secret does not have the require annotation", + tokenName: "default-member-token", + namespace: "default", + secret: existingSecretNoAnnotation, + rolebinding: existingRolebinding, + serviceAccount: existingServiceAccount, + numsOfServiceAccount: 0, + numsOfRolebinding: 0, + numsOfSecret: 1, + expectedOutput: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{} + fakeClient := fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).WithObjects(tt.secret, tt.rolebinding, tt.serviceAccount).Build() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + DeleteMemberToken(cmd, fakeClient, tt.tokenName, tt.namespace) + + assert.Contains(t, buf.String(), tt.expectedOutput) + + remainSecrets := &corev1.SecretList{} + fakeClient.List(context.Background(), remainSecrets, &client.ListOptions{}) + assert.Equal(t, tt.numsOfSecret, len(remainSecrets.Items)) + remainRoleBinding := &rbacv1.RoleBindingList{} + fakeClient.List(context.Background(), remainRoleBinding, &client.ListOptions{}) + assert.Equal(t, tt.numsOfRolebinding, len(remainRoleBinding.Items)) + remainServiceAccount := &corev1.ServiceAccountList{} + fakeClient.List(context.Background(), remainServiceAccount, &client.ListOptions{}) + assert.Equal(t, tt.numsOfServiceAccount, len(remainServiceAccount.Items)) + }) + } +} diff --git a/pkg/antctl/raw/multicluster/create/access_token.go b/pkg/antctl/raw/multicluster/create/member_token.go similarity index 90% rename from pkg/antctl/raw/multicluster/create/access_token.go rename to pkg/antctl/raw/multicluster/create/member_token.go index 5a597902dde..96fe8dfe557 100644 --- a/pkg/antctl/raw/multicluster/create/access_token.go +++ b/pkg/antctl/raw/multicluster/create/member_token.go @@ -34,15 +34,15 @@ type memberTokenOptions struct { var memberTokenOpts *memberTokenOptions var memberTokenExamples = strings.Trim(` -# Create a member token. +# Create a member token in the antrea-multicluster Namespace $ antctl mc create membertoken cluster-east-token -n antrea-multicluster -# Create a member token and save the Secret manifest to a file. +# Create a member token and save the Secret manifest to a file $ antctl mc create membertoken cluster-east-token -n antrea-multicluster -o token-secret.yml `, "\n") func (o *memberTokenOptions) validateAndComplete(cmd *cobra.Command) error { if o.namespace == "" { - return fmt.Errorf("the Namespace is required") + return fmt.Errorf("Namespace is required") } var err error if o.k8sClient == nil { @@ -54,12 +54,12 @@ func (o *memberTokenOptions) validateAndComplete(cmd *cobra.Command) error { return nil } -func NewAccessTokenCmd() *cobra.Command { +func NewMemberTokenCmd() *cobra.Command { command := &cobra.Command{ Use: "membertoken", Args: cobra.MaximumNArgs(1), Short: "Create a member token in a leader cluster", - Long: "Create a member token in a leader cluster, which will be saved in a Secret. A ServiceAccount and a RoleBinding will be created too if they do not exist.", + Long: "Create a member token in a leader cluster, which will be saved in a Secret. A ServiceAccount and a RoleBinding will be created too.", Example: memberTokenExamples, RunE: memberTokenRunE, } @@ -76,8 +76,8 @@ func memberTokenRunE(cmd *cobra.Command, args []string) error { if err := memberTokenOpts.validateAndComplete(cmd); err != nil { return err } - if len(args) != 1 { - return fmt.Errorf("exactly one NAME is required, got %d", len(args)) + if len(args) == 0 { + return fmt.Errorf("token name must be specified") } var createErr error createdRes := []map[string]interface{}{} diff --git a/pkg/antctl/raw/multicluster/create/access_token_test.go b/pkg/antctl/raw/multicluster/create/member_token_test.go similarity index 75% rename from pkg/antctl/raw/multicluster/create/access_token_test.go rename to pkg/antctl/raw/multicluster/create/member_token_test.go index 9b6ec80fd83..f9213aa9d49 100644 --- a/pkg/antctl/raw/multicluster/create/access_token_test.go +++ b/pkg/antctl/raw/multicluster/create/member_token_test.go @@ -16,7 +16,6 @@ package create import ( "bytes" - "log" "os" "testing" @@ -38,58 +37,61 @@ func TestCreateAccessToken(t *testing.T) { Data: map[string][]byte{"token": []byte("12345")}, } - secretContent := []byte(`apiVersion: v1 + secretContent := []byte(`# Manifest to create a Secret for an Antrea Multi-cluster member cluster token. +--- +apiVersion: v1 +data: + token: MTIzNDU= kind: Secret metadata: + creationTimestamp: null name: default-member-token -data: - ca.crt: YWJjZAo= - namespace: ZGVmYXVsdAo= - token: YWJjZAo= -type: Opaque`) + namespace: default +type: Opaque +`) tests := []struct { name string namespace string expectedOutput string - secretFile bool + secretFile string failureType string - tokeName string + tokenName string }{ { name: "create successfully", - tokeName: "default-member-token", + tokenName: "default-member-token", namespace: "default", expectedOutput: "You can now run the \"antctl mc join\" command with the token to have the cluster join the ClusterSet\n", }, { name: "create successfully with file", - tokeName: "default-member-token", + tokenName: "default-member-token", namespace: "default", expectedOutput: "You can now run the \"antctl mc join\" command with the token to have the cluster join the ClusterSet\n", - secretFile: true, + secretFile: "test.yml", }, { name: "fail to create without name", namespace: "default", - expectedOutput: "exactly one NAME is required, got 0", + expectedOutput: "token name must be specified", }, { - name: "fail to create without namespace", + name: "fail to create without Namespace", namespace: "", - expectedOutput: "the Namespace is required", + expectedOutput: "Namespace is required", }, { name: "fail to create and rollback", namespace: "default", failureType: "create", - tokeName: "default-member-token", + tokenName: "default-member-token", expectedOutput: "failed to create object", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cmd := NewAccessTokenCmd() + cmd := NewMemberTokenCmd() buf := new(bytes.Buffer) cmd.SetOutput(buf) cmd.SetOut(buf) @@ -103,19 +105,20 @@ type: Opaque`) ShouldError: true, } } - if tt.tokeName != "" { - cmd.SetArgs([]string{tt.tokeName}) + if tt.secretFile != "" { + memberTokenOpts.output = tt.secretFile } - if tt.secretFile { - secret, err := os.CreateTemp("", "secret") - if err != nil { - log.Fatal(err) - } - defer os.Remove(secret.Name()) - secret.Write([]byte(secretContent)) - memberTokenOpts.output = secret.Name() + + if tt.tokenName != "" { + cmd.SetArgs([]string{tt.tokenName}) } err := cmd.Execute() + if tt.secretFile != "" { + defer os.Remove(tt.secretFile) + yamlFile, _ := os.ReadFile(tt.secretFile) + + assert.Equal(t, string(yamlFile), string(secretContent)) + } if err != nil { assert.Contains(t, err.Error(), tt.expectedOutput) } else { diff --git a/pkg/antctl/raw/multicluster/delete/member_token.go b/pkg/antctl/raw/multicluster/delete/member_token.go new file mode 100644 index 00000000000..d6aaf71d270 --- /dev/null +++ b/pkg/antctl/raw/multicluster/delete/member_token.go @@ -0,0 +1,84 @@ +// 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 ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "sigs.k8s.io/controller-runtime/pkg/client" + + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" +) + +type deleteTokenOptions struct { + namespace string + k8sClient client.Client +} + +var deleteTokenOpts *deleteTokenOptions + +var deleteTokenExamples = strings.Trim(` +# Delete a member token in the antrea-multicluster Namespace + $ antctl mc delete membertoken cluster-east-token -n antrea-multicluster +`, "\n") + +func (o *deleteTokenOptions) validateAndComplete(cmd *cobra.Command) error { + if o.namespace == "" { + return fmt.Errorf("Namespace is required") + } + + var err error + if o.k8sClient == nil { + o.k8sClient, err = common.NewClient(cmd) + if err != nil { + return err + } + } + return nil +} + +func NewMemberTokenCmd() *cobra.Command { + command := &cobra.Command{ + Use: "membertoken", + Args: cobra.MaximumNArgs(1), + Short: "Delete a member token in a leader cluster Namespace", + Long: "Delete a member token in a leader cluster Namespace. Corresponding Secret, ServiceAccount and RoleBinding will be deleted if they exist.", + Example: deleteTokenExamples, + RunE: deleteTokenRunE, + } + + o := &deleteTokenOptions{} + deleteTokenOpts = o + command.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of the token") + + return command +} + +func deleteTokenRunE(cmd *cobra.Command, args []string) error { + if err := deleteTokenOpts.validateAndComplete(cmd); err != nil { + return err + } + if len(args) == 0 { + return fmt.Errorf("token name must be specified") + } + + if err := common.DeleteMemberToken(cmd, deleteTokenOpts.k8sClient, args[0], deleteTokenOpts.namespace); err != nil { + return err + } + + return nil +} diff --git a/pkg/antctl/raw/multicluster/delete/member_token_test.go b/pkg/antctl/raw/multicluster/delete/member_token_test.go new file mode 100644 index 00000000000..be85084a723 --- /dev/null +++ b/pkg/antctl/raw/multicluster/delete/member_token_test.go @@ -0,0 +1,117 @@ +// 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 ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestDeleteToken(t *testing.T) { + secretContent := []byte(`apiVersion: v1 +kind: Secret +metadata: + name: default-member-token +data: + ca.crt: YWJjZAo= + namespace: ZGVmYXVsdAo= + token: YWJjZAo= +type: Opaque`) + existingSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + Annotations: map[string]string{ + "multicluster.antrea.io/created-by-antctl": "true", + }, + }, + Data: map[string][]byte{"token": secretContent}, + } + + existingRolebinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + Annotations: map[string]string{ + "multicluster.antrea.io/created-by-antctl": "true", + }, + }, + } + + existingServiceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + Annotations: map[string]string{ + "multicluster.antrea.io/created-by-antctl": "true", + }, + }, + } + + tests := []struct { + name string + namespace string + expectedOutput string + tokenName string + }{ + { + name: "delete successfully", + tokenName: "default-member-token", + namespace: "default", + expectedOutput: "Secret default-member-token deleted", + }, + { + name: "fail to delete without name", + namespace: "default", + expectedOutput: "token name must be specified", + }, + { + name: "fail to delete without namespace", + namespace: "", + expectedOutput: "Namespace is required", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := NewMemberTokenCmd() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + deleteTokenOpts.namespace = tt.namespace + deleteTokenOpts.k8sClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(existingSecret, existingRolebinding, existingServiceAccount).Build() + + if tt.tokenName != "" { + cmd.SetArgs([]string{tt.tokenName}) + } + + err := cmd.Execute() + if err != nil { + assert.Contains(t, err.Error(), tt.expectedOutput) + } else { + assert.Contains(t, buf.String(), tt.expectedOutput) + } + }) + } +} diff --git a/pkg/antctl/raw/multicluster/get/clusterset.go b/pkg/antctl/raw/multicluster/get/clusterset.go index 9c2834c4fda..ca13c608685 100644 --- a/pkg/antctl/raw/multicluster/get/clusterset.go +++ b/pkg/antctl/raw/multicluster/get/clusterset.go @@ -42,7 +42,7 @@ type clusterSetOptions struct { var optionsClusterSet *clusterSetOptions var clusterSetExamples = strings.Trim(` -Gel all ClusterSets in default Namesapce +Gel all ClusterSets in the default Namesapce $ antctl mc get clusterset Get all ClusterSets in all Namespaces $ antctl mc get clusterset -A @@ -51,7 +51,7 @@ $ antctl mc get clusterset -n Get all ClusterSets and print them in JSON format $ antctl mc get clusterset -o json Get the specified ClusterSet -$ antctl mc get clusterset +$ antctl mc get clusterset `, "\n") func (o *clusterSetOptions) validateAndComplete(cmd *cobra.Command) error { diff --git a/pkg/antctl/raw/multicluster/get/clusterset_test.go b/pkg/antctl/raw/multicluster/get/clusterset_test.go index 16ceb8db62d..24b70ace397 100644 --- a/pkg/antctl/raw/multicluster/get/clusterset_test.go +++ b/pkg/antctl/raw/multicluster/get/clusterset_test.go @@ -52,7 +52,7 @@ func TestGetClusterSet(t *testing.T) { name: "get single ClusterSet", existingClusterSets: clusterSetList, args: []string{"clusterset-name"}, - expectedOutput: "CLUSTER-ID NAMESPACE CLUSTER-SET-ID TYPE STATUS REASON\n default clusterset-name \n", + expectedOutput: "CLUSTER-ID NAMESPACE CLUSTERSET-ID TYPE STATUS REASON\n default clusterset-name \n", }, { name: "get single ClusterSet with json output", @@ -107,7 +107,7 @@ func TestGetClusterSet(t *testing.T) { }, }, }, - expectedOutput: "CLUSTER-ID NAMESPACE CLUSTER-SET-ID TYPE STATUS REASON \n default clusterset-name \ncluster-a kube-system clusterset-1 Ready True Connected\n", + expectedOutput: "CLUSTER-ID NAMESPACE CLUSTERSET-ID TYPE STATUS REASON \n default clusterset-name \ncluster-a kube-system clusterset-1 Ready True Connected\n", }, { name: "get all ClusterSets but empty result", diff --git a/pkg/antctl/raw/multicluster/get/member_token.go b/pkg/antctl/raw/multicluster/get/member_token.go new file mode 100644 index 00000000000..a8ee75f8952 --- /dev/null +++ b/pkg/antctl/raw/multicluster/get/member_token.go @@ -0,0 +1,149 @@ +// 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 get + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" + "antrea.io/antrea/pkg/antctl/transform/membertoken" +) + +type tokenOptions struct { + namespace string + outputFormat string + allNamespaces bool + k8sClient client.Client +} + +var cmdToken *cobra.Command + +var optionsToken *tokenOptions + +var tokenExamples = strings.Trim(` +# Get all member tokens in the specified Namespace + $ antctl mc get membertoken -n antrea-multicluster +# Get all member tokens in all Namespaces + $ antctl mc get membertoken -A +# Get the specified member token + $ antctl mc get membertoken cluster-east-token -n antrea-multicluster +# Get the specified member token and print the token Secret in YAML format + $ antctl mc get membertoken cluster-east-token -n antrea-multicluster -o yaml +`, "\n") + +func (o *tokenOptions) validateAndComplete(cmd *cobra.Command) error { + if o.namespace == "" && !o.allNamespaces { + return fmt.Errorf("Namespace is required") + } + if o.allNamespaces { + o.namespace = metav1.NamespaceAll + } + + var err error + if o.k8sClient == nil { + o.k8sClient, err = common.NewClient(cmd) + if err != nil { + return err + } + } + return nil +} + +func NewMemberTokenCommand() *cobra.Command { + cmdToken = &cobra.Command{ + Use: "membertoken", + Aliases: []string{ + "membertokens", + }, + Short: "Print member tokens in a leader cluster", + Args: cobra.MaximumNArgs(1), + Example: tokenExamples, + RunE: runEToken, + } + o := &tokenOptions{} + optionsToken = o + cmdToken.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of the token") + cmdToken.Flags().StringVarP(&o.outputFormat, "output", "o", "", "Output format. Supported formats: json|yaml") + cmdToken.Flags().BoolVarP(&o.allNamespaces, "all-namespaces", "A", false, "Get tokens across all Namespaces") + return cmdToken +} + +func runEToken(cmd *cobra.Command, args []string) error { + err := optionsToken.validateAndComplete(cmd) + if err != nil { + return err + } + + var memberTokens []corev1.Secret + singleToken := false + + if len(args) > 0 { + singleToken = true + memberTokenName := args[0] + memberToken := corev1.Secret{} + err = optionsToken.k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: optionsToken.namespace, + Name: memberTokenName, + }, &memberToken) + if err != nil { + return err + } + + memberTokens = append(memberTokens, memberToken) + } else { + memberTokenList := &corev1.SecretList{} + err = optionsToken.k8sClient.List(context.TODO(), memberTokenList, &client.ListOptions{Namespace: optionsToken.namespace}) + if err != nil { + return err + } + memberTokens = memberTokenList.Items + } + + opaqueMemberTokens := []corev1.Secret{} + for _, memberToken := range memberTokens { + if memberToken.Annotations[common.CreateByAntctlAnnotation] == "true" { + opaqueToken := common.ConvertMemberTokenSecret(memberToken) + opaqueMemberTokens = append(opaqueMemberTokens, opaqueToken) + } + } + + if len(opaqueMemberTokens) == 0 { + if singleToken { + return fmt.Errorf("Member token %s created by antctl is not found", args[0]) + } + if optionsToken.namespace != "" { + fmt.Fprintf(cmd.ErrOrStderr(), "No token found in Namespace %s\n", optionsToken.namespace) + } else { + fmt.Fprintln(cmd.ErrOrStderr(), "No token found") + } + return nil + } + + if singleToken { + opaqueMemberToken := opaqueMemberTokens[0] + err = output(opaqueMemberToken, singleToken, optionsToken.outputFormat, cmd.OutOrStdout(), membertoken.Transform) + } else { + err = output(opaqueMemberTokens, singleToken, optionsToken.outputFormat, cmd.OutOrStdout(), membertoken.Transform) + } + return err +} diff --git a/pkg/antctl/raw/multicluster/get/member_token_test.go b/pkg/antctl/raw/multicluster/get/member_token_test.go new file mode 100644 index 00000000000..a270054eede --- /dev/null +++ b/pkg/antctl/raw/multicluster/get/member_token_test.go @@ -0,0 +1,162 @@ +// 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 get + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestGetAccessToken(t *testing.T) { + secretList := &corev1.SecretList{ + Items: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + Annotations: map[string]string{ + "multicluster.antrea.io/created-by-antctl": "true", + }, + }, + Data: map[string][]byte{"token": []byte("12345")}, + }, + }, + } + + secretListNoAnnotations := &corev1.SecretList{ + Items: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + }, + Data: map[string][]byte{"token": []byte("12345")}, + }, + }, + } + + tests := []struct { + name string + existingSecrets *corev1.SecretList + output string + args []string + expectedOutput string + namespace string + allNamespaces bool + }{ + { + name: "get single Secret", + existingSecrets: secretList, + args: []string{"default-member-token"}, + namespace: "default", + expectedOutput: "NAMESPACE NAME \ndefault default-member-token\n", + }, + { + name: "get single Secret with json output", + existingSecrets: secretList, + args: []string{"default-member-token"}, + namespace: "default", + output: "json", + expectedOutput: "[\n {\n \"kind\": \"Secret\",\n \"apiVersion\": \"v1\",\n \"metadata\": {\n \"name\": \"default-member-token\",\n \"namespace\": \"default\",\n \"creationTimestamp\": null\n },\n \"data\": {\n \"token\": \"MTIzNDU=\"\n },\n \"type\": \"Opaque\"\n }\n]\n", + }, + { + name: "get single Secret with yaml output", + existingSecrets: secretList, + args: []string{"default-member-token"}, + namespace: "default", + output: "yaml", + expectedOutput: "- apiVersion: v1\n data:\n token: MTIzNDU=\n kind: Secret\n metadata:\n creationTimestamp: null\n name: default-member-token\n namespace: default\n type: Opaque\n", + }, + { + name: "get non-existing Secret", + args: []string{"default-member-token"}, + namespace: "default", + expectedOutput: "No token found in Namespace default\n", + }, + { + name: "get all Secret but empty result", + existingSecrets: secretListNoAnnotations, + args: []string{"default-member-token"}, + namespace: "default", + expectedOutput: "No token found in Namespace default\n", + }, + { + name: "get all Secrets", + allNamespaces: true, + existingSecrets: &corev1.SecretList{ + Items: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + Annotations: map[string]string{ + "multicluster.antrea.io/created-by-antctl": "true", + }, + }, + Data: map[string][]byte{"token": []byte("12345")}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default1", + Name: "default-member-token1", + Annotations: map[string]string{ + "multicluster.antrea.io/created-by-antctl": "true", + }, + }, + Data: map[string][]byte{"token": []byte("123451")}, + }, + }, + }, + expectedOutput: "NAMESPACE NAME \ndefault default-member-token \ndefault1 default-member-token1\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := NewMemberTokenCommand() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + fakeClient := fake.NewClientBuilder().WithScheme(mcscheme.Scheme).Build() + if tt.existingSecrets != nil { + fakeClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithLists(tt.existingSecrets).Build() + } + optionsToken.k8sClient = fakeClient + if tt.allNamespaces { + optionsToken.allNamespaces = true + } + if tt.namespace != "" { + optionsToken.namespace = tt.namespace + } + if tt.output != "" { + optionsToken.outputFormat = tt.output + } + err := cmd.Execute() + if err != nil { + assert.Equal(t, tt.expectedOutput, err.Error()) + } else { + assert.Equal(t, tt.expectedOutput, buf.String()) + } + }) + } +} diff --git a/pkg/antctl/transform/clusterset/transform.go b/pkg/antctl/transform/clusterset/transform.go index 9d0c26a0f08..1f0d4f52385 100644 --- a/pkg/antctl/transform/clusterset/transform.go +++ b/pkg/antctl/transform/clusterset/transform.go @@ -76,7 +76,7 @@ func objectTransform(clusterSet multiclusterv1alpha1.ClusterSet, status multiclu var _ common.TableOutput = new(Response) func (r Response) GetTableHeader() []string { - return []string{"CLUSTER-ID", "NAMESPACE", "CLUSTER-SET-ID", "TYPE", "STATUS", "REASON"} + return []string{"CLUSTER-ID", "NAMESPACE", "CLUSTERSET-ID", "TYPE", "STATUS", "REASON"} } func (r Response) GetTableRow(maxColumnLength int) []string { diff --git a/pkg/antctl/transform/membertoken/transform.go b/pkg/antctl/transform/membertoken/transform.go new file mode 100644 index 00000000000..0915736cf2f --- /dev/null +++ b/pkg/antctl/transform/membertoken/transform.go @@ -0,0 +1,69 @@ +// 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 membertoken + +import ( + corev1 "k8s.io/api/core/v1" + + "antrea.io/antrea/pkg/antctl/transform/common" +) + +type Response struct { + Name string `json:"name" yaml:"name"` + Namespace string `json:"namespace" yaml:"namespace"` +} + +func Transform(r interface{}, single bool) (interface{}, error) { + if single { + return objectTransform(r) + } + return listTransform(r) +} + +func listTransform(l interface{}) (interface{}, error) { + secrets := l.([]corev1.Secret) + var result []interface{} + + for i := range secrets { + item := secrets[i] + o, _ := objectTransform(item) + result = append(result, o.(Response)) + } + + return result, nil +} + +func objectTransform(o interface{}) (interface{}, error) { + secret := o.(corev1.Secret) + + return Response{ + Namespace: secret.Namespace, + Name: secret.Name, + }, nil +} + +var _ common.TableOutput = new(Response) + +func (r Response) GetTableHeader() []string { + return []string{"NAMESPACE", "NAME"} +} + +func (r Response) GetTableRow(maxColumnLength int) []string { + return []string{r.Namespace, r.Name} +} + +func (r Response) SortRows() bool { + return true +}