Skip to content

Commit

Permalink
Fix antctl ClusterSet output when status is empty (#4174)
Browse files Browse the repository at this point in the history
After a ClusterSet is just created in the leader cluster, it might have
no status yet, then `antctl mc get clustersets -A` will skip print the
ClusterSet when its status is empty.
Fix it by passing an empty status and condition.

Signed-off-by: Lan Luo <[email protected]>
  • Loading branch information
luolanzone authored Sep 2, 2022
1 parent 4a378d5 commit 53bb0c3
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 56 deletions.
65 changes: 23 additions & 42 deletions pkg/antctl/raw/multicluster/get/clusterset.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type clusterSetOptions struct {
namespace string
outputFormat string
allNamespaces bool
k8sClient client.Client
}

var optionsClusterSet *clusterSetOptions
Expand All @@ -53,15 +54,23 @@ Get the specified ClusterSet
$ antctl mc get clusterset <CLUSTERSETID>
`, "\n")

func (o *clusterSetOptions) validateAndComplete() {
func (o *clusterSetOptions) 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 NewClusterSetCommand() *cobra.Command {
Expand All @@ -79,42 +88,28 @@ func NewClusterSetCommand() *cobra.Command {
optionsClusterSet = o
cmdClusterSet.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace of ClusterSets")
cmdClusterSet.Flags().StringVarP(&o.outputFormat, "output", "o", "", "Output format. Supported formats: json|yaml")
cmdClusterSet.Flags().BoolVarP(&o.allNamespaces, "all-namespaces", "A", false, "If present, list ClusterSets across all namespaces")
cmdClusterSet.Flags().BoolVarP(&o.allNamespaces, "all-namespaces", "A", false, "If present, list ClusterSets across all Namespaces")

return cmdClusterSet
}

func runEClusterSet(cmd *cobra.Command, args []string) error {
optionsClusterSet.validateAndComplete()

kubeconfig, err := raw.ResolveKubeconfig(cmd)
err := optionsClusterSet.validateAndComplete(cmd)
if err != nil {
return err
}

argsNum := len(args)
singleResource := false
if argsNum > 0 {
singleResource = true
}

k8sClient, err := client.New(kubeconfig, client.Options{Scheme: multiclusterscheme.Scheme})
if err != nil {
return err
}

var clusterSets []multiclusterv1alpha1.ClusterSet
if singleResource {
if len(args) > 0 {
clusterSetName := args[0]
clusterSet := multiclusterv1alpha1.ClusterSet{}
err = k8sClient.Get(context.TODO(), types.NamespacedName{
err = optionsClusterSet.k8sClient.Get(context.TODO(), types.NamespacedName{
Namespace: optionsClusterSet.namespace,
Name: clusterSetName,
}, &clusterSet)
if err != nil {
return err
}
gvks, unversioned, err := k8sClient.Scheme().ObjectKinds(&clusterSet)
gvks, unversioned, err := optionsClusterSet.k8sClient.Scheme().ObjectKinds(&clusterSet)
if err != nil {
return err
}
Expand All @@ -124,7 +119,7 @@ func runEClusterSet(cmd *cobra.Command, args []string) error {
clusterSets = append(clusterSets, clusterSet)
} else {
clusterSetList := &multiclusterv1alpha1.ClusterSetList{}
err = k8sClient.List(context.TODO(), clusterSetList, &client.ListOptions{Namespace: optionsClusterSet.namespace})
err = optionsClusterSet.k8sClient.List(context.TODO(), clusterSetList, &client.ListOptions{Namespace: optionsClusterSet.namespace})
if err != nil {
return err
}
Expand All @@ -140,23 +135,9 @@ func runEClusterSet(cmd *cobra.Command, args []string) error {
return nil
}

switch optionsClusterSet.outputFormat {
case "json", "yaml":
err := output(clusterSets, true, optionsClusterSet.outputFormat, clusterset.Transform)
if err != nil {
return err
}
default:
clusterSetsNum := len(clusterSets)
for i, singleclusterset := range clusterSets {
err := output(singleclusterset, true, optionsClusterSet.outputFormat, clusterset.Transform)
if err != nil {
return err
}
if i != clusterSetsNum-1 {
fmt.Print("\n")
}
}
err = output(clusterSets, false, optionsClusterSet.outputFormat, cmd.OutOrStdout(), clusterset.Transform)
if err != nil {
return err
}
return nil
}
160 changes: 160 additions & 0 deletions pkg/antctl/raw/multicluster/get/clusterset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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"
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"
mcscheme "antrea.io/antrea/pkg/antctl/raw/multicluster/scheme"
)

func TestGetClusterSet(t *testing.T) {
clusterSetList := &mcsv1alpha1.ClusterSetList{
Items: []mcsv1alpha1.ClusterSet{
{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "clusterset-name",
},
Status: mcsv1alpha1.ClusterSetStatus{},
},
},
}
tests := []struct {
name string
existingClusterSets *mcsv1alpha1.ClusterSetList
args []string
output string
allNamespaces bool
donotFake bool
expectedErr string
expectedOutput string
}{
{
name: "get single ClusterSet",
existingClusterSets: clusterSetList,
args: []string{"clusterset-name"},
expectedOutput: "CLUSTER-ID NAMESPACE CLUSTER-SET-ID TYPE STATUS REASON\n<NONE> default clusterset-name <NONE> <NONE> <NONE>\n",
},
{
name: "get single ClusterSet with json output",
existingClusterSets: clusterSetList,
args: []string{"clusterset-name"},
output: "json",
expectedOutput: "[\n {\n \"kind\": \"ClusterSet\",\n \"apiVersion\": \"multicluster.crd.antrea.io/v1alpha1\",\n \"metadata\": {\n \"name\": \"clusterset-name\",\n \"namespace\": \"default\",\n \"resourceVersion\": \"999\",\n \"creationTimestamp\": null\n },\n \"spec\": {\n \"leaders\": null\n },\n \"status\": {}\n }\n]\n",
},
{
name: "get single ClusterSet with yaml output",
existingClusterSets: clusterSetList,
args: []string{"clusterset-name"},
output: "yaml",
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 all ClusterSets",
allNamespaces: true,
existingClusterSets: &mcsv1alpha1.ClusterSetList{
Items: []mcsv1alpha1.ClusterSet{
{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "clusterset-name",
},
Status: mcsv1alpha1.ClusterSetStatus{},
},
{
ObjectMeta: metav1.ObjectMeta{
Namespace: "kube-system",
Name: "clusterset-1",
},
Status: mcsv1alpha1.ClusterSetStatus{
ClusterStatuses: []mcsv1alpha1.ClusterStatus{{
ClusterID: "cluster-a",
Conditions: []mcsv1alpha1.ClusterCondition{
{
Message: "Member Connected",
Reason: "Connected",
Status: v1.ConditionTrue,
Type: mcsv1alpha1.ClusterReady,
},
},
},
},
},
},
},
},
expectedOutput: "CLUSTER-ID NAMESPACE CLUSTER-SET-ID TYPE STATUS REASON \n<NONE> default clusterset-name <NONE> <NONE> <NONE> \ncluster-a kube-system clusterset-1 Ready True Connected\n",
},
{
name: "get all ClusterSets but empty result",
allNamespaces: true,
expectedOutput: "No resources found\n",
},
{
name: "get all ClusterSets but empty result in default Namespace",
expectedOutput: "No resource found in Namespace default\n",
},
{
name: "error due to no kubeconfig",
expectedErr: "flag accessed but not defined: kubeconfig",
donotFake: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := NewClusterSetCommand()
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.existingClusterSets != nil {
fakeClient = fake.NewClientBuilder().WithScheme(mcscheme.Scheme).WithLists(tt.existingClusterSets).Build()
}
if !tt.donotFake {
optionsClusterSet.k8sClient = fakeClient
}

if tt.allNamespaces {
optionsClusterSet.allNamespaces = true
}
if tt.output != "" {
optionsClusterSet.outputFormat = tt.output
}
err := cmd.Execute()
if err != nil {
assert.Equal(t, tt.expectedErr, err.Error())
} else {
assert.Equal(t, tt.expectedOutput, buf.String())
}
})
}
}
13 changes: 7 additions & 6 deletions pkg/antctl/raw/multicluster/get/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,28 @@
package get

import (
"os"
"io"

antcltOutput "antrea.io/antrea/pkg/antctl/output"
antctlOutput "antrea.io/antrea/pkg/antctl/output"
)

func output(resources interface{}, single bool, outputFormat string, transform func(r interface{}, single bool) (interface{}, error)) error {
func output(resources interface{}, single bool, outputFormat string, output io.Writer,
transform func(r interface{}, single bool) (interface{}, error)) error {
switch outputFormat {
case "json":
if err := antcltOutput.JsonOutput(resources, os.Stdout); err != nil {
if err := antctlOutput.JsonOutput(resources, output); err != nil {
return err
}
case "yaml":
if err := antcltOutput.YamlOutput(resources, os.Stdout); err != nil {
if err := antctlOutput.YamlOutput(resources, output); err != nil {
return err
}
default:
obj, err := transform(resources, single)
if err != nil {
return err
}
err = antcltOutput.TableOutputForGetCommands(obj, os.Stdout)
err = antctlOutput.TableOutputForGetCommands(obj, output)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit 53bb0c3

Please sign in to comment.