diff --git a/docs/multicluster/quick-start.md b/docs/multicluster/quick-start.md index 2cabe89af3c..d1af384d8b8 100644 --- a/docs/multicluster/quick-start.md +++ b/docs/multicluster/quick-start.md @@ -55,7 +55,8 @@ achieve the same using YAML manifests. To execute any command in this section, `antctl` needs access to the target cluster's API server, and it needs a kubeconfig file for that. Please refer to the [`antctl` Multi-cluster manual](antctl.md) to learn more about the -kubeconfig file configuration, and the `antctl` Multi-cluster commands. +kubeconfig file configuration, and the `antctl` Multi-cluster commands. For +installation of `antctl`, please refer to the [installation guide](../antctl.md#installation). ### Set up Leader and Member in Cluster A diff --git a/pkg/antctl/raw/helper.go b/pkg/antctl/raw/helper.go index d6f254d63b7..b38ca4f48bb 100644 --- a/pkg/antctl/raw/helper.go +++ b/pkg/antctl/raw/helper.go @@ -59,8 +59,8 @@ func ResolveKubeconfig(cmd *cobra.Command) (*rest.Config, error) { return kubeconfig, nil } -// TODO: generate kubeconfig in Antrea agent for antctl in-Pod access. func SetupKubeconfig(kubeconfig *rest.Config) { + // TODO: generate kubeconfig in Antrea agent for antctl in-Pod access. kubeconfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion() kubeconfig.Insecure = true kubeconfig.CAFile = "" diff --git a/pkg/antctl/raw/multicluster/common/cleanup.go b/pkg/antctl/raw/multicluster/common/cleanup.go index 82d3eadff96..a2054ed4eff 100644 --- a/pkg/antctl/raw/multicluster/common/cleanup.go +++ b/pkg/antctl/raw/multicluster/common/cleanup.go @@ -21,34 +21,44 @@ import ( "fmt" "github.com/spf13/cobra" + "sigs.k8s.io/controller-runtime/pkg/client" ) type CleanOptions struct { Namespace string ClusterSet string + K8sClient client.Client } -func (o *CleanOptions) validate() error { +func (o *CleanOptions) validate(cmd *cobra.Command) error { if o.ClusterSet == "" { - return fmt.Errorf("ClusterSet is required") + return fmt.Errorf("the ClusterSet is required") } + if o.Namespace == "" { + return fmt.Errorf("the Namespace is required") + } + + var err error + if o.K8sClient == nil { + o.K8sClient, err = NewClient(cmd) + if err != nil { + return err + } + } return nil } func Cleanup(cmd *cobra.Command, cleanOpts *CleanOptions) error { - if err := cleanOpts.validate(); err != nil { + if err := cleanOpts.validate(cmd); err != nil { return err } - k8sClient, err := NewClient(cmd) - if err != nil { - return err - } - deleteClusterSet(cmd, k8sClient, cleanOpts.Namespace, cleanOpts.ClusterSet) - deleteClusterClaims(cmd, k8sClient, cleanOpts.Namespace) - deleteSecrets(cmd, k8sClient, cleanOpts.Namespace) - deleteServiceAccounts(cmd, k8sClient, cleanOpts.Namespace) - deleteRoleBindings(cmd, k8sClient, cleanOpts.Namespace) + + deleteClusterSet(cmd, cleanOpts.K8sClient, cleanOpts.Namespace, cleanOpts.ClusterSet) + deleteClusterClaims(cmd, cleanOpts.K8sClient, cleanOpts.Namespace) + deleteSecrets(cmd, cleanOpts.K8sClient, cleanOpts.Namespace) + deleteServiceAccounts(cmd, cleanOpts.K8sClient, cleanOpts.Namespace) + deleteRoleBindings(cmd, cleanOpts.K8sClient, cleanOpts.Namespace) return nil } diff --git a/pkg/antctl/raw/multicluster/common/cleanup_test.go b/pkg/antctl/raw/multicluster/common/cleanup_test.go new file mode 100644 index 00000000000..e25eb9769d2 --- /dev/null +++ b/pkg/antctl/raw/multicluster/common/cleanup_test.go @@ -0,0 +1,53 @@ +// 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 ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestValidation(t *testing.T) { + tests := []struct { + name string + expectedOutput string + opts *CleanOptions + }{ + { + name: "empty ClusterSet", + expectedOutput: "the ClusterSet is required", + opts: &CleanOptions{}, + }, + { + name: "empty Namespace", + expectedOutput: "the Namespace is required", + opts: &CleanOptions{ClusterSet: "test"}, + }, + } + + cmd := &cobra.Command{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.opts.validate(cmd) + if err != nil { + assert.Equal(t, tt.expectedOutput, err.Error()) + } else { + t.Error("Expected to get error but got nil") + } + }) + } +} diff --git a/pkg/antctl/raw/multicluster/common/common.go b/pkg/antctl/raw/multicluster/common/common.go index f962af32563..46f47ab5808 100644 --- a/pkg/antctl/raw/multicluster/common/common.go +++ b/pkg/antctl/raw/multicluster/common/common.go @@ -43,6 +43,9 @@ const ( ClusterSetJoinConfigKind = "ClusterSetJoinConfig" CreateByAntctlAnnotation = "multicluster.antrea.io/created-by-antctl" + + DefaultMemberNamespace = "kube-system" + DefaultLeaderNamespace = "antrea-multicluster" ) func NewClient(cmd *cobra.Command) (client.Client, error) { @@ -65,7 +68,6 @@ func CreateClusterClaim(cmd *cobra.Command, k8sClient client.Client, namespace s var createErr error var unstructuredClusterClaim map[string]interface{} clusterClaim := newClusterClaim(clusterID, namespace, false) - unstructuredClusterClaim, _ = runtime.DefaultUnstructuredConverter.ToUnstructured(clusterClaim) if createErr = k8sClient.Create(context.TODO(), clusterClaim); createErr != nil { if !apierrors.IsAlreadyExists(createErr) { @@ -77,12 +79,11 @@ func CreateClusterClaim(cmd *cobra.Command, k8sClient client.Client, namespace s createErr = nil } else { fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" created in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimID, namespace) + unstructuredClusterClaim, _ = runtime.DefaultUnstructuredConverter.ToUnstructured(clusterClaim) *createdRes = append(*createdRes, unstructuredClusterClaim) } clusterClaim = newClusterClaim(clusterset, namespace, true) - unstructuredClusterClaim, _ = runtime.DefaultUnstructuredConverter.ToUnstructured(clusterClaim) - if createErr = k8sClient.Create(context.TODO(), clusterClaim); createErr != nil { if !apierrors.IsAlreadyExists(createErr) { fmt.Fprintf(cmd.OutOrStdout(), "Failed to create ClusterClaim \"%s\": %v\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, createErr) @@ -92,6 +93,7 @@ func CreateClusterClaim(cmd *cobra.Command, k8sClient client.Client, namespace s createErr = nil } else { fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" created in Namespace %s\n", multiclusterv1alpha2.WellKnownClusterClaimClusterSet, namespace) + unstructuredClusterClaim, _ = runtime.DefaultUnstructuredConverter.ToUnstructured(clusterClaim) *createdRes = append(*createdRes, unstructuredClusterClaim) } @@ -100,9 +102,7 @@ func CreateClusterClaim(cmd *cobra.Command, k8sClient client.Client, namespace s func CreateClusterSet(cmd *cobra.Command, k8sClient client.Client, namespace string, clusterset string, leaderServer string, secret string, memberClusterID string, leaderClusterID string, leaderClusterNamespace string, createdRes *[]map[string]interface{}) error { - var unstructuredClusterSet map[string]interface{} clusterSet := newClusterSet(clusterset, namespace, leaderServer, secret, memberClusterID, leaderClusterID, leaderClusterNamespace) - unstructuredClusterSet, _ = runtime.DefaultUnstructuredConverter.ToUnstructured(clusterSet) if err := k8sClient.Create(context.TODO(), clusterSet); err != nil { if !apierrors.IsAlreadyExists(err) { @@ -112,41 +112,49 @@ func CreateClusterSet(cmd *cobra.Command, k8sClient client.Client, namespace str fmt.Fprintf(cmd.OutOrStdout(), "ClusterSet \"%s\" already exists in Namespace %s\n", clusterSet.Name, clusterSet.Namespace) } else { fmt.Fprintf(cmd.OutOrStdout(), "ClusterSet \"%s\" created in Namespace %s\n", clusterSet.Name, clusterSet.Namespace) + unstructuredClusterSet, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(clusterSet) *createdRes = append(*createdRes, unstructuredClusterSet) } return nil } -func deleteClusterClaims(cmd *cobra.Command, k8sClient client.Client, namespace string) error { - var e error - clusterClaimNames := []string{multiclusterv1alpha2.WellKnownClusterClaimID, multiclusterv1alpha2.WellKnownClusterClaimClusterSet} +func deleteClusterClaims(cmd *cobra.Command, k8sClient client.Client, namespace string) { + clusterClaimNames := []string{ + multiclusterv1alpha2.WellKnownClusterClaimID, + multiclusterv1alpha2.WellKnownClusterClaimClusterSet, + } for _, name := range clusterClaimNames { - if err := k8sClient.Delete(context.TODO(), newClusterClaim(name, namespace, name == multiclusterv1alpha2.WellKnownClusterClaimClusterSet)); err != nil { - fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ClusterClaim \"%s\": %v\n", name, err) - e = err + if err := k8sClient.Delete(context.TODO(), newClusterClaim(name, namespace, name == multiclusterv1alpha2.WellKnownClusterClaimClusterSet)); err == nil { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" deleted in Namespace %s\n", name, namespace) + } else { + if apierrors.IsNotFound(err) { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" not found in Namespace %s\n", name, namespace) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ClusterClaim \"%s\": %v\n", name, err) + } } - fmt.Fprintf(cmd.OutOrStdout(), "ClusterClaim \"%s\" deleted in Namespace %s\n", name, namespace) } - return e } -func deleteClusterSet(cmd *cobra.Command, k8sClient client.Client, namespace string, clusterSet string) error { +func deleteClusterSet(cmd *cobra.Command, k8sClient client.Client, namespace string, clusterSet string) { var err error - 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 - } - if err == nil { + if err = k8sClient.Delete(context.TODO(), newClusterSet(clusterSet, namespace, "", "", "", "", "")); err == nil { fmt.Fprintf(cmd.OutOrStdout(), "ClusterSet \"%s\" deleted in Namespace %s\n", clusterSet, namespace) + return } - return nil + if apierrors.IsNotFound(err) { + fmt.Fprintf(cmd.OutOrStdout(), "ClusterSet \"%s\" not found in Namespace %s\n", clusterSet, namespace) + return + } + fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ClusterSet \"%s\": %v\n", clusterSet, err) } -func deleteSecrets(cmd *cobra.Command, k8sClient client.Client, namespace string) error { +func deleteSecrets(cmd *cobra.Command, k8sClient client.Client, namespace string) { secretList := &corev1.SecretList{} if err := k8sClient.List(context.TODO(), secretList, client.InNamespace(namespace)); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to list Secrets in Namespace %s: %v\n", namespace, err) + return } for _, s := range secretList.Items { @@ -157,18 +165,17 @@ func deleteSecrets(cmd *cobra.Command, k8sClient client.Client, namespace string if err := k8sClient.Delete(context.TODO(), &secret); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete Secret \"%s\": %v\n", secret.Name, err) - return err + return } fmt.Fprintf(cmd.OutOrStdout(), "Secret \"%s\" deleted in Namespace %s\n", secret.Name, namespace) } - - return nil } -func deleteRoleBindings(cmd *cobra.Command, k8sClient client.Client, namespace string) error { +func deleteRoleBindings(cmd *cobra.Command, k8sClient client.Client, namespace string) { roleBindingList := &rbacv1.RoleBindingList{} if err := k8sClient.List(context.TODO(), roleBindingList, client.InNamespace(namespace)); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to list RoleBindings in Namespace %s: %v\n", namespace, err) + return } for _, r := range roleBindingList.Items { @@ -179,18 +186,17 @@ func deleteRoleBindings(cmd *cobra.Command, k8sClient client.Client, namespace s if err := k8sClient.Delete(context.TODO(), &roleBinding); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete RoleBinding \"%s\": %v\n", roleBinding.Name, err) - return err + return } fmt.Fprintf(cmd.OutOrStdout(), "RoleBinding \"%s\" deleted in Namespace %s\n", roleBinding.Name, namespace) } - - return nil } -func deleteServiceAccounts(cmd *cobra.Command, k8sClient client.Client, namespace string) error { +func deleteServiceAccounts(cmd *cobra.Command, k8sClient client.Client, namespace string) { serviceAccountList := &corev1.ServiceAccountList{} if err := k8sClient.List(context.TODO(), serviceAccountList, client.InNamespace(namespace)); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to list ServiceAccounts in Namespace %s: %v\n", namespace, err) + return } for _, sa := range serviceAccountList.Items { @@ -201,19 +207,15 @@ func deleteServiceAccounts(cmd *cobra.Command, k8sClient client.Client, namespac if err := k8sClient.Delete(context.TODO(), &serviceAccount); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to delete ServiceAccount \"%s\": %v\n", serviceAccount.Name, err) - return err + return } fmt.Fprintf(cmd.OutOrStdout(), "ServiceAccount \"%s\" deleted in Namespace %s\n", serviceAccount.Name, namespace) } - - return nil } 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) - unstructuredSA, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(serviceAccount) - createErr = k8sClient.Create(context.TODO(), serviceAccount) if createErr != nil { if !apierrors.IsAlreadyExists(createErr) { @@ -224,12 +226,11 @@ func CreateMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, createErr = nil } else { fmt.Fprintf(cmd.OutOrStdout(), "ServiceAccount \"%s\" created\n", serviceAccount.Name) + unstructuredSA, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(serviceAccount) *createdRes = append(*createdRes, unstructuredSA) } roleBinding := newRoleBinding(name, name, namespace) - unstructuredRoleBinding, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(roleBinding) - createErr = k8sClient.Create(context.TODO(), roleBinding) if createErr != nil { if !apierrors.IsAlreadyExists(createErr) { @@ -240,13 +241,15 @@ func CreateMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, createErr = nil } else { fmt.Fprintf(cmd.OutOrStdout(), "RoleBinding \"%s\" created\n", roleBinding.Name) + unstructuredRoleBinding, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(roleBinding) *createdRes = append(*createdRes, unstructuredRoleBinding) } - + var secretAlreadyExists bool secret := newSecret(name, name, namespace) createErr = k8sClient.Create(context.TODO(), secret) if createErr != nil { - if !apierrors.IsAlreadyExists(createErr) { + secretAlreadyExists = apierrors.IsAlreadyExists(createErr) + if !secretAlreadyExists { fmt.Fprintf(cmd.OutOrStdout(), "Failed to create Secret \"%s\", start rollback\n", name) return createErr } @@ -255,8 +258,13 @@ func CreateMemberToken(cmd *cobra.Command, k8sClient client.Client, name string, // It will take one or two seconds to wait for the Data.token to be created. if err := waitForSecretReady(k8sClient, name, namespace); err != nil { return err + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Secret \"%s\" created\n", secret.Name) + if !secretAlreadyExists { + unstructuredSecret, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(secret) + *createdRes = append(*createdRes, unstructuredSecret) + } } - fmt.Fprintf(cmd.OutOrStdout(), "Secret \"%s\" created\n", secret.Name) if file == nil { return nil diff --git a/pkg/antctl/raw/multicluster/common/common_test.go b/pkg/antctl/raw/multicluster/common/common_test.go new file mode 100644 index 00000000000..8031e3cc885 --- /dev/null +++ b/pkg/antctl/raw/multicluster/common/common_test.go @@ -0,0 +1,431 @@ +// 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 ( + "bytes" + "context" + "log" + "os" + "testing" + + "github.com/spf13/cobra" + "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" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + mcsv1alpha2 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha2" + multiclusterscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestCreateClusterClaim(t *testing.T) { + tests := []struct { + name string + expectedResLen int + existingClusterClaims []mcsv1alpha2.ClusterClaim + }{ + { + name: "create successfully", + expectedResLen: 2, + }, + { + name: "create one cluster ClusterClaim successfully", + expectedResLen: 1, + existingClusterClaims: []mcsv1alpha2.ClusterClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "clusterset.k8s.io", + }, + }, + }, + }, + { + name: "create one clusterSet ClusterClaim successfully", + expectedResLen: 1, + existingClusterClaims: []mcsv1alpha2.ClusterClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "id.k8s.io", + }, + }, + }, + }, + { + name: "falied to create two ClusterClaims", + expectedResLen: 0, + existingClusterClaims: []mcsv1alpha2.ClusterClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "id.k8s.io", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "clusterset.k8s.io", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{} + createdRes := []map[string]interface{}{} + fakeClient := fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).Build() + if len(tt.existingClusterClaims) > 0 { + var obj []client.Object + for _, n := range tt.existingClusterClaims { + cc := n + obj = append(obj, &cc) + } + fakeClient = fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).WithObjects(obj...).Build() + } + + _ = CreateClusterClaim(cmd, fakeClient, "default", "clusterset", "clusterID", &createdRes) + + assert.Equal(t, len(createdRes), tt.expectedResLen) + }) + } +} + +func TestCreateClusterSet(t *testing.T) { + tests := []struct { + name string + expectedResLen int + existingClusterSet *mcsv1alpha1.ClusterSet + }{ + { + name: "create successfully", + expectedResLen: 1, + }, + { + name: "falied to create ClusterSet", + expectedResLen: 0, + existingClusterSet: &mcsv1alpha1.ClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "clusterset", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{} + createdRes := []map[string]interface{}{} + fakeClient := fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).Build() + if tt.existingClusterSet != nil { + fakeClient = fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).WithObjects(tt.existingClusterSet).Build() + } + + _ = CreateClusterSet(cmd, fakeClient, "default", "clusterset", "http://localhost", "token", + "member-id", "leader-id", "leader-ns", &createdRes) + + assert.Equal(t, len(createdRes), tt.expectedResLen) + }) + } +} + +func TestDeleteClusterClaims(t *testing.T) { + id := mcsv1alpha2.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "id.k8s.io", + }, + } + clusterset := mcsv1alpha2.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "clusterset.k8s.io", + }, + } + tests := []struct { + name string + expectedOutput string + existingClusterClaims *mcsv1alpha2.ClusterClaimList + }{ + { + name: "delete successfully", + expectedOutput: "ClusterClaim \"id.k8s.io\" deleted in Namespace default\nClusterClaim \"clusterset.k8s.io\" deleted in Namespace default\n", + existingClusterClaims: &mcsv1alpha2.ClusterClaimList{ + Items: []mcsv1alpha2.ClusterClaim{ + id, clusterset, + }, + }, + }, + { + name: "delete with not found error", + expectedOutput: "ClusterClaim \"id.k8s.io\" not found in Namespace default\nClusterClaim \"clusterset.k8s.io\" deleted in Namespace default\n", + existingClusterClaims: &mcsv1alpha2.ClusterClaimList{ + Items: []mcsv1alpha2.ClusterClaim{ + clusterset, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{} + fakeClient := fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).Build() + if tt.existingClusterClaims != nil { + fakeClient = fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).WithLists(tt.existingClusterClaims).Build() + } + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + deleteClusterClaims(cmd, fakeClient, "default") + + assert.Equal(t, tt.expectedOutput, buf.String()) + remainClusterClaims := &mcsv1alpha2.ClusterClaimList{} + fakeClient.List(context.Background(), remainClusterClaims, &client.ListOptions{}) + assert.Equal(t, len(remainClusterClaims.Items), 0) + }) + } +} + +func TestDeleteClusterSet(t *testing.T) { + existingClusterSet := &mcsv1alpha1.ClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "clusterset", + }, + } + tests := []struct { + name string + expectedOutput string + existingClusterSet *mcsv1alpha1.ClusterSet + }{ + { + name: "delete successfully", + expectedOutput: "ClusterSet \"clusterset\" deleted in Namespace default\n", + existingClusterSet: existingClusterSet, + }, + { + name: "delete with not found error", + expectedOutput: "ClusterSet \"clusterset\" not found in Namespace default\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{} + fakeClient := fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).Build() + if tt.existingClusterSet != nil { + fakeClient = fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).WithObjects(tt.existingClusterSet).Build() + } + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + deleteClusterSet(cmd, fakeClient, "default", "clusterset") + + assert.Equal(t, tt.expectedOutput, buf.String()) + }) + } +} + +func TestDeleteSecrets(t *testing.T) { + secret1 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "membertoken", + Annotations: map[string]string{ + CreateByAntctlAnnotation: "true", + }, + }, + } + secret2 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "othertoken2", + }, + } + + cmd := &cobra.Command{} + fakeClient := fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).WithObjects(secret1, secret2).Build() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + deleteSecrets(cmd, fakeClient, "default") + + assert.Equal(t, "Secret \"membertoken\" deleted in Namespace default\n", buf.String()) + remainSecrets := &corev1.SecretList{} + fakeClient.List(context.Background(), remainSecrets, &client.ListOptions{}) + assert.Equal(t, 1, len(remainSecrets.Items)) +} + +func TestDeleteRoleBindings(t *testing.T) { + rb1 := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "rb1", + Annotations: map[string]string{ + CreateByAntctlAnnotation: "true", + }, + }, + } + rb2 := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "rb2", + }, + } + cmd := &cobra.Command{} + fakeClient := fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).WithObjects(rb1, rb2).Build() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + deleteRoleBindings(cmd, fakeClient, "default") + + assert.Equal(t, "RoleBinding \"rb1\" deleted in Namespace default\n", buf.String()) + remainRBs := &rbacv1.RoleBindingList{} + fakeClient.List(context.Background(), remainRBs, &client.ListOptions{}) + assert.Equal(t, 1, len(remainRBs.Items)) +} + +func TestDeleteServiceAccounts(t *testing.T) { + sa1 := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "sa1", + Annotations: map[string]string{ + CreateByAntctlAnnotation: "true", + }, + }, + } + sa2 := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "sa2", + }, + } + cmd := &cobra.Command{} + fakeClient := fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).WithObjects(sa1, sa2).Build() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + deleteServiceAccounts(cmd, fakeClient, "default") + + assert.Equal(t, "ServiceAccount \"sa1\" deleted in Namespace default\n", buf.String()) + remainSAs := &corev1.ServiceAccountList{} + fakeClient.List(context.Background(), remainSAs, &client.ListOptions{}) + assert.Equal(t, 1, len(remainSAs.Items)) +} + +func TestCreateMemberToken(t *testing.T) { + tests := []struct { + name string + expectedResLen int + expectedErr string + existingSecret *corev1.Secret + existingSA *corev1.ServiceAccount + existingRB *rbacv1.RoleBinding + }{ + { + name: "create ServiceAccount and RoleBinding successfully", + expectedResLen: 2, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "membertoken", + }, + Data: map[string][]byte{"token": []byte("12345")}, + }, + }, + { + name: "create RoleBinding successfully", + expectedResLen: 1, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "membertoken", + }, + Data: map[string][]byte{"token": []byte("12345")}, + }, + existingSA: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "membertoken", + }, + }, + }, + { + name: "create successfully with all existing resources", + expectedResLen: 0, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "membertoken", + }, + Data: map[string][]byte{"token": []byte("12345")}, + }, + existingSA: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "membertoken", + }, + }, + existingRB: &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "membertoken", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{} + createdRes := []map[string]interface{}{} + var obj []client.Object + if tt.existingSA != nil { + obj = append(obj, tt.existingSA) + } + if tt.existingRB != nil { + obj = append(obj, tt.existingRB) + } + if tt.existingSecret != nil { + obj = append(obj, tt.existingSecret) + } + file, err := os.CreateTemp("", "membertoken") + if err != nil { + log.Fatal(err) + } + defer os.Remove(file.Name()) + fakeClient := fake.NewClientBuilder().WithScheme(multiclusterscheme.Scheme).WithObjects(obj...).Build() + _ = CreateMemberToken(cmd, fakeClient, "membertoken", "default", file, &createdRes) + assert.Equal(t, tt.expectedResLen, len(createdRes)) + }) + } +} diff --git a/pkg/antctl/raw/multicluster/common/mock_client.go b/pkg/antctl/raw/multicluster/common/mock_client.go new file mode 100644 index 00000000000..5f8075e766b --- /dev/null +++ b/pkg/antctl/raw/multicluster/common/mock_client.go @@ -0,0 +1,57 @@ +// 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" + "errors" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type FakeCtrlRuntimeClient struct { + client.Client + ShouldError bool +} + +func (fc FakeCtrlRuntimeClient) Create( + ctx context.Context, + obj client.Object, + opts ...client.CreateOption) error { + if fc.ShouldError { + return errors.New("failed to create object") + } + return fc.Client.Create(ctx, obj) +} + +func (fc FakeCtrlRuntimeClient) Update( + ctx context.Context, + obj client.Object, + opts ...client.UpdateOption) error { + if fc.ShouldError { + return errors.New("failed to update object") + } + return fc.Client.Update(ctx, obj) +} + +func (fc FakeCtrlRuntimeClient) Delete( + ctx context.Context, + obj client.Object, + opts ...client.DeleteOption) error { + if fc.ShouldError { + return errors.New("failed to delete object") + } + return fc.Client.Delete(ctx, obj) +} diff --git a/pkg/antctl/raw/multicluster/create/access_token.go b/pkg/antctl/raw/multicluster/create/access_token.go index 8a909072df3..5a597902dde 100644 --- a/pkg/antctl/raw/multicluster/create/access_token.go +++ b/pkg/antctl/raw/multicluster/create/access_token.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/spf13/cobra" + "sigs.k8s.io/controller-runtime/pkg/client" "antrea.io/antrea/pkg/antctl/raw/multicluster/common" ) @@ -27,6 +28,7 @@ import ( type memberTokenOptions struct { namespace string output string + k8sClient client.Client } var memberTokenOpts *memberTokenOptions @@ -38,10 +40,17 @@ var memberTokenExamples = strings.Trim(` $ antctl mc create membertoken cluster-east-token -n antrea-multicluster -o token-secret.yml `, "\n") -func (o *memberTokenOptions) validateAndComplete() error { +func (o *memberTokenOptions) 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 } @@ -64,40 +73,36 @@ func NewAccessTokenCmd() *cobra.Command { } func memberTokenRunE(cmd *cobra.Command, args []string) error { - if err := memberTokenOpts.validateAndComplete(); err != nil { + 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)) } - k8sClient, err := common.NewClient(cmd) - if err != nil { - return err - } - var createErr error createdRes := []map[string]interface{}{} defer func() { if createErr != nil { - if err := common.Rollback(cmd, k8sClient, createdRes); err != nil { + fmt.Fprintf(cmd.OutOrStderr(), "Failed to create new member token. Deleting the created resources\n") + if err := common.Rollback(cmd, memberTokenOpts.k8sClient, createdRes); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to rollback: %v\n", err) } } }() + var err error var file *os.File if memberTokenOpts.output != "" { if file, err = os.OpenFile(memberTokenOpts.output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil { return err } + defer file.Close() } - defer file.Close() - if createErr = common.CreateMemberToken(cmd, k8sClient, args[0], memberTokenOpts.namespace, file, &createdRes); createErr != nil { + if createErr = common.CreateMemberToken(cmd, memberTokenOpts.k8sClient, args[0], memberTokenOpts.namespace, file, &createdRes); createErr != nil { return createErr } fmt.Fprintf(cmd.OutOrStdout(), "You can now run the \"antctl mc join\" command with the token to have the cluster join the ClusterSet\n") - return nil } diff --git a/pkg/antctl/raw/multicluster/create/access_token_test.go b/pkg/antctl/raw/multicluster/create/access_token_test.go new file mode 100644 index 00000000000..9b6ec80fd83 --- /dev/null +++ b/pkg/antctl/raw/multicluster/create/access_token_test.go @@ -0,0 +1,126 @@ +// 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 ( + "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" + + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestCreateAccessToken(t *testing.T) { + existingSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + }, + Data: map[string][]byte{"token": []byte("12345")}, + } + + secretContent := []byte(`apiVersion: v1 +kind: Secret +metadata: + name: default-member-token +data: + ca.crt: YWJjZAo= + namespace: ZGVmYXVsdAo= + token: YWJjZAo= +type: Opaque`) + + tests := []struct { + name string + namespace string + expectedOutput string + secretFile bool + failureType string + tokeName string + }{ + { + name: "create successfully", + tokeName: "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", + 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, + }, + { + name: "fail to create without name", + namespace: "default", + expectedOutput: "exactly one NAME is required, got 0", + }, + { + name: "fail to create without namespace", + namespace: "", + expectedOutput: "the Namespace is required", + }, + { + name: "fail to create and rollback", + namespace: "default", + failureType: "create", + tokeName: "default-member-token", + expectedOutput: "failed to create object", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := NewAccessTokenCmd() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + + memberTokenOpts.namespace = tt.namespace + memberTokenOpts.k8sClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(existingSecret).Build() + if tt.failureType == "create" { + memberTokenOpts.k8sClient = common.FakeCtrlRuntimeClient{ + Client: fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(existingSecret).Build(), + ShouldError: true, + } + } + if tt.tokeName != "" { + cmd.SetArgs([]string{tt.tokeName}) + } + 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() + } + 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/deploy/deploy_helper.go b/pkg/antctl/raw/multicluster/deploy/deploy_helper.go index d378d162adf..d74b90b9b27 100644 --- a/pkg/antctl/raw/multicluster/deploy/deploy_helper.go +++ b/pkg/antctl/raw/multicluster/deploy/deploy_helper.go @@ -37,6 +37,7 @@ import ( "k8s.io/client-go/restmapper" "antrea.io/antrea/pkg/antctl/raw" + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" ) const ( @@ -74,29 +75,13 @@ func generateManifests(role string, version string) ([]string, error) { } } default: - return manifests, fmt.Errorf("invalid role %s", role) + return nil, fmt.Errorf("invalid role: %s", role) } - return manifests, nil } -func createResources(cmd *cobra.Command, content []byte) error { - kubeconfig, err := raw.ResolveKubeconfig(cmd) - if err != nil { - return err - } - restconfigTmpl := rest.CopyConfig(kubeconfig) - raw.SetupKubeconfig(restconfigTmpl) - - k8sClient, err := kubernetes.NewForConfig(kubeconfig) - if err != nil { - return err - } - dynamicClient, err := dynamic.NewForConfig(kubeconfig) - if err != nil { - return err - } - +func createResources(cmd *cobra.Command, k8sClient kubernetes.Interface, dynamicClient dynamic.Interface, content []byte) error { + var err error decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(content)), 100) for { var rawObj runtime.RawExtension @@ -137,20 +122,37 @@ func createResources(cmd *cobra.Command, content []byte) error { if !kerrors.IsAlreadyExists(err) { return err } + fmt.Fprintf(cmd.OutOrStdout(), "%s/%s already exists\n", unstructuredObj.GetKind(), unstructuredObj.GetName()) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "%s/%s created\n", unstructuredObj.GetKind(), unstructuredObj.GetName()) } - fmt.Fprintf(cmd.OutOrStdout(), "%s/%s created\n", unstructuredObj.GetKind(), unstructuredObj.GetName()) } - return nil } func deploy(cmd *cobra.Command, role string, version string, namespace string, filename string) error { + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + restconfigTmpl := rest.CopyConfig(kubeconfig) + raw.SetupKubeconfig(restconfigTmpl) + + k8sClient, err := kubernetes.NewForConfig(kubeconfig) + if err != nil { + return err + } + dynamicClient, err := dynamic.NewForConfig(kubeconfig) + if err != nil { + return err + } + if filename != "" { content, err := os.ReadFile(filename) if err != nil { return err } - if err := createResources(cmd, content); err != nil { + if err := createResources(cmd, k8sClient, dynamicClient, content); err != nil { return err } } else { @@ -170,19 +172,17 @@ func deploy(cmd *cobra.Command, role string, version string, namespace string, f } content := string(b) - if role == leaderRole && strings.Contains(manifest, "namespaced") { - content = strings.ReplaceAll(content, "antrea-multicluster", namespace) + if role == leaderRole && strings.Contains(manifest, "namespaced") && namespace != common.DefaultLeaderNamespace { + content = strings.ReplaceAll(content, common.DefaultLeaderNamespace, namespace) } - if role == memberRole && strings.Contains(manifest, "member") { - content = strings.ReplaceAll(content, "kube-system", namespace) + if role == memberRole && strings.Contains(manifest, "member") && namespace != common.DefaultMemberNamespace { + content = strings.ReplaceAll(content, common.DefaultMemberNamespace, namespace) } - - if err := createResources(cmd, []byte(content)); err != nil { + if err := createResources(cmd, k8sClient, dynamicClient, []byte(content)); err != nil { return err } } } fmt.Fprintf(cmd.OutOrStdout(), "The %s cluster resources are deployed\n", role) - return nil } diff --git a/pkg/antctl/raw/multicluster/deploy/deploy_helper_test.go b/pkg/antctl/raw/multicluster/deploy/deploy_helper_test.go new file mode 100644 index 00000000000..226c0466459 --- /dev/null +++ b/pkg/antctl/raw/multicluster/deploy/deploy_helper_test.go @@ -0,0 +1,140 @@ +// Copyright 2022 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package deploy + +import ( + "log" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + dynamicfake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes/fake" + + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestGenerateManifests(t *testing.T) { + tests := []struct { + name string + role string + version string + expectedManifests []string + expectedErr string + }{ + { + name: "generate latest leader manifests", + role: "leader", + version: "latest", + expectedManifests: []string{ + "https://raw.githubusercontent.com/antrea-io/antrea/main/multicluster/build/yamls/antrea-multicluster-leader-global.yml", + "https://raw.githubusercontent.com/antrea-io/antrea/main/multicluster/build/yamls/antrea-multicluster-leader-namespaced.yml", + }, + }, + { + name: "generate latest member manifests", + role: "member", + version: "latest", + expectedManifests: []string{ + "https://raw.githubusercontent.com/antrea-io/antrea/main/multicluster/build/yamls/antrea-multicluster-member.yml", + }, + }, + { + name: "generate versioned leader manifests", + role: "leader", + version: "v1.7.0", + expectedManifests: []string{ + "https://github.com/antrea-io/antrea/releases/download/v1.7.0/antrea-multicluster-leader-global.yml", + "https://github.com/antrea-io/antrea/releases/download/v1.7.0/antrea-multicluster-leader-namespaced.yml", + }, + }, + { + name: "generate versioned member manifests", + role: "member", + version: "v1.7.0", + expectedManifests: []string{ + "https://github.com/antrea-io/antrea/releases/download/v1.7.0/antrea-multicluster-member.yml", + }, + }, + { + name: "invalid role", + role: "member1", + version: "latest", + expectedErr: "invalid role: member1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actualManifests, err := generateManifests(tt.role, tt.version) + if err != nil { + assert.Equal(t, tt.expectedErr, err.Error()) + } else if !reflect.DeepEqual(actualManifests, tt.expectedManifests) { + t.Errorf("Expected %v but got %v", tt.expectedManifests, actualManifests) + } + }) + } +} + +func TestCreateResources(t *testing.T) { + fakeDynamicClient := dynamicfake.NewSimpleDynamicClient(mcscheme.Scheme) + fakeClient := fake.NewSimpleClientset() + cmd := &cobra.Command{} + file := filepath.Join("..", "..", "..", "..", "..", "multicluster", "build", "yamls", "antrea-multicluster-leader-global.yml") + content, err := os.ReadFile(file) + if err != nil { + t.Errorf("Failed to open the file %s", file) + } + err = createResources(cmd, fakeClient, fakeDynamicClient, content) + if err != nil { + assert.Contains(t, err.Error(), "no matches for kind \"CustomResourceDefinition\"") + } +} + +func TestDeploy(t *testing.T) { + fakeConfigs := []byte(`apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lJTHJac3Z6ZFQ3ekF3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TWpBNE1qSXdNakl6TXpkYUZ3MHlNekE0TWpJd01qSXpNemxhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTB4N2JEd2NqSzN3VjRGSzkKYUtrd0FUdjVoT2NsbHhUSEI1ejFUbHZJV3pmdTNYNjZtaWkxUE04ODI1dTArdDRRdisxUVRIRHFzUkNvWFA1awpuNGNWZkxkeTlad25uN01uSDExVTRsRWRoeXBrdlZsc0RmajlBdWh3WHBZVE82eE5kM2o2Y3BIZGNMOW9PbGw2CkowcGU2RzBleHpTSHMvbHRUZXlyalRGbXM2Sm5zSWV6T2lHRmhZOTJCbDBmZ1krb2p6MFEwM2cvcE5QZUszcGMKK05wTWh4eG1UY1lVNzlaZVRqV1JPYTFQSituNk1SMEhDbW0xQk5QNmdwWmozbGtWSktkZnBEYmovWHYvQWNkVQpab3E5Ym95aGNDUCtiYmgyaWVtaTc0bnZqZ1BUTkVDZWU2a3ZHY3VNaXRKUkdvWjBxbFpZbXZDaWdEeGlSTnBNClBPa1dud0lEQVFBQm8xWXdWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JSc2VoZXVkM0l5VWRNdkhhRS9YU3MrOFErLwpiVEFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBcmg4UFRadFgvWjlHVzlMYmxZZ1FWWE04VlRrWEtGSEpTZldOCkJLNXo2NWNWdGN2cFZ0WDZNTlppTFhuYkFzQ0JPY1RqejBJRlphYkNNUkZzYmdYbEVqV0ZuRE5abzBMVHFTZUcKQ2RqTWljK0JzbmFGUThZOXJ5TTVxZ0RhQzNWQkdTSXVscklXeGxPYmRmUEpWRnpUaVNTcmJBR1Z3Uk5sQlpmYgpYOXBlRlpNNmNFNUhTOE5RTmNoZkh2SWhGSUVuR2YxOUx2enp0WGUzQWwwb3hYNjdRKzhyWXd0Tm56dS9xM29BCmJIN1dsNld5ODVYNS90RWlQcWU0ZU1GalRDME9tR2NHZ2lQdU90NjlIejAwV2hvaWNYYWpma1FZOHNKMk5Uc1cKdUcxbWZqb0tTdUN0OC9BRmhPNURlaHZ3eFNIQU12eG1VQUJYL294bU1DNzdwV0VnRWc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://localhost + name: fake-cluster +contexts: +- context: + cluster: fake-cluster + user: user-id + name: fake-cluster +current-context: fake-cluster +kind: Config`) + + var err error + fakeKubeconfig, err := os.CreateTemp("", "fakeKubeconfig") + if err != nil { + log.Fatal(err) + } + defer os.Remove(fakeKubeconfig.Name()) + fakeKubeconfig.Write(fakeConfigs) + kubeconfig := "" + cmd := &cobra.Command{} + cmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", fakeKubeconfig.Name(), "path of kubeconfig") + err = deploy(cmd, "leader", "latest", "kube-system", "") + if err != nil { + assert.Contains(t, err.Error(), "Get \"https://localhost/api\": dial tcp [::1]:443: connect: connection refused") + } else { + t.Error("Expected to get error but nil") + } +} diff --git a/pkg/antctl/raw/multicluster/deploy/leader_cluster.go b/pkg/antctl/raw/multicluster/deploy/leader_cluster.go index 9a47ced9cf4..c0464a2e679 100644 --- a/pkg/antctl/raw/multicluster/deploy/leader_cluster.go +++ b/pkg/antctl/raw/multicluster/deploy/leader_cluster.go @@ -46,7 +46,6 @@ func (o *leaderClusterOptions) validateAndComplete() error { if _, err := os.Stat(o.filename); err != nil { return err } - return nil } if o.namespace == "" { return fmt.Errorf("the Namespace cannot be empty") @@ -54,7 +53,6 @@ func (o *leaderClusterOptions) validateAndComplete() error { if o.antreaVersion == "" { o.antreaVersion = "latest" } - return nil } diff --git a/pkg/antctl/raw/multicluster/deploy/member_cluster.go b/pkg/antctl/raw/multicluster/deploy/member_cluster.go index aeb8a440ef7..ee54dd92b56 100644 --- a/pkg/antctl/raw/multicluster/deploy/member_cluster.go +++ b/pkg/antctl/raw/multicluster/deploy/member_cluster.go @@ -45,7 +45,6 @@ func (o *memberClusterOptions) validateAndComplete() error { if _, err := os.Stat(o.filename); err != nil { return err } - return nil } if o.namespace == "" { return fmt.Errorf("the Namespace cannot be empty") diff --git a/pkg/antctl/raw/multicluster/destory_test.go b/pkg/antctl/raw/multicluster/destory_test.go new file mode 100644 index 00000000000..4185d287b9b --- /dev/null +++ b/pkg/antctl/raw/multicluster/destory_test.go @@ -0,0 +1,85 @@ +// 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" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + mcsv1alpha2 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha2" + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestDestroy(t *testing.T) { + tests := []struct { + name string + expectedOutput string + namespace string + }{ + { + name: "destroy successfully", + expectedOutput: "ClusterSet \"test-clusterset\" deleted in Namespace default\nClusterClaim \"id.k8s.io\" deleted in Namespace default\nClusterClaim \"clusterset.k8s.io\" deleted in Namespace default\n", + namespace: "default", + }, + { + name: "fail to destroy due to empty Namespace", + expectedOutput: "the Namespace is required", + namespace: "", + }, + } + + cmd := NewDestroyCommand() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.Flag("clusterset").Value.Set("test-clusterset") + clusterSet := &mcsv1alpha1.ClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-clusterset", + }, + } + clusterClaim1 := &mcsv1alpha2.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "id.k8s.io", + }, + } + clusterClaim2 := &mcsv1alpha2.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "clusterset.k8s.io", + }, + } + fakeClient := fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(clusterSet, clusterClaim1, clusterClaim2).Build() + destroyOpts.K8sClient = fakeClient + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd.Flag("namespace").Value.Set(tt.namespace) + 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/raw/multicluster/get/clusterset_test.go b/pkg/antctl/raw/multicluster/get/clusterset_test.go index 27597740e95..16ceb8db62d 100644 --- a/pkg/antctl/raw/multicluster/get/clusterset_test.go +++ b/pkg/antctl/raw/multicluster/get/clusterset_test.go @@ -46,7 +46,6 @@ func TestGetClusterSet(t *testing.T) { output string allNamespaces bool donotFake bool - expectedErr string expectedOutput string }{ { @@ -70,9 +69,9 @@ func TestGetClusterSet(t *testing.T) { expectedOutput: "- apiVersion: multicluster.crd.antrea.io/v1alpha1\n kind: ClusterSet\n metadata:\n creationTimestamp: null\n name: clusterset-name\n namespace: default\n resourceVersion: \"999\"\n spec:\n leaders: null\n status: {}\n", }, { - name: "get non-existing ClusterSet", - args: []string{"clusterset1"}, - expectedErr: "clustersets.multicluster.crd.antrea.io \"clusterset1\" not found", + name: "get non-existing ClusterSet", + args: []string{"clusterset1"}, + expectedOutput: "clustersets.multicluster.crd.antrea.io \"clusterset1\" not found", }, { name: "get all ClusterSets", @@ -120,9 +119,9 @@ func TestGetClusterSet(t *testing.T) { expectedOutput: "No resource found in Namespace default\n", }, { - name: "error due to no kubeconfig", - expectedErr: "flag accessed but not defined: kubeconfig", - donotFake: true, + name: "error due to no kubeconfig", + expectedOutput: "flag accessed but not defined: kubeconfig", + donotFake: true, }, } @@ -151,7 +150,7 @@ func TestGetClusterSet(t *testing.T) { } err := cmd.Execute() if err != nil { - assert.Equal(t, tt.expectedErr, err.Error()) + assert.Equal(t, tt.expectedOutput, err.Error()) } else { assert.Equal(t, tt.expectedOutput, buf.String()) } diff --git a/pkg/antctl/raw/multicluster/get/resourceexport.go b/pkg/antctl/raw/multicluster/get/resourceexport.go index a9079fa44a7..da39e25635c 100644 --- a/pkg/antctl/raw/multicluster/get/resourceexport.go +++ b/pkg/antctl/raw/multicluster/get/resourceexport.go @@ -37,6 +37,7 @@ type resourceExportOptions struct { outputFormat string allNamespaces bool clusterID string + k8sClient client.Client } var optionsResourceExport *resourceExportOptions @@ -54,15 +55,24 @@ Get the specified ResourceExport $ antctl mc get resourceexport -n `, "\n") -func (o *resourceExportOptions) validateAndComplete() { +func (o *resourceExportOptions) validateAndComplete(cmd *cobra.Command) error { if o.allNamespaces { o.namespace = metav1.NamespaceAll - return - } - if o.namespace == "" { + } else if o.namespace == "" { o.namespace = metav1.NamespaceDefault - return } + if o.k8sClient == nil { + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + + o.k8sClient, err = client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme}) + if err != nil { + return err + } + } + return nil } func NewResourceExportCommand() *cobra.Command { @@ -88,42 +98,39 @@ func NewResourceExportCommand() *cobra.Command { } func runEResourceExport(cmd *cobra.Command, args []string) error { - optionsResourceExport.validateAndComplete() - - kubeconfig, err := raw.ResolveKubeconfig(cmd) + err := optionsResourceExport.validateAndComplete(cmd) if err != nil { return err } - argsNum := len(args) + var resExports interface{} singleResource := false - if argsNum > 0 { + if len(args) > 0 { singleResource = true } - var resExports []multiclusterv1alpha1.ResourceExport - k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme}) - if err != nil { - return err + + if optionsResourceExport.allNamespaces && singleResource { + return fmt.Errorf("a resource cannot be retrieved by name across all Namespaces") } if singleResource { resourceExportName := args[0] resourceExport := multiclusterv1alpha1.ResourceExport{} - err = k8sClient.Get(context.TODO(), types.NamespacedName{ + err = optionsResourceExport.k8sClient.Get(context.TODO(), types.NamespacedName{ Namespace: optionsResourceExport.namespace, Name: resourceExportName, }, &resourceExport) if err != nil { return err } - gvks, unversioned, err := k8sClient.Scheme().ObjectKinds(&resourceExport) + gvks, unversioned, err := optionsResourceExport.k8sClient.Scheme().ObjectKinds(&resourceExport) if err != nil { return err } if !unversioned && len(gvks) == 1 { resourceExport.SetGroupVersionKind(gvks[0]) } - resExports = append(resExports, resourceExport) + resExports = resourceExport } else { var labels map[string]string if optionsResourceExport.clusterID != "" { @@ -132,25 +139,23 @@ func runEResourceExport(cmd *cobra.Command, args []string) error { selector := metav1.LabelSelector{MatchLabels: labels} labelSelector, _ := metav1.LabelSelectorAsSelector(&selector) resourceExportList := &multiclusterv1alpha1.ResourceExportList{} - err = k8sClient.List(context.TODO(), resourceExportList, &client.ListOptions{ + err = optionsResourceExport.k8sClient.List(context.TODO(), resourceExportList, &client.ListOptions{ Namespace: optionsResourceExport.namespace, LabelSelector: labelSelector, }) if err != nil { return err } - resExports = resourceExportList.Items - } - - if len(resExports) == 0 { - if optionsResourceExport.namespace != "" { - fmt.Fprintf(cmd.ErrOrStderr(), "No resources found in Namespace %s\n", optionsResourceExport.namespace) - } else { - fmt.Fprintln(cmd.ErrOrStderr(), "No resources found") + if len(resourceExportList.Items) == 0 { + if optionsResourceExport.namespace != "" { + fmt.Fprintf(cmd.ErrOrStderr(), "No resources found in Namespace %s\n", optionsResourceExport.namespace) + } else { + fmt.Fprintln(cmd.ErrOrStderr(), "No resources found") + } + return nil } - return nil + resExports = resourceExportList.Items } - return output(resExports, false, optionsResourceExport.outputFormat, cmd.OutOrStdout(), resourceexport.Transform) - + return output(resExports, singleResource, optionsResourceExport.outputFormat, cmd.OutOrStdout(), resourceexport.Transform) } diff --git a/pkg/antctl/raw/multicluster/get/resourceexport_test.go b/pkg/antctl/raw/multicluster/get/resourceexport_test.go new file mode 100644 index 00000000000..4f292672ead --- /dev/null +++ b/pkg/antctl/raw/multicluster/get/resourceexport_test.go @@ -0,0 +1,155 @@ +// 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" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestGetResourceExport(t *testing.T) { + resourceExportList := &mcsv1alpha1.ResourceExportList{ + Items: []mcsv1alpha1.ResourceExport{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "re-cluster-id-1", + }, + }, + }, + } + tests := []struct { + name string + existingResourceExports *mcsv1alpha1.ResourceExportList + args []string + flags map[string]string + allNamespaces bool + donotFake bool + expectedOutput string + }{ + { + name: "get single ResourceExport", + existingResourceExports: resourceExportList, + args: []string{"re-cluster-id-1"}, + expectedOutput: "CLUSTER-ID NAMESPACE NAME KIND \n default re-cluster-id-1 \n", + }, + { + name: "get single ResourceExport with json output", + existingResourceExports: resourceExportList, + args: []string{"re-cluster-id-1"}, + flags: map[string]string{"output": "json"}, + expectedOutput: "{\n \"kind\": \"ResourceExport\",\n \"apiVersion\": \"multicluster.crd.antrea.io/v1alpha1\",\n \"metadata\": {\n \"name\": \"re-cluster-id-1\",\n \"namespace\": \"default\",\n \"resourceVersion\": \"999\",\n \"creationTimestamp\": null\n },\n \"spec\": {},\n \"status\": {}\n}\n", + }, + { + name: "get single ResourceExport with yaml output", + existingResourceExports: resourceExportList, + args: []string{"re-cluster-id-1"}, + flags: map[string]string{"output": "yaml"}, + expectedOutput: "apiVersion: multicluster.crd.antrea.io/v1alpha1\nkind: ResourceExport\nmetadata:\n creationTimestamp: null\n name: re-cluster-id-1\n namespace: default\n resourceVersion: \"999\"\nspec: {}\nstatus: {}\n", + }, + { + name: "get non-existing ResourceExport", + args: []string{"re-cluster-id-2"}, + expectedOutput: "resourceexports.multicluster.crd.antrea.io \"re-cluster-id-2\" not found", + }, + { + name: "get all ResourceExports with given cluster ID", + allNamespaces: true, + flags: map[string]string{"clusterID": "cluster-id-1"}, + existingResourceExports: &mcsv1alpha1.ResourceExportList{ + Items: []mcsv1alpha1.ResourceExport{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "re-cluster-id-1", + Labels: map[string]string{ + "sourceClusterID": "cluster-id-1", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "kube-system", + Name: "re-cluster-id-2", + }, + }, + }, + }, + expectedOutput: "CLUSTER-ID NAMESPACE NAME KIND \ncluster-id-1 default re-cluster-id-1 \n", + }, + { + name: "get all ResourceExports but empty result", + allNamespaces: true, + expectedOutput: "No resources found\n", + }, + { + name: "error to get a ResourceExport in all Namespaces", + args: []string{"re-cluster-id-1"}, + allNamespaces: true, + expectedOutput: "a resource cannot be retrieved by name across all Namespaces", + }, + { + name: "get all ResourceExports in default Namespace but empty result", + expectedOutput: "No resources found in Namespace default\n", + }, + { + name: "error due to no kubeconfig", + expectedOutput: "flag accessed but not defined: kubeconfig", + donotFake: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := NewResourceExportCommand() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs(tt.args) + + fakeClient := fake.NewClientBuilder().WithScheme(mcscheme.Scheme).Build() + if tt.existingResourceExports != nil { + fakeClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithLists(tt.existingResourceExports).Build() + + } + if !tt.donotFake { + optionsResourceExport.k8sClient = fakeClient + } + if tt.allNamespaces { + optionsResourceExport.allNamespaces = true + } + if v, ok := tt.flags["output"]; ok { + optionsResourceExport.outputFormat = v + } + if v, ok := tt.flags["clusterID"]; ok { + optionsResourceExport.clusterID = v + } + 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/raw/multicluster/get/resourceimport.go b/pkg/antctl/raw/multicluster/get/resourceimport.go index 7b69127d47f..3606995873c 100644 --- a/pkg/antctl/raw/multicluster/get/resourceimport.go +++ b/pkg/antctl/raw/multicluster/get/resourceimport.go @@ -20,11 +20,8 @@ import ( "strings" "github.com/spf13/cobra" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" "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" @@ -37,6 +34,7 @@ type resourceImportOptions struct { namespace string outputFormat string allNamespaces bool + k8sClient client.Client } var options *resourceImportOptions @@ -54,15 +52,24 @@ Get the specified ResourceImport $ antctl mc get resourceimport -n `, "\n") -func (o *resourceImportOptions) validateAndComplete() { +func (o *resourceImportOptions) validateAndComplete(cmd *cobra.Command) error { if o.allNamespaces { o.namespace = metav1.NamespaceAll - return - } - if o.namespace == "" { + } else if o.namespace == "" { o.namespace = metav1.NamespaceDefault - return } + if o.k8sClient == nil { + kubeconfig, err := raw.ResolveKubeconfig(cmd) + if err != nil { + return err + } + + o.k8sClient, err = client.New(kubeconfig, client.Options{Scheme: scheme.Scheme}) + if err != nil { + return err + } + } + return nil } func NewResourceImportCommand() *cobra.Command { @@ -87,47 +94,34 @@ func NewResourceImportCommand() *cobra.Command { } func runE(cmd *cobra.Command, args []string) error { - options.validateAndComplete() - argsNum := len(args) - if options.allNamespaces && argsNum > 0 { - return fmt.Errorf("a resource cannot be retrieved by name across all Namespaces") - } - - kubeconfig, err := raw.ResolveKubeconfig(cmd) + err := options.validateAndComplete(cmd) if err != nil { return err } - kubeconfig.GroupVersion = &schema.GroupVersion{Group: "", Version: ""} - restconfigTmpl := rest.CopyConfig(kubeconfig) - raw.SetupKubeconfig(restconfigTmpl) - - k8sClient, err := client.New(kubeconfig, client.Options{Scheme: scheme.Scheme}) - if err != nil { - return err - } - + argsNum := len(args) singleResource := false if argsNum > 0 { singleResource = true } + + if options.allNamespaces && singleResource { + return fmt.Errorf("a resource cannot be retrieved by name across all Namespaces") + } + var res interface{} if singleResource { resourceImportName := args[0] resourceImport := multiclusterv1alpha1.ResourceImport{} - err = k8sClient.Get(context.TODO(), types.NamespacedName{ + err = options.k8sClient.Get(context.TODO(), types.NamespacedName{ Namespace: options.namespace, Name: resourceImportName, }, &resourceImport) if err != nil { - if apierrors.IsNotFound(err) { - return fmt.Errorf("ResourceImport %s not found in Namespace %s", resourceImportName, options.namespace) - } - return err } - gvks, unversioned, err := k8sClient.Scheme().ObjectKinds(&resourceImport) + gvks, unversioned, err := options.k8sClient.Scheme().ObjectKinds(&resourceImport) if err != nil { return err } @@ -137,16 +131,15 @@ func runE(cmd *cobra.Command, args []string) error { res = resourceImport } else { resourceImportList := &multiclusterv1alpha1.ResourceImportList{} - err = k8sClient.List(context.TODO(), resourceImportList, &client.ListOptions{Namespace: options.namespace}) + err = options.k8sClient.List(context.TODO(), resourceImportList, &client.ListOptions{Namespace: options.namespace}) if err != nil { return err } - if len(resourceImportList.Items) == 0 { if options.namespace != "" { fmt.Fprintf(cmd.ErrOrStderr(), "No resources found in Namespace %s\n", options.namespace) } else { - fmt.Fprintln(cmd.ErrOrStderr(), "No resources found in all Namespaces") + fmt.Fprintln(cmd.ErrOrStderr(), "No resources found") } return nil } diff --git a/pkg/antctl/raw/multicluster/get/resourceimport_test.go b/pkg/antctl/raw/multicluster/get/resourceimport_test.go new file mode 100644 index 00000000000..275ad13d0b6 --- /dev/null +++ b/pkg/antctl/raw/multicluster/get/resourceimport_test.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 ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestGetResourceImport(t *testing.T) { + resourceImportList := &mcsv1alpha1.ResourceImportList{ + Items: []mcsv1alpha1.ResourceImport{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "ri-cluster-id-1", + }, + Spec: mcsv1alpha1.ResourceImportSpec{ + Kind: "ServiceImport", + }, + }, + }, + } + tests := []struct { + name string + existingResourceImports *mcsv1alpha1.ResourceImportList + args []string + output string + allNamespaces bool + donotFake bool + expectedOutput string + }{ + { + name: "get single ResourceImport", + existingResourceImports: resourceImportList, + args: []string{"ri-cluster-id-1"}, + expectedOutput: "NAMESPACE NAME KIND \ndefault ri-cluster-id-1 ServiceImport\n", + }, + { + name: "get single ResourceImport with json output", + existingResourceImports: resourceImportList, + args: []string{"ri-cluster-id-1"}, + output: "json", + expectedOutput: "{\n \"kind\": \"ResourceImport\",\n \"apiVersion\": \"multicluster.crd.antrea.io/v1alpha1\",\n \"metadata\": {\n \"name\": \"ri-cluster-id-1\",\n \"namespace\": \"default\",\n \"resourceVersion\": \"999\",\n \"creationTimestamp\": null\n },\n \"spec\": {\n \"kind\": \"ServiceImport\"\n },\n \"status\": {}\n}\n", + }, + { + name: "get single ResourceImport with yaml output", + existingResourceImports: resourceImportList, + args: []string{"ri-cluster-id-1"}, + output: "yaml", + expectedOutput: "apiVersion: multicluster.crd.antrea.io/v1alpha1\nkind: ResourceImport\nmetadata:\n creationTimestamp: null\n name: ri-cluster-id-1\n namespace: default\n resourceVersion: \"999\"\nspec:\n kind: ServiceImport\nstatus: {}\n", + }, + { + name: "get non-existing ResourceImport", + args: []string{"ri-cluster-id-2"}, + expectedOutput: "resourceimports.multicluster.crd.antrea.io \"ri-cluster-id-2\" not found", + }, + { + name: "get all ResourceImports", + allNamespaces: true, + existingResourceImports: &mcsv1alpha1.ResourceImportList{ + Items: []mcsv1alpha1.ResourceImport{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "ri-cluster-id-1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "kube-system", + Name: "ri-cluster-id-2", + }, + }, + }, + }, + expectedOutput: "NAMESPACE NAME KIND \ndefault ri-cluster-id-1 \nkube-system ri-cluster-id-2 \n", + }, + { + name: "get all ResourceImports but empty result", + allNamespaces: true, + expectedOutput: "No resources found\n", + }, + { + name: "error to get a ResourceImport in all Namespaces", + args: []string{"ri-cluster-id-1"}, + allNamespaces: true, + expectedOutput: "a resource cannot be retrieved by name across all Namespaces", + }, + { + name: "get all ResourceImports in default Namespace but empty result", + expectedOutput: "No resources found in Namespace default\n", + }, + { + name: "error due to no kubeconfig", + expectedOutput: "flag accessed but not defined: kubeconfig", + donotFake: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := NewResourceImportCommand() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs(tt.args) + + fakeClient := fake.NewClientBuilder().WithScheme(mcscheme.Scheme).Build() + if tt.existingResourceImports != nil { + fakeClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithLists(tt.existingResourceImports).Build() + + } + if !tt.donotFake { + options.k8sClient = fakeClient + } + if tt.allNamespaces { + options.allNamespaces = true + } + options.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/raw/multicluster/init.go b/pkg/antctl/raw/multicluster/init.go index d52e4113942..c4836ced1fd 100644 --- a/pkg/antctl/raw/multicluster/init.go +++ b/pkg/antctl/raw/multicluster/init.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cobra" "gopkg.in/yaml.v3" + "sigs.k8s.io/controller-runtime/pkg/client" "antrea.io/antrea/pkg/antctl/raw" "antrea.io/antrea/pkg/antctl/raw/multicluster/common" @@ -37,7 +38,7 @@ const ( # Use the pre-created token Secret. #tokenSecretName: "" # Create a token Secret with the manifest file. -#toeknSecretFile: "" +#tokenSecretFile: "" ` ) @@ -47,19 +48,27 @@ type initOptions struct { clusterID string createToken bool output string + k8sClient client.Client } var initOpts *initOptions -func (o *initOptions) validate() error { +func (o *initOptions) validate(cmd *cobra.Command) error { if o.namespace == "" { - return fmt.Errorf("Namespace is required") + return fmt.Errorf("the Namespace is required") } if o.clusterSet == "" { - return fmt.Errorf("ClusterSet is required") + return fmt.Errorf("the ClusterSet is required") } if o.clusterID == "" { - return fmt.Errorf("ClusterID is required") + return fmt.Errorf("the ClusterID is required") + } + var err error + if o.k8sClient == nil { + o.k8sClient, err = common.NewClient(cmd) + if err != nil { + return err + } } return nil } @@ -95,11 +104,7 @@ func NewInitCommand() *cobra.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 { + if err := initOpts.validate(cmd); err != nil { return err } createdRes := []map[string]interface{}{} @@ -107,33 +112,34 @@ func initRunE(cmd *cobra.Command, args []string) error { defer func() { if createErr != nil { fmt.Fprintf(cmd.OutOrStderr(), "Failed to init the Antrea Multi-cluster. Deleting the created resources\n") - if err := common.Rollback(cmd, k8sClient, createdRes); err != nil { + if err := common.Rollback(cmd, initOpts.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) + createErr = common.CreateClusterClaim(cmd, initOpts.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) + createErr = common.CreateClusterSet(cmd, initOpts.k8sClient, initOpts.namespace, initOpts.clusterSet, "", "", "", initOpts.clusterID, initOpts.namespace, &createdRes) if createErr != nil { return createErr } + var err error var file *os.File if initOpts.output != "" { if file, err = os.OpenFile(initOpts.output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_APPEND, 0644); err != nil { fmt.Fprintf(cmd.OutOrStderr(), "Failed to open file %s: %v\n", initOpts.output, err) } + defer file.Close() } - defer file.Close() if err := outputConfig(cmd, file); err != nil { return err } if initOpts.createToken { - if createErr = common.CreateMemberToken(cmd, k8sClient, defaultToken, initOpts.namespace, file, &createdRes); createErr != nil { + if createErr = common.CreateMemberToken(cmd, initOpts.k8sClient, defaultToken, initOpts.namespace, file, &createdRes); createErr != nil { fmt.Fprintf(cmd.OutOrStderr(), "Failed to create Secret: %v\n", createErr) return createErr } diff --git a/pkg/antctl/raw/multicluster/init_test.go b/pkg/antctl/raw/multicluster/init_test.go new file mode 100644 index 00000000000..00c657f774c --- /dev/null +++ b/pkg/antctl/raw/multicluster/init_test.go @@ -0,0 +1,176 @@ +// 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" + "log" + "os" + "testing" + + "github.com/spf13/cobra" + "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" + + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestInit(t *testing.T) { + existingSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "default-member-token", + }, + Data: map[string][]byte{"token": []byte("12345")}, + } + + cmd := NewInitCommand() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.Flag("clusterset").Value.Set("test-clusterset") + + initOpts.namespace = "default" + initOpts.clusterSet = "test-clusterset" + initOpts.clusterID = "cluster-id" + initOpts.createToken = true + + fakeConfigs := []byte(`apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: data + server: https://localhost + name: fake-cluster +contexts: +- context: + cluster: fake-cluster + user: user-id + name: fake-cluster +current-context: fake-cluster +kind: Config`) + + var err error + fakeKubeconfig, err := os.CreateTemp("", "fakeKubeconfig") + if err != nil { + log.Fatal(err) + } + defer os.Remove(fakeKubeconfig.Name()) + fakeKubeconfig.Write(fakeConfigs) + kubeconfig := "" + cmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", fakeKubeconfig.Name(), "path of kubeconfig") + + tests := []struct { + name string + namespace string + expectedOutput string + failureType string + outputToFile bool + }{ + { + name: "init successfully", + namespace: "default", + expectedOutput: "ClusterClaim \"id.k8s.io\" created in Namespace default\nClusterClaim \"clusterset.k8s.io\" created in Namespace default\nClusterSet \"test-clusterset\" created in Namespace default\nServiceAccount \"default-member-token\" created\nRoleBinding \"default-member-token\" created\nSecret \"default-member-token\" already exists\nSecret \"default-member-token\" created\nSuccessfully initialized ClusterSet test-clusterset\n", + }, + { + name: "init fail due to empty Namespace", + namespace: "", + expectedOutput: "the Namespace is required", + }, + { + name: "fail to create and rollback", + namespace: "default", + failureType: "create", + expectedOutput: "failed to create object", + }, + { + name: "init successfully with output", + namespace: "default", + expectedOutput: "Member token saved to", + outputToFile: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + initOpts.k8sClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(existingSecret).Build() + if tt.failureType == "create" { + initOpts.k8sClient = common.FakeCtrlRuntimeClient{ + Client: fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(existingSecret).Build(), + ShouldError: true, + } + } + cmd.Flag("namespace").Value.Set(tt.namespace) + if tt.outputToFile { + output, err := os.CreateTemp("", "output") + if err != nil { + log.Fatal(err) + } + defer os.Remove(output.Name()) + initOpts.output = output.Name() + } + err := cmd.Execute() + if err != nil { + assert.Contains(t, err.Error(), tt.expectedOutput) + } else { + assert.Contains(t, buf.String(), tt.expectedOutput) + } + }) + } +} + +func TestInitOptValidate(t *testing.T) { + tests := []struct { + name string + expectedOutput string + opts *initOptions + }{ + { + name: "empty Namespace", + expectedOutput: "the Namespace is required", + opts: &initOptions{clusterID: "cluster-a"}, + }, + { + name: "empty ClusterSet", + expectedOutput: "the ClusterSet is required", + opts: &initOptions{ + clusterID: "cluster-a", + namespace: "default", + }, + }, + { + name: "empty ClusterID", + expectedOutput: "the ClusterID is required", + opts: &initOptions{ + clusterSet: "clusterset-a", + namespace: "default", + }, + }, + } + + cmd := &cobra.Command{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.opts.validate(cmd) + if err != nil { + assert.Equal(t, tt.expectedOutput, err.Error()) + } else { + t.Error("Expected to get error but got nil") + } + }) + } +} diff --git a/pkg/antctl/raw/multicluster/join.go b/pkg/antctl/raw/multicluster/join.go index f0aa34e4844..68087a0ae7b 100644 --- a/pkg/antctl/raw/multicluster/join.go +++ b/pkg/antctl/raw/multicluster/join.go @@ -37,10 +37,6 @@ import ( "antrea.io/antrea/pkg/antctl/raw/multicluster/common" ) -const ( - defaultMemberNamespace = "kube-system" -) - // "omitempty" fields (clusterID, namespace, tokenSecretName, tokenSecretFile) // can be populated by the corresponding command line options if not set in the // config file. @@ -58,11 +54,12 @@ type ClusterSetJoinConfig struct { // The following fields are not included in the config file. ConfigFile string `yaml:"-"` Secret *v1.Secret `yaml:"-"` + k8sClient client.Client } var joinOpts *ClusterSetJoinConfig -func (o *ClusterSetJoinConfig) validateAndComplete() error { +func (o *ClusterSetJoinConfig) validateAndComplete(cmd *cobra.Command) error { if o.ConfigFile != "" { raw, err := os.ReadFile(o.ConfigFile) if err != nil { @@ -92,7 +89,7 @@ func (o *ClusterSetJoinConfig) validateAndComplete() error { } o.Secret, err = unmarshallSecret(raw) if err != nil { - return fmt.Errorf("failed to unmarshall Secret from token Secret file: %v", err) + return fmt.Errorf("failed to unmarshall Secret from token Secret file: %s, error: %v", o.TokenSecretFile, err) } } @@ -112,17 +109,25 @@ func (o *ClusterSetJoinConfig) validateAndComplete() error { return fmt.Errorf("the ClusterSet ID is required") } if o.ClusterID == "" { - return fmt.Errorf("the member ClusterID is required") + return fmt.Errorf("the ClusterID of member cluster is required") } if o.Namespace == "" { - fmt.Printf("Antrea Multi-cluster Namespace is not specified. Use %s\n.", defaultMemberNamespace) - o.Namespace = defaultMemberNamespace + fmt.Printf("Antrea Multi-cluster Namespace is not specified. Use %s\n.", common.DefaultMemberNamespace) + o.Namespace = common.DefaultMemberNamespace } // Always set the Secret Namespace with the member cluster Multi-cluster Namespace. if o.Secret != nil { o.Secret.Namespace = o.Namespace } + + var err error + if o.k8sClient == nil { + o.k8sClient, err = common.NewClient(cmd) + if err != nil { + return err + } + } return nil } @@ -145,7 +150,6 @@ func unmarshallSecret(raw []byte) (*v1.Secret, error) { if err := decoder.Decode(secret); err != nil { return nil, err } - return secret, nil } @@ -215,7 +219,7 @@ func NewJoinCommand() *cobra.Command { command.Flags().StringVarP(&joinOpts.TokenSecretName, "token-secret-name", "", "", "Name of the Secret resource that contains the member token. "+ "Token Secret name takes precedence over token Secret file and the Secret manifest in the join config file") command.Flags().StringVarP(&joinOpts.LeaderAPIServer, "leader-apiserver", "", "", "API Server endpoint of the leader cluster") - command.Flags().StringVarP(&joinOpts.Namespace, "namespace", "n", defaultMemberNamespace, "Antrea Multi-cluster Namespace. Defaults to "+defaultMemberNamespace) + command.Flags().StringVarP(&joinOpts.Namespace, "namespace", "n", common.DefaultMemberNamespace, "Antrea Multi-cluster Namespace. Defaults to "+common.DefaultMemberNamespace) command.Flags().StringVarP(&joinOpts.ClusterID, "clusterid", "", "", "Cluster ID of the member cluster") command.Flags().StringVarP(&joinOpts.ClusterSetID, "clusterset", "", "", "ClusterSet ID") command.Flags().StringVarP(&joinOpts.TokenSecretFile, "token-secret-file", "", "", "Secret manifest for the member token. If specified, a Secret will be created with the manifest. "+ @@ -228,10 +232,9 @@ func NewJoinCommand() *cobra.Command { func joinRunE(cmd *cobra.Command, args []string) error { var err error - if err = joinOpts.validateAndComplete(); err != nil { + if err = joinOpts.validateAndComplete(cmd); err != nil { return err } - k8sClient, err := common.NewClient(cmd) memberClusterNamespace := joinOpts.Namespace memberClusterID := joinOpts.ClusterID @@ -240,7 +243,7 @@ func joinRunE(cmd *cobra.Command, args []string) error { 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 { + if err := common.Rollback(cmd, joinOpts.k8sClient, createdRes); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to rollback: %v\n", err) } } @@ -250,7 +253,7 @@ func joinRunE(cmd *cobra.Command, args []string) error { joinOpts.Secret.Annotations = map[string]string{ common.CreateByAntctlAnnotation: "true", } - if err := k8sClient.Create(context.TODO(), joinOpts.Secret); err != nil { + if err := joinOpts.k8sClient.Create(context.TODO(), joinOpts.Secret); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to create the Secret from the config file: %v\n", err) return err } @@ -262,17 +265,17 @@ func joinRunE(cmd *cobra.Command, args []string) error { joinOpts.TokenSecretName = joinOpts.Secret.Name } - err = common.CreateClusterClaim(cmd, k8sClient, memberClusterNamespace, memberClusterSet, memberClusterID, &createdRes) + err = common.CreateClusterClaim(cmd, joinOpts.k8sClient, memberClusterNamespace, memberClusterSet, memberClusterID, &createdRes) if err != nil { return err } - err = common.CreateClusterSet(cmd, k8sClient, memberClusterNamespace, memberClusterSet, joinOpts.LeaderAPIServer, joinOpts.TokenSecretName, + err = common.CreateClusterSet(cmd, joinOpts.k8sClient, memberClusterNamespace, memberClusterSet, joinOpts.LeaderAPIServer, joinOpts.TokenSecretName, joinOpts.ClusterID, joinOpts.LeaderClusterID, joinOpts.LeaderNamespace, &createdRes) if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "Waiting for member cluster ready\n") - if err = waitForMemberClusterReady(cmd, k8sClient); err != nil { + if err = waitForMemberClusterReady(cmd, joinOpts.k8sClient); err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Failed to wait for member cluster ready: %v\n", err) return err } diff --git a/pkg/antctl/raw/multicluster/join_test.go b/pkg/antctl/raw/multicluster/join_test.go new file mode 100644 index 00000000000..043b8425dc1 --- /dev/null +++ b/pkg/antctl/raw/multicluster/join_test.go @@ -0,0 +1,301 @@ +// 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" + "log" + "os" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + "antrea.io/antrea/pkg/antctl/raw/multicluster/common" + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestJoin(t *testing.T) { + existingClusterSet := &mcsv1alpha1.ClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-clusterset", + }, + Status: mcsv1alpha1.ClusterSetStatus{ + ClusterStatuses: []mcsv1alpha1.ClusterStatus{ + { + ClusterID: "leader-id", + Conditions: []mcsv1alpha1.ClusterCondition{ + { + Message: "Is the leader", + Status: v1.ConditionTrue, + Type: mcsv1alpha1.ClusterReady, + }, + }, + }, + }, + }, + } + + secretContent := []byte(`#test file +--- +apiVersion: v1 +kind: Secret +metadata: + name: token-secret +data: + ca.crt: YWJjZAo= + namespace: ZGVmYXVsdAo= + token: YWJjZAo= +type: Opaque`) + + configContent := []byte(`apiVersion: multicluster.antrea.io/v1alpha1 +kind: ClusterSetJoinConfig +clusterSetID: test-clusterset +clusterID: cluster-a +namespace: default +leaderClusterID: leader-id +leaderNamespace: leader-ns +leaderAPIServer: "http://localhost" +--- +apiVersion: v1 +kind: Secret +metadata: + name: token-secret +data: + ca.crt: YWJjZAo= + namespace: ZGVmYXVsdAo= + token: YWJjZAo= +type: Opaque`) + + tests := []struct { + name string + expectedOutput string + clusterID string + failureType string + secretFile bool + configFile bool + }{ + { + name: "join successfully", + clusterID: "cluster-a", + expectedOutput: "Member cluster joined successfully", + }, + { + name: "join successfully with Secret file", + clusterID: "cluster-a", + expectedOutput: "Created the Secret from the config file", + secretFile: true, + }, + { + name: "join successfully with config file", + clusterID: "cluster-a", + expectedOutput: "Created the Secret from the config file", + configFile: true, + }, + { + name: "fail to join due to empty ClusterID", + clusterID: "", + expectedOutput: "the ClusterID of member cluster is required", + }, + { + name: "fail to join and rollback", + clusterID: "cluster-a", + failureType: "create", + expectedOutput: "failed to create object", + }, + } + cmd := NewJoinCommand() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.Flag("clusterset").Value.Set("test-clusterset") + + joinOpts.ClusterSetID = "test-clusterset" + joinOpts.LeaderClusterID = "leader-id" + joinOpts.LeaderNamespace = "leader-ns" + joinOpts.LeaderAPIServer = "http://localhost" + joinOpts.TokenSecretName = "member-token" + joinOpts.Namespace = "default" + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + joinOpts.ClusterID = tt.clusterID + joinOpts.k8sClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(existingClusterSet).Build() + if tt.failureType == "create" { + joinOpts.k8sClient = common.FakeCtrlRuntimeClient{ + Client: fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(existingClusterSet).Build(), + ShouldError: true, + } + } + if tt.secretFile { + secret, err := os.CreateTemp("", "secret") + if err != nil { + log.Fatal(err) + } + defer os.Remove(secret.Name()) + secret.Write([]byte(secretContent)) + joinOpts.TokenSecretName = "" + joinOpts.TokenSecretFile = secret.Name() + } + + joinOpts.ConfigFile = "" + if tt.configFile { + config, err := os.CreateTemp("", "config") + if err != nil { + log.Fatal(err) + } + defer os.Remove(config.Name()) + config.Write([]byte(configContent)) + joinOpts.TokenSecretName = "" + joinOpts.TokenSecretFile = "" + joinOpts.ConfigFile = config.Name() + } + err := cmd.Execute() + if err != nil { + assert.Contains(t, err.Error(), tt.expectedOutput) + } else { + assert.Contains(t, buf.String(), tt.expectedOutput) + } + }) + } +} + +func TestJoinOptValidate(t *testing.T) { + tests := []struct { + name string + expectedOutput string + opts *ClusterSetJoinConfig + secretFile bool + }{ + { + name: "empty ClusterID", + expectedOutput: "the ClusterID of leader cluster is required", + opts: &ClusterSetJoinConfig{ + TokenSecretName: "token-a", + }, + }, + { + name: "empty API Server", + expectedOutput: "the API server of the leader cluster is required", + opts: &ClusterSetJoinConfig{ + TokenSecretName: "token-a", + ClusterID: "cluster-a", + LeaderClusterID: "leader-id", + }, + }, + { + name: "empty Secret", + expectedOutput: "a member token Secret must be provided through the Secret name, or Secret file, or Secret manifest in the config file", + opts: &ClusterSetJoinConfig{ + ClusterID: "cluster-a", + LeaderClusterID: "leader-id", + LeaderAPIServer: "http://localhost", + }, + }, + { + name: "empty leader Namespace", + expectedOutput: "the leader cluster Namespace is required", + opts: &ClusterSetJoinConfig{ + ClusterID: "cluster-a", + LeaderClusterID: "leader-id", + LeaderAPIServer: "http://localhost", + TokenSecretName: "token-a", + }, + }, + { + name: "empty ClusterSet ID", + expectedOutput: "the ClusterSet ID is required", + opts: &ClusterSetJoinConfig{ + ClusterID: "cluster-a", + LeaderClusterID: "leader-id", + LeaderAPIServer: "http://localhost", + TokenSecretName: "token-a", + LeaderNamespace: "default", + }, + }, + { + name: "empty member ClusterID", + expectedOutput: "the ClusterID of member cluster is required", + opts: &ClusterSetJoinConfig{ + LeaderClusterID: "leader-id", + LeaderAPIServer: "http://localhost", + TokenSecretName: "token-a", + LeaderNamespace: "default", + ClusterSetID: "test-clusterset", + }, + }, + { + name: "empty kubeconfig", + expectedOutput: "flag accessed but not defined: kubeconfig", + opts: &ClusterSetJoinConfig{ + LeaderClusterID: "leader-id", + LeaderAPIServer: "http://localhost", + TokenSecretName: "token-a", + LeaderNamespace: "default", + ClusterSetID: "test-clusterset", + ClusterID: "cluster-a", + }, + }, + { + name: "failed to unmarshal Secret file", + expectedOutput: "failed to unmarshall Secret from token Secret file", + opts: &ClusterSetJoinConfig{ + LeaderClusterID: "leader-id", + LeaderAPIServer: "http://localhost", + LeaderNamespace: "default", + ClusterSetID: "test-clusterset", + ClusterID: "cluster-a", + }, + secretFile: true, + }, + } + + secretContent := []byte(`apiVersion: v1 + kind: Secret + metadata: + name: token-secret + data: + ca.crt: a + namespace: ZGVmYXVsdAo= + token: YWJjZAo= + type: Opaque`) + cmd := &cobra.Command{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.secretFile { + secret, err := os.CreateTemp("", "secret") + if err != nil { + log.Fatal(err) + } + defer os.Remove(secret.Name()) + secret.Write([]byte(secretContent)) + tt.opts.TokenSecretName = "" + tt.opts.TokenSecretFile = secret.Name() + } + err := tt.opts.validateAndComplete(cmd) + if err != nil { + assert.Contains(t, err.Error(), tt.expectedOutput) + } else { + t.Error("Expected to get error but got nil") + } + }) + } +} diff --git a/pkg/antctl/raw/multicluster/leave.go b/pkg/antctl/raw/multicluster/leave.go index 69a9612e7a3..c3be809f64f 100644 --- a/pkg/antctl/raw/multicluster/leave.go +++ b/pkg/antctl/raw/multicluster/leave.go @@ -40,7 +40,7 @@ func NewLeaveCommand() *cobra.Command { o := common.CleanOptions{} leaveOpts = &o - command.Flags().StringVarP(&o.Namespace, "namespace", "n", defaultMemberNamespace, "Antrea Multi-cluster Namespace. Defaults to "+defaultMemberNamespace) + command.Flags().StringVarP(&o.Namespace, "namespace", "n", common.DefaultMemberNamespace, "Antrea Multi-cluster Namespace. Defaults to "+common.DefaultMemberNamespace) command.Flags().StringVarP(&o.ClusterSet, "clusterset", "", "", "ClusterSet ID") return command diff --git a/pkg/antctl/raw/multicluster/leave_test.go b/pkg/antctl/raw/multicluster/leave_test.go new file mode 100644 index 00000000000..5d7d346ffc1 --- /dev/null +++ b/pkg/antctl/raw/multicluster/leave_test.go @@ -0,0 +1,85 @@ +// 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" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1" + mcsv1alpha2 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha2" + mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme" +) + +func TestLeave(t *testing.T) { + tests := []struct { + name string + expectedOutput string + namespace string + }{ + { + name: "leave successfully", + expectedOutput: "ClusterSet \"test-clusterset\" deleted in Namespace default\nClusterClaim \"id.k8s.io\" deleted in Namespace default\nClusterClaim \"clusterset.k8s.io\" deleted in Namespace default\n", + namespace: "default", + }, + { + name: "fail to leave due to empty Namespace", + expectedOutput: "the Namespace is required", + namespace: "", + }, + } + + cmd := NewLeaveCommand() + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.Flag("clusterset").Value.Set("test-clusterset") + clusterSet := &mcsv1alpha1.ClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-clusterset", + }, + } + clusterClaim1 := &mcsv1alpha2.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "id.k8s.io", + }, + } + clusterClaim2 := &mcsv1alpha2.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "clusterset.k8s.io", + }, + } + fakeClient := fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithObjects(clusterSet, clusterClaim1, clusterClaim2).Build() + leaveOpts.K8sClient = fakeClient + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd.Flag("namespace").Value.Set(tt.namespace) + 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/resourceexport/transform.go b/pkg/antctl/transform/resourceexport/transform.go index 679dbf06ca6..9ce9fc77a57 100644 --- a/pkg/antctl/transform/resourceexport/transform.go +++ b/pkg/antctl/transform/resourceexport/transform.go @@ -39,7 +39,7 @@ func listTransform(l interface{}) (interface{}, error) { for i := range resourceExports { item := resourceExports[i] - o, _ := objectTransform(&item) + o, _ := objectTransform(item) result = append(result, o.(Response)) } @@ -47,7 +47,7 @@ func listTransform(l interface{}) (interface{}, error) { } func objectTransform(o interface{}) (interface{}, error) { - resourceExport := o.(*multiclusterv1alpha1.ResourceExport) + resourceExport := o.(multiclusterv1alpha1.ResourceExport) return Response{ ClusterID: resourceExport.Labels["sourceClusterID"],