From 5c818664cfe58b6ab038fe49df6f5496fad3de22 Mon Sep 17 00:00:00 2001 From: Bangqi Zhu Date: Mon, 10 Oct 2022 10:52:12 -0700 Subject: [PATCH] fetch and delete token command line Signed-off-by: Bangqi Zhu --- go.mod | 2 +- go.sum | 4 +- pkg/antctl/antctl.go | 6 + pkg/antctl/raw/multicluster/commands.go | 8 + pkg/antctl/raw/multicluster/common/common.go | 117 +++++++++-- .../raw/multicluster/common/common_test.go | 181 ++++++++++++++++++ .../multicluster/create/access_token_test.go | 12 +- .../raw/multicluster/delete/access_token.go | 82 ++++++++ .../multicluster/delete/access_token_test.go | 117 +++++++++++ .../raw/multicluster/get/access_token.go | 98 ++++++++++ .../raw/multicluster/get/access_token_test.go | 120 ++++++++++++ 11 files changed, 727 insertions(+), 20 deletions(-) create mode 100644 pkg/antctl/raw/multicluster/delete/access_token.go create mode 100644 pkg/antctl/raw/multicluster/delete/access_token_test.go create mode 100644 pkg/antctl/raw/multicluster/get/access_token.go create mode 100644 pkg/antctl/raw/multicluster/get/access_token_test.go diff --git a/go.mod b/go.mod index 3943f6fa395..feee895a625 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/golang/protobuf v1.5.2 github.com/google/btree v1.1.2 github.com/google/uuid v1.3.0 - github.com/hashicorp/memberlist v0.4.0 + github.com/hashicorp/memberlist v0.5.0 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.1.1 github.com/k8snetworkplumbingwg/sriov-cni v2.1.0+incompatible github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd diff --git a/go.sum b/go.sum index 19e73afae78..fcf7b06e53e 100644 --- a/go.sum +++ b/go.sum @@ -559,8 +559,8 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.4.0 h1:k3uda5gZcltmafuFF+UFqNEl5PrH+yPZ4zkjp1f/H/8= -github.com/hashicorp/memberlist v0.4.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 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 2d997238599..4cd28226e30 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" ) @@ -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 multi-cluster resources", +} + var JoinCmd = NewJoinCommand() var LeaveCmd = NewLeaveCommand() var InitCmd = NewInitCommand() @@ -46,7 +52,9 @@ func init() { GetCmd.AddCommand(get.NewClusterSetCommand()) GetCmd.AddCommand(get.NewResourceImportCommand()) GetCmd.AddCommand(get.NewResourceExportCommand()) + GetCmd.AddCommand(get.NewTokenCommand()) CreateCmd.AddCommand(create.NewAccessTokenCmd()) DeployCmd.AddCommand(deploy.NewLeaderClusterCmd()) DeployCmd.AddCommand(deploy.NewMemberClusterCmd()) + DeleteCmd.AddCommand(delete.NewDeleteTokenCmd()) } diff --git a/pkg/antctl/raw/multicluster/common/common.go b/pkg/antctl/raw/multicluster/common/common.go index 46f47ab5808..75b7d6b1c58 100644 --- a/pkg/antctl/raw/multicluster/common/common.go +++ b/pkg/antctl/raw/multicluster/common/common.go @@ -34,6 +34,7 @@ import ( multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" multiclusterv1alpha2 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha2" + "antrea.io/antrea/pkg/antctl/output" "antrea.io/antrea/pkg/antctl/raw" multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" ) @@ -213,6 +214,21 @@ func deleteServiceAccounts(cmd *cobra.Command, k8sClient client.Client, namespac } } +func GetTokenSecret(secret *corev1.Secret) *corev1.Secret { + s := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: secret.Name, + }, + 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) @@ -273,17 +289,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 := GetTokenSecret(secret) b, err := yaml.Marshal(s) if err != nil { @@ -303,6 +310,94 @@ func CreateMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, return nil } +func GetMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, namespace string, file *os.File) error { + secret := &corev1.Secret{} + if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, secret); err != nil { + return err + } + + if secret.Annotations[CreateByAntctlAnnotation] == "true" { + s := GetTokenSecret(secret) + + if file != nil { + b, err := yaml.Marshal(s) + if err != nil { + return err + } + if _, err := file.Write([]byte("---\n")); err != nil { + return err + } + if _, err := file.Write(b); err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Member token saved to %s\n", file.Name()) + } else { + output.YamlOutput(s, cmd.OutOrStdout()) + } + return nil + } + fmt.Fprintf(cmd.OutOrStdout(), "\033[1;31;40m Warning: Token %s created by antctl is not found\033[0m\n", name) + return nil +} + +func DeleteMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, namespace string) { + secret := &corev1.Secret{} + getErr := k8sClient.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: name}, secret) + if getErr != nil { + fmt.Fprintf(cmd.OutOrStderr(), "Error from server: Secret \"%s\", error: %s\n", name, getErr) + } + if secret.Annotations[CreateByAntctlAnnotation] == "true" { + deleteErr := k8sClient.Delete(context.TODO(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }}, &client.DeleteOptions{}) + if deleteErr != nil { + fmt.Fprintf(cmd.OutOrStderr(), "Failed to delete Secret \"%s\", error: %s\n", name, deleteErr) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Token %s deleted successfully", name) + } + } + + roleBinding := &rbacv1.RoleBinding{} + getErr = k8sClient.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: name}, roleBinding) + if getErr != nil { + fmt.Fprintf(cmd.OutOrStderr(), "Error from server: RoleBinding \"%s\", error: %s\n", name, getErr) + } + if roleBinding.Annotations[CreateByAntctlAnnotation] == "true" { + deleteErr := k8sClient.Delete(context.TODO(), &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }}, &client.DeleteOptions{}) + + if deleteErr != nil { + fmt.Fprintf(cmd.OutOrStderr(), "Failed to delete RoleBinding \"%s\", error: %s\n", name, deleteErr) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "RoleBinding %s deleted successfully", name) + } + } + + serviceAccount := &corev1.ServiceAccount{} + getErr = k8sClient.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: name}, serviceAccount) + if getErr != nil { + fmt.Fprintf(cmd.OutOrStderr(), "Error from server: ServiceAccount \"%s\", error: %s\n", name, getErr) + } + if serviceAccount.Annotations[CreateByAntctlAnnotation] == "true" { + deleteErr := k8sClient.Delete(context.TODO(), &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }}, &client.DeleteOptions{}) + + if deleteErr != nil { + fmt.Fprintf(cmd.OutOrStderr(), "Failed to delete ServiceAccount \"%s\", error: %s\n", name, deleteErr) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "ServiceAccount %s deleted successfully", name) + } + } +} + 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..d76e0e652b7 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: "Error from server: Secret", + }, + { + 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: "Error from server: RoleBinding", + }, + { + 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: "Error from server: ServiceAccount", + }, + { + 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_test.go b/pkg/antctl/raw/multicluster/create/access_token_test.go index 9b6ec80fd83..fee4b1e1d74 100644 --- a/pkg/antctl/raw/multicluster/create/access_token_test.go +++ b/pkg/antctl/raw/multicluster/create/access_token_test.go @@ -54,17 +54,17 @@ type: Opaque`) expectedOutput string secretFile bool 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, @@ -83,7 +83,7 @@ type: Opaque`) name: "fail to create and rollback", namespace: "default", failureType: "create", - tokeName: "default-member-token", + tokenName: "default-member-token", expectedOutput: "failed to create object", }, } @@ -103,8 +103,8 @@ type: Opaque`) ShouldError: true, } } - if tt.tokeName != "" { - cmd.SetArgs([]string{tt.tokeName}) + if tt.tokenName != "" { + cmd.SetArgs([]string{tt.tokenName}) } if tt.secretFile { secret, err := os.CreateTemp("", "secret") diff --git a/pkg/antctl/raw/multicluster/delete/access_token.go b/pkg/antctl/raw/multicluster/delete/access_token.go new file mode 100644 index 00000000000..a3c66072ec7 --- /dev/null +++ b/pkg/antctl/raw/multicluster/delete/access_token.go @@ -0,0 +1,82 @@ +// 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. + $ 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("the Namespace is required") + } + + var err error + if o.k8sClient == nil { + o.k8sClient, err = common.NewClient(cmd) + if err != nil { + return err + } + } + return nil +} + +func NewDeleteTokenCmd() *cobra.Command { + command := &cobra.Command{ + Use: "membertoken", + Args: cobra.MaximumNArgs(1), + Short: "Delete a member token in a leader cluster", + Long: "Delete a member token in a leader cluster, corresponding ServiceAccount and a RoleBinding will also be deleted", + 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) != 1 { + return fmt.Errorf("exactly one NAME is required, got %d", len(args)) + } + + common.DeleteMemberToken(cmd, deleteTokenOpts.k8sClient, args[0], deleteTokenOpts.namespace) + + return nil +} diff --git a/pkg/antctl/raw/multicluster/delete/access_token_test.go b/pkg/antctl/raw/multicluster/delete/access_token_test.go new file mode 100644 index 00000000000..ea6a5715874 --- /dev/null +++ b/pkg/antctl/raw/multicluster/delete/access_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: "Token default-member-token deleted successfully", + }, + { + name: "fail to delete without name", + namespace: "default", + expectedOutput: "exactly one NAME is required, got 0", + }, + { + name: "fail to delete without namespace", + namespace: "", + expectedOutput: "the Namespace is required", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := NewDeleteTokenCmd() + 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/access_token.go b/pkg/antctl/raw/multicluster/get/access_token.go new file mode 100644 index 00000000000..8f5d5a7c033 --- /dev/null +++ b/pkg/antctl/raw/multicluster/get/access_token.go @@ -0,0 +1,98 @@ +// 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 ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "sigs.k8s.io/controller-runtime/pkg/client" + + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" +) + +type tokenOptions struct { + namespace string + output string + k8sClient client.Client +} + +var cmdToken *cobra.Command + +var optionsToken *tokenOptions + +var tokenExamples = strings.Trim(` +# Fetch a member token and print it in YAML format. + $ antctl mc get membertoken cluster-east-token -n antrea-multicluster +# Fetch a member token and save the Secret manifest to a file. + $ antctl mc get membertoken cluster-east-token -n antrea-multicluster -o token-secret.yml +`, "\n") + +func (o *tokenOptions) validateAndComplete(cmd *cobra.Command) error { + if o.namespace == "" { + return fmt.Errorf("the Namespace is required") + } + + var err error + if o.k8sClient == nil { + o.k8sClient, err = common.NewClient(cmd) + if err != nil { + return err + } + } + return nil +} + +func NewTokenCommand() *cobra.Command { + cmdToken = &cobra.Command{ + Use: "membertoken", + Short: "Fetch a member token 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.output, "output", "o", "", "Output file to save the token Secret manifest") + + return cmdToken +} + +func runEToken(cmd *cobra.Command, args []string) error { + if err := optionsToken.validateAndComplete(cmd); err != nil { + return err + } + + if len(args) != 1 { + return fmt.Errorf("exactly one NAME is required, got %d", len(args)) + } + + var err error + var file *os.File + if optionsToken.output != "" { + if file, err = os.OpenFile(optionsToken.output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil { + return err + } + defer file.Close() + } + + if err = common.GetMemberToken(cmd, optionsToken.k8sClient, args[0], optionsToken.namespace, file); err != nil { + return err + } + return nil +} diff --git a/pkg/antctl/raw/multicluster/get/access_token_test.go b/pkg/antctl/raw/multicluster/get/access_token_test.go new file mode 100644 index 00000000000..eed0ec2a9cc --- /dev/null +++ b/pkg/antctl/raw/multicluster/get/access_token_test.go @@ -0,0 +1,120 @@ +// 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" + "log" + "os" + "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) { + 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}, + } + + tests := []struct { + name string + namespace string + outputfile string + expectedOutput string + secretFile bool + failureType string + tokenName string + }{ + { + name: "fetch successfully", + tokenName: "default-member-token", + namespace: "default", + expectedOutput: "", + }, + { + name: "fetch successfully with file", + tokenName: "default-member-token", + namespace: "default", + expectedOutput: "Member token saved to", + secretFile: true, + }, + { + name: "fail to fetch without name", + namespace: "default", + expectedOutput: "exactly one NAME is required, got 0", + }, + { + name: "fail to fetch without namespace", + namespace: "", + expectedOutput: "the Namespace is required", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := NewTokenCommand() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + optionsToken.namespace = tt.namespace + optionsToken.k8sClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(existingSecret).Build() + + if tt.tokenName != "" { + cmd.SetArgs([]string{tt.tokenName}) + } + if tt.secretFile { + secret, err := os.CreateTemp("", "secret") + if err != nil { + log.Fatal(err) + } + defer os.Remove(secret.Name()) + secret.Write([]byte(secretContent)) + optionsToken.output = secret.Name() + } + err := cmd.Execute() + if err != nil { + if tt.name == "fetch successfully" { + assert.Equal(t, err.Error(), tt.expectedOutput) + } else { + assert.Contains(t, err.Error(), tt.expectedOutput) + } + } else { + assert.Contains(t, buf.String(), tt.expectedOutput) + } + }) + } +}