From c9ff3bee2b3f5ec7b2100b928b786158fe1090be Mon Sep 17 00:00:00 2001 From: Kenshin <35095889+kqzh@users.noreply.github.com> Date: Wed, 14 Jul 2021 15:25:08 +0800 Subject: [PATCH] ngctl: add check command and doc (#39) --- doc/user/ngctl_guide.md | 85 +++++++++++ pkg/ngctl/cmd/check/check.go | 195 ++++++++++++++++++++++++ pkg/ngctl/cmd/check/check_test.go | 206 ++++++++++++++++++++++++++ pkg/ngctl/cmd/cmd.go | 2 + pkg/ngctl/cmd/console/console.go | 6 +- pkg/ngctl/cmd/console/console_test.go | 69 +++++++++ pkg/ngctl/cmd/info/info.go | 21 +-- pkg/ngctl/cmd/list/list.go | 127 +++++++++------- pkg/ngctl/cmd/use/use.go | 20 +-- pkg/ngctl/cmd/util/factory.go | 47 +----- pkg/ngctl/cmd/util/factory_test.go | 83 +++++++++++ pkg/ngctl/cmd/version/version.go | 9 +- pkg/ngctl/config/config_test.go | 86 +++++++++++ 13 files changed, 831 insertions(+), 125 deletions(-) create mode 100644 doc/user/ngctl_guide.md create mode 100644 pkg/ngctl/cmd/check/check.go create mode 100644 pkg/ngctl/cmd/check/check_test.go create mode 100644 pkg/ngctl/cmd/console/console_test.go create mode 100644 pkg/ngctl/cmd/util/factory_test.go create mode 100644 pkg/ngctl/config/config_test.go diff --git a/doc/user/ngctl_guide.md b/doc/user/ngctl_guide.md new file mode 100644 index 00000000..1dd61fe9 --- /dev/null +++ b/doc/user/ngctl_guide.md @@ -0,0 +1,85 @@ + +# Overview +ngctl is a terminal cmd tool for Nebula Graph managed by nebula-operator, it has the following commands: +- [ngctl use](#ngctl-use) +- [ngctl console](#ngctl-console) +- [ngctl list](#ngctl-list) +- [ngctl check](#ngctl-check) +- [ngctl info](#ngctl-info) + +## ngctl use +`ngctl use` specify a nebula cluster to use. By using a certain cluster, you may omit --nebulacluster option in many control commands. + +``` +Examples: + # specify a nebula cluster to use + ngctl use demo-cluster + + # specify the cluster name and namespace + ngctl use demo-cluster -n test-system +``` +## ngctl console +`ngctl console` create a nebula-console pod and connect to the specified nebula cluster. + +``` +Examples: + # open console to the current nebula cluster, which is set by 'ngctl use' command + ngctl console + + # Open console to the specified nebula cluster + ngctl console --nebulacluster=nebula +``` +## ngctl list +`ngctl list` list nebula clusters or there sub resources. Its usage is the same as `kubectl get`, but only resources related to nbuela cluster are displayed. +``` +Examples: + # List all nebula clusters. + ngctl list + + # List all nebula clusters in all namespaces. + ngctl list -A + + # List all nebula clusters with json format. + ngctl list -o json + + # List nebula cluster sub resources with specified cluster name. + ngctl list pod --nebulacluster=nebula + + # Return only the metad's phase value of the specified pod. + ngctl list -o template --template="{{.status.graphd.phase}}" NAME + + # List image information in custom columns. + ngctl list -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,IMAGE:.spec.graphd.image +``` + +## ngctl check +`ngctl check` check whether the specified nebula cluster resources are ready. Command will print the error message from nebula cluster resource conditions, help you locate the reason quickly. + +``` +Examples: + # check whether the specified nebula cluster is ready + ngctl check + + # check specified nebula cluster pods + ngctl check pods --nebulacluster=nebula +``` + +## ngctl info +`ngctl info` get current nebula cluster information, the cluster is set by 'ngctl use' command or use `--nebulacluster` flag. + +```Examples: + # get current nebula cluster information, which is set by 'ngctl use' command + ngctl info + + # get current nebula cluster information, which is set by '--nebulacluster' flag + ngctl info --nebulacluster=nebula +``` +## ngctl version +`nfgctl version` print the cli and nebula operator version + +```bash +Examples: + # Print the cli and nebula operator version + ngctl version +``` + diff --git a/pkg/ngctl/cmd/check/check.go b/pkg/ngctl/cmd/check/check.go new file mode 100644 index 00000000..c18bfc90 --- /dev/null +++ b/pkg/ngctl/cmd/check/check.go @@ -0,0 +1,195 @@ +/* +Copyright 2021 Vesoft Inc. + +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 check + +import ( + "bytes" + "context" + "text/tabwriter" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/util/templates" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/vesoft-inc/nebula-operator/apis/apps/v1alpha1" + "github.com/vesoft-inc/nebula-operator/pkg/label" + cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util/ignore" +) + +var ( + checkLong = templates.LongDesc(` + Check whether the specified nebula cluster resources are ready.`) + + checkExample = templates.Examples(` + # check whether the specified nebula cluster is ready + ngctl check + + # check specified nebula cluster pods + ngctl check pods --nebulacluster=nebula`) +) + +type CheckOptions struct { + Namespace string + NebulaClusterName string + ResourceType string + + runtimeCli client.Client + genericclioptions.IOStreams +} + +func NewCheckOptions(streams genericclioptions.IOStreams) *CheckOptions { + return &CheckOptions{ + IOStreams: streams, + } +} + +// NewCmdCheck returns a cobra command for check whether nebula cluster resources are ready +func NewCmdCheck(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewCheckOptions(ioStreams) + + cmd := &cobra.Command{ + Use: "check", + Short: "check whether nebula cluster resources are ready", + Long: checkLong, + Example: checkExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, args)) + cmdutil.CheckErr(o.Validate(cmd)) + cmdutil.CheckErr(o.RunCheck()) + }, + } + + f.AddFlags(cmd) + return cmd +} + +// Complete completes all the required options +func (o *CheckOptions) Complete(f cmdutil.Factory, args []string) error { + var err error + + o.NebulaClusterName, o.Namespace, err = f.GetNebulaClusterNameAndNamespace(true, nil) + if err != nil && !cmdutil.IsErNotSpecified(err) { + return err + } + + if len(args) > 0 { + o.ResourceType = args[0] + } else { + o.ResourceType = cmdutil.NebulaClusterResourceType + } + + o.runtimeCli, err = f.ToRuntimeClient() + if err != nil { + return err + } + + return nil +} + +// Validate validates the provided options +func (o *CheckOptions) Validate(cmd *cobra.Command) error { + if o.NebulaClusterName == "" { + return cmdutil.UsageErrorf(cmd, "using 'ngctl use' or '--nebulacluster' to set nebula cluster first.") + } + + return nil +} + +// RunCheck executes check command +func (o *CheckOptions) RunCheck() error { + switch o.ResourceType { + case cmdutil.NebulaClusterResourceType, "nebulaclusters", "nc": + { + str, err := o.CheckNebulaCluster() + if err != nil { + return err + } + ignore.Fprintf(o.Out, "%s\n", str) + } + case "pod", "pods": + { + str, err := o.CheckPods() + if err != nil { + return err + } + ignore.Fprintf(o.Out, "%s\n", str) + } + } + + return nil +} + +func (o *CheckOptions) CheckNebulaCluster() (string, error) { + var nc appsv1alpha1.NebulaCluster + key := client.ObjectKey{Namespace: o.Namespace, Name: o.NebulaClusterName} + if err := o.runtimeCli.Get(context.TODO(), key, &nc); err != nil { + return "", err + } + for _, cond := range nc.Status.Conditions { + if cond.Type == appsv1alpha1.NebulaClusterReady { + return cond.Message, nil + } + } + return "", nil +} + +func (o *CheckOptions) CheckPods() (string, error) { + selector, err := label.New().Cluster(o.NebulaClusterName).Selector() + if err != nil { + return "", err + } + + var pods corev1.PodList + listOptions := client.ListOptions{ + LabelSelector: selector, + Namespace: o.Namespace, + } + if err := o.runtimeCli.List(context.TODO(), &pods, &listOptions); err != nil { + return "", err + } + + allWork := true + tw := new(tabwriter.Writer) + buf := &bytes.Buffer{} + tw.Init(buf, 0, 8, 4, ' ', 0) + + ignore.Fprintf(tw, "Trouble Pods:\n") + ignore.Fprintf(tw, "\tPodName\tPhase\tConditionType\tMessage\n") + ignore.Fprintf(tw, "\t-------\t------\t-------------\t-------\n") + + for i := range pods.Items { + if pods.Items[i].Status.Phase != corev1.PodRunning { + allWork = false + for _, cond := range pods.Items[i].Status.Conditions { + if cond.Status != corev1.ConditionTrue { + ignore.Fprintf(tw, "\t%s", pods.Items[i].Name) + ignore.Fprintf(tw, "\t%s\t%s\t%s\n", pods.Items[i].Status.Phase, cond.Type, cond.Message) + } + } + } + } + + _ = tw.Flush() + + if allWork { + return "All pods are running", nil + } + return buf.String(), nil +} diff --git a/pkg/ngctl/cmd/check/check_test.go b/pkg/ngctl/cmd/check/check_test.go new file mode 100644 index 00000000..d1383b74 --- /dev/null +++ b/pkg/ngctl/cmd/check/check_test.go @@ -0,0 +1,206 @@ +/* +Copyright 2021 Vesoft Inc. + +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 check + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/vesoft-inc/nebula-operator/apis/apps/v1alpha1" +) + +var scheme = runtime.NewScheme() + +func init() { + _ = clientgoscheme.AddToScheme(scheme) + _ = v1alpha1.AddToScheme(scheme) +} + +func TestCheckNebulaCluster(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + testcases := []struct { + desc string + options CheckOptions + condType v1alpha1.NebulaClusterConditionType + message string + expected string + }{ + { + desc: "", + options: CheckOptions{ + Namespace: "", + NebulaClusterName: "nc-test1", + runtimeCli: fakeClient, + }, + condType: v1alpha1.NebulaClusterReady, + message: "Nebula cluster is running", + expected: "Nebula cluster is running", + }, + { + desc: "", + options: CheckOptions{ + Namespace: "nebula", + NebulaClusterName: "nc-test2", + runtimeCli: fakeClient, + }, + condType: v1alpha1.NebulaClusterReady, + message: "Nebula cluster is running", + expected: "Nebula cluster is running", + }, + { + desc: "", + options: CheckOptions{ + Namespace: "nebula2", + NebulaClusterName: "nc-test3", + runtimeCli: fakeClient, + }, + expected: "", + }, + } + + for _, tc := range testcases { + fakeNebulaCluster := &v1alpha1.NebulaCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.options.NebulaClusterName, + Namespace: tc.options.Namespace, + }, + Status: v1alpha1.NebulaClusterStatus{ + Conditions: []v1alpha1.NebulaClusterCondition{ + { + Type: tc.condType, + Message: tc.message, + }, + }, + }, + } + _ = fakeClient.Create(context.Background(), fakeNebulaCluster, &client.CreateOptions{}) + } + + for i, tc := range testcases { + str, err := tc.options.CheckNebulaCluster() + if err != nil { + t.Error(err) + } + if tc.expected != str { + t.Errorf("%d: Expected: \n%#v\n but actual: \n%#v\n", i, tc.expected, str) + } + } +} + +func TestCheckPods(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + testcases := []struct { + desc string + options CheckOptions + phase corev1.PodPhase + conditions []corev1.PodCondition + expected string + }{ + { + options: CheckOptions{ + Namespace: "", + NebulaClusterName: "nc-test1", + runtimeCli: fakeClient, + }, + phase: corev1.PodPending, + conditions: []corev1.PodCondition{ + { + Type: corev1.PodScheduled, + Status: corev1.ConditionFalse, + Message: "0/5 nodes are available: 5 pod has unbound immediate PersistentVolumeClaims.", + }, + }, + expected: `Trouble Pods: + PodName Phase ConditionType Message + ------- ------ ------------- ------- + nc-test1 Pending PodScheduled 0/5 nodes are available: 5 pod has unbound immediate PersistentVolumeClaims. +`, + }, { + options: CheckOptions{ + Namespace: "nebula", + NebulaClusterName: "nc-test2", + runtimeCli: fakeClient, + }, + phase: corev1.PodRunning, + conditions: []corev1.PodCondition{}, + expected: "All pods are running", + }, { + options: CheckOptions{ + Namespace: "nebula2", + NebulaClusterName: "nc-test3", + runtimeCli: fakeClient, + }, + phase: corev1.PodFailed, + conditions: []corev1.PodCondition{ + { + Type: corev1.PodScheduled, + Status: corev1.ConditionFalse, + Message: "0/5 nodes are available: 5 pod has unbound immediate PersistentVolumeClaims.", + }, + { + Type: corev1.PodInitialized, + Status: corev1.ConditionFalse, + Message: "0/5 nodes are available: 5 pod has unbound immediate PersistentVolumeClaims.", + }, + }, + expected: `Trouble Pods: + PodName Phase ConditionType Message + ------- ------ ------------- ------- + nc-test3 Failed PodScheduled 0/5 nodes are available: 5 pod has unbound immediate PersistentVolumeClaims. + nc-test3 Failed Initialized 0/5 nodes are available: 5 pod has unbound immediate PersistentVolumeClaims. +`, + }, + } + + for _, tc := range testcases { + fakePod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.options.NebulaClusterName, + Namespace: tc.options.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/cluster": tc.options.NebulaClusterName, + "app.kubernetes.io/managed-by": "nebula-operator", + "app.kubernetes.io/name": "nebula-graph", + }, + }, + Status: corev1.PodStatus{ + Phase: tc.phase, + Conditions: tc.conditions, + }, + } + _ = fakeClient.Create(context.Background(), fakePod, &client.CreateOptions{}) + } + + for i, tc := range testcases { + str, err := tc.options.CheckPods() + if err != nil { + t.Error(err) + } + if tc.expected != str { + t.Errorf("%d: Expected: \n%#v\n but actual: \n%#v\n", i, tc.expected, str) + } + } +} diff --git a/pkg/ngctl/cmd/cmd.go b/pkg/ngctl/cmd/cmd.go index 307584ed..341e271b 100644 --- a/pkg/ngctl/cmd/cmd.go +++ b/pkg/ngctl/cmd/cmd.go @@ -24,6 +24,7 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/kubectl/pkg/util/templates" + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/check" "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/console" "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/info" "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/list" @@ -64,6 +65,7 @@ func NewNgctlCmd(in io.Reader, out, err io.Writer) *cobra.Command { { Message: "Cluster Management Commands:", Commands: []*cobra.Command{ + check.NewCmdCheck(f, ioStreams), console.NewCmdConsole(f, ioStreams), info.NewCmdInfo(f, ioStreams), list.NewCmdList(f, ioStreams), diff --git a/pkg/ngctl/cmd/console/console.go b/pkg/ngctl/cmd/console/console.go index 3874a226..b1bda3bd 100644 --- a/pkg/ngctl/cmd/console/console.go +++ b/pkg/ngctl/cmd/console/console.go @@ -53,7 +53,7 @@ to set nebula cluster first. ) type ( - // Options is a struct to support version command + // Options is a struct to support console command Options struct { Namespace string NebulaClusterName string @@ -79,7 +79,7 @@ func NewOptions(ioStreams genericclioptions.IOStreams) *Options { } } -// NewCmdConsole returns a cobra command for specify a nebula cluster to use +// NewCmdConsole returns a cobra command for open console to the specified nebula cluster func NewCmdConsole(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { o := NewOptions(ioStreams) cmd := &cobra.Command{ @@ -159,7 +159,7 @@ func (o *Options) Validate(cmd *cobra.Command) error { return nil } -// Run executes use command +// Run executes console command func (o *Options) Run() error { pod, err := o.generateConsolePod() if err != nil { diff --git a/pkg/ngctl/cmd/console/console_test.go b/pkg/ngctl/cmd/console/console_test.go new file mode 100644 index 00000000..09dd3c4f --- /dev/null +++ b/pkg/ngctl/cmd/console/console_test.go @@ -0,0 +1,69 @@ +/* +Copyright 2021 Vesoft Inc. + +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 console + +import ( + "context" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/vesoft-inc/nebula-operator/apis/apps/v1alpha1" +) + +var scheme = runtime.NewScheme() + +func init() { + _ = clientgoscheme.AddToScheme(scheme) + _ = v1alpha1.AddToScheme(scheme) +} + +func TestGenerateConsolePod(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + fakeOptions := Options{ + Namespace: "nebula", + NebulaClusterName: "nc-test1", + Image: consoleDefaultImage, + User: "root", + Password: "*", + runtimeCli: fakeClient, + restClientGetter: nil, + } + + fakeNebulaCluster := &v1alpha1.NebulaCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeOptions.NebulaClusterName, + Namespace: fakeOptions.Namespace, + }, + } + _ = fakeClient.Create(context.Background(), fakeNebulaCluster, &client.CreateOptions{}) + + pod, err := fakeOptions.generateConsolePod() + if err != nil { + t.Error(err) + } + + err = fakeOptions.runtimeCli.Create(context.Background(), pod, &client.CreateOptions{}) + if err != nil { + t.Error(err) + } +} diff --git a/pkg/ngctl/cmd/info/info.go b/pkg/ngctl/cmd/info/info.go index 5dbbfb78..d5fcd93c 100644 --- a/pkg/ngctl/cmd/info/info.go +++ b/pkg/ngctl/cmd/info/info.go @@ -21,29 +21,30 @@ import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/util/templates" "sigs.k8s.io/controller-runtime/pkg/client" cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" ) -const ( - infoLong = ` - Get specified nebula cluster information. -` - infoExample = ` +var ( + infoLong = templates.LongDesc(` + Get specified nebula cluster information.`) + + infoExample = templates.Examples(` # get current nebula cluster information, which is set by 'ngctl use' command ngctl info # get specified nebula cluster information - ngctl info CLUSTER_NAME -` + ngctl info CLUSTER_NAME`) + infoUsage = `expected 'info CLUSTER_NAME' for the info command, or using 'ngctl use' to set nebula cluster first. ` ) type ( - // Options is a struct to support version command + // Options is a struct to support info command Options struct { Namespace string NebulaClusterName string @@ -60,7 +61,7 @@ func NewOptions(streams genericclioptions.IOStreams) *Options { } } -// NewCmdInfo returns a cobra command for specify a nebula cluster to use +// NewCmdInfo returns a cobra command for get specified nebula cluster information func NewCmdInfo(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { o := NewOptions(ioStreams) cmd := &cobra.Command{ @@ -104,7 +105,7 @@ func (o *Options) Validate(cmd *cobra.Command) error { return nil } -// Run executes use command +// Run executes info command func (o *Options) Run() error { nci, err := NewNebulaClusterInfo(o.NebulaClusterName, o.Namespace, o.runtimeCli) if err != nil { diff --git a/pkg/ngctl/cmd/list/list.go b/pkg/ngctl/cmd/list/list.go index 508f1e3d..c7d56be4 100644 --- a/pkg/ngctl/cmd/list/list.go +++ b/pkg/ngctl/cmd/list/list.go @@ -35,22 +35,20 @@ import ( "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" + "k8s.io/kubectl/pkg/util/templates" + "github.com/vesoft-inc/nebula-operator/pkg/label" cmdutil "github.com/vesoft-inc/nebula-operator/pkg/ngctl/cmd/util" ) -const ( - listLong = ` - List nebula clusters. +var ( + listLong = templates.LongDesc(` + List nebula clusters or there sub resources. - Prints a table of the most important information about the nebula clusters. You can - filter the list using a label selector and the --selector flag. You will only see - results in your current namespace unless you pass --all-namespaces. - - By specifying the output as 'template' and providing a Go template as the value of - the --template flag, you can filter the attributes of the nebula clusters. -` - listExample = ` + Prints a table of the most important information about the nebula cluster resources. + You can use many of the same flags as kubectl get`) + + listExample = templates.Examples(` # List all nebula clusters. ngctl list @@ -60,52 +58,56 @@ const ( # List all nebula clusters with json format. ngctl list -o json - # List a single nebula clusters with specified NAME in ps output format. - ngctl list NAME + # List nebula cluster sub resources with specified cluster name. + ngctl list pod --nebulacluster=nebula # Return only the metad's phase value of the specified pod. ngctl list -o template --template="{{.status.graphd.phase}}" NAME # List image information in custom columns. - ngctl list -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,IMAGE:.spec.graphd.image -` - OutputFormatWide = "wide" + ngctl list -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,IMAGE:.spec.graphd.image`) ) -type ( - // Options is a struct to support version command - Options struct { - LabelSelector string - FieldSelector string - AllNamespaces bool - Namespace string - NebulaClusterNames []string - Sort bool - SortBy string - IgnoreNotFound bool - IsHumanReadablePrinter bool - ServerPrint bool - - PrintFlags *PrintFlags - ToPrinter func(mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinterFunc, error) - - builder *resource.Builder - genericclioptions.IOStreams - } +const ( + OutputFormatWide = "wide" ) -// NewOptions returns initialized Options -func NewOptions(streams genericclioptions.IOStreams) *Options { - return &Options{ +// ListOptions is a struct to support list command +type ListOptions struct { + Namespace string + NebulaClusterName string + NebulaClusterLabel string + ResourceType string + + LabelSelector string + FieldSelector string + AllNamespaces bool + Sort bool + SortBy string + IgnoreNotFound bool + + ServerPrint bool + + PrintFlags *PrintFlags + ToPrinter func(mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinterFunc, error) + IsHumanReadablePrinter bool + + builder *resource.Builder + genericclioptions.IOStreams +} + +// NewListOptions returns initialized Options +func NewListOptions(streams genericclioptions.IOStreams) *ListOptions { + return &ListOptions{ PrintFlags: NewGetPrintFlags(), IOStreams: streams, ServerPrint: true, } } -// NewCmdList returns a cobra command for list nebula clusters +// NewCmdList returns a cobra command for list nebula clusters or there sub resources. func NewCmdList(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - o := NewOptions(ioStreams) + o := NewListOptions(ioStreams) cmd := &cobra.Command{ Use: "list", Short: "list all nebula clusters", @@ -113,18 +115,20 @@ func NewCmdList(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra Example: listExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.Validate(cmd)) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) + f.AddFlags(cmd) o.AddFlags(cmd) return cmd } -// AddFlags add all the flags. -func (o *Options) AddFlags(cmd *cobra.Command) { +// AddFlags add extra list options flags. +func (o *ListOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.IgnoreNotFound, "ignore-not-found", o.IgnoreNotFound, "If the requested object does not exist the command will return exit code 0.") cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, @@ -139,13 +143,23 @@ func (o *Options) AddFlags(cmd *cobra.Command) { } // Complete completes all the required options -func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { +func (o *ListOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error - o.NebulaClusterNames, o.Namespace, err = f.GetNebulaClusterNamesAndNamespace(false, args) + + o.NebulaClusterName, o.Namespace, err = f.GetNebulaClusterNameAndNamespace(false, nil) if err != nil && !cmdutil.IsErNotSpecified(err) { return err } + o.NebulaClusterLabel = label.ClusterLabelKey + "=" + o.NebulaClusterName + + if len(args) > 0 { + o.ResourceType = args[0] + } else { + o.ResourceType = cmdutil.NebulaClusterResourceType + o.NebulaClusterLabel = "" + } + o.SortBy, err = cmd.Flags().GetString("sort-by") if err != nil { return err @@ -203,7 +217,7 @@ func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) return nil } -func (o *Options) Validate(cmd *cobra.Command) error { +func (o *ListOptions) Validate(cmd *cobra.Command) error { if showLabels, err := cmd.Flags().GetBool("show-labels"); err != nil { return err } else if showLabels { @@ -213,10 +227,14 @@ func (o *Options) Validate(cmd *cobra.Command) error { } } + if o.NebulaClusterName == "" && o.ResourceType != cmdutil.NebulaClusterResourceType { + return cmdutil.UsageErrorf(cmd, "using '--nebulacluster' to set nebula cluster first.") + } + return nil } -func (o *Options) transformRequests(req *rest.Request) { +func (o *ListOptions) transformRequests(req *rest.Request) { if !o.ServerPrint || !o.IsHumanReadablePrinter { return } @@ -232,14 +250,20 @@ func (o *Options) transformRequests(req *rest.Request) { } } -// Run executes use command -func (o *Options) Run() error { +// Run executes list command +func (o *ListOptions) Run() error { + if o.LabelSelector == "" { + o.LabelSelector = o.NebulaClusterLabel + } else { + o.LabelSelector = o.LabelSelector + "," + o.NebulaClusterLabel + } + r := o.builder. Unstructured(). NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces). LabelSelectorParam(o.LabelSelector). FieldSelectorParam(o.FieldSelector). - ResourceTypeOrNameArgs(true, append([]string{"nebulacluster"}, o.NebulaClusterNames...)...). + ResourceTypeOrNameArgs(true, o.ResourceType). ContinueOnError(). Latest(). Flatten(). @@ -261,7 +285,6 @@ func (o *Options) Run() error { if err != nil { return err } - objs := make([]runtime.Object, len(infos)) for ix := range infos { objs[ix] = infos[ix].Object @@ -314,7 +337,7 @@ func (o *Options) Run() error { return nil } -func (o *Options) printGeneric(r *resource.Result) error { +func (o *ListOptions) printGeneric(r *resource.Result) error { var errs []error singleItemImplied := false infos, err := r.IntoSingleItemImplied(&singleItemImplied).Infos() diff --git a/pkg/ngctl/cmd/use/use.go b/pkg/ngctl/cmd/use/use.go index a3ae9a4b..ee6ff9ab 100644 --- a/pkg/ngctl/cmd/use/use.go +++ b/pkg/ngctl/cmd/use/use.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/util/templates" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/vesoft-inc/nebula-operator/apis/apps/v1alpha1" @@ -29,24 +30,25 @@ import ( "github.com/vesoft-inc/nebula-operator/pkg/ngctl/config" ) -const ( - useLong = ` +var ( + useLong = templates.LongDesc(` Specify a nebula cluster to use. By using a certain cluster, you may omit --nebulacluster option - in many control commands. -` - useExample = ` + in many control commands.`) + + useExample = templates.Examples(` # specify a nebula cluster to use ngctl use demo-cluster - # specify kubernetes context and namespace - ngctl use --namespace=demo-ns demo-cluster -` + + # specify the cluster name and namespace + ngctl use --namespace=demo-ns demo-cluster`) + useUsage = "expected 'use CLUSTER_NAME' for the use command" ) type ( - // Options is a struct to support version command + // Options is a struct to support use command Options struct { Namespace string NebulaClusterName string diff --git a/pkg/ngctl/cmd/util/factory.go b/pkg/ngctl/cmd/util/factory.go index 7a173ed5..1dbb588b 100644 --- a/pkg/ngctl/cmd/util/factory.go +++ b/pkg/ngctl/cmd/util/factory.go @@ -36,9 +36,6 @@ type ( ToRuntimeClient() (client.Client, error) GetNebulaClusterNameAndNamespace(withUseConfig bool, args []string) (string, string, error) GetNebulaClusterNamesAndNamespace(withUseConfig bool, args []string) ([]string, string, error) - // GetNebulaClusterName() (string, error) - // GetNebulaClusterNameWithoutConfig() string - // GetNamespace() (string, error) GetNebulaClusterConfigFile() (string, error) } factoryImpl struct { @@ -52,6 +49,8 @@ type ( } ) +const NebulaClusterResourceType = "nebulacluster" + var ( _ Factory = (*factoryImpl)(nil) errNotSpecified = errors.New("Not Specified") @@ -82,24 +81,6 @@ func (f *factoryImpl) ToRuntimeClient() (client.Client, error) { return client.New(restConfig, client.Options{}) } -func (f *factoryImpl) GetNamespace() (string, error) { - namespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() - if err != nil { - return "", err - } - - if enforceNamespace { - return namespace, err - } - - c, err := f.getNebulaClusterConfig() - if err != nil { - return namespace, nil - } - - return c.Namespace, err -} - func (f *factoryImpl) GetNebulaClusterNameAndNamespace(withUseConfig bool, args []string) (name, namespace string, err error) { var names []string names, namespace, err = f.GetNebulaClusterNamesAndNamespace(withUseConfig, args) @@ -115,11 +96,9 @@ func (f *factoryImpl) GetNebulaClusterNamesAndNamespace(withUseConfig bool, args if err != nil { return nil, "", err } - if f.nebulaClusterName != "" { return []string{f.nebulaClusterName}, namespace, nil } - if len(args) > 0 { return args, namespace, nil } @@ -136,28 +115,6 @@ func (f *factoryImpl) GetNebulaClusterNamesAndNamespace(withUseConfig bool, args return []string{c.ClusterName}, c.Namespace, nil } -func (f *factoryImpl) GetNebulaClusterName() (string, error) { - return f.getNebulaClusterName(true) -} - -func (f *factoryImpl) GetNebulaClusterNameWithoutConfig() string { - name, _ := f.getNebulaClusterName(false) - return name -} - -func (f *factoryImpl) getNebulaClusterName(withConfig bool) (string, error) { - if !withConfig || f.nebulaClusterName != "" { - return f.nebulaClusterName, nil - } - - c, err := f.getNebulaClusterConfig() - if err != nil { - return "", err - } - - return c.ClusterName, nil -} - func (f *factoryImpl) GetNebulaClusterConfigFile() (string, error) { if f.nebulaClusterConfigFile != "" { return f.nebulaClusterConfigFile, nil diff --git a/pkg/ngctl/cmd/util/factory_test.go b/pkg/ngctl/cmd/util/factory_test.go new file mode 100644 index 00000000..72dedabd --- /dev/null +++ b/pkg/ngctl/cmd/util/factory_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2021 Vesoft Inc. + +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 util + +import ( + "os" + "sync" + "testing" + + "github.com/vesoft-inc/nebula-operator/pkg/ngctl/config" +) + +func TestGetNebulaClusterConfig(t *testing.T) { + testcases := []struct { + desc string + config *config.NebulaClusterConfig + factory *factoryImpl + expectedErr bool + }{ + { + config: &config.NebulaClusterConfig{ + Namespace: "nebula-system", + ClusterName: "nebula", + }, + factory: &factoryImpl{ + nebulaClusterName: "", + nebulaClusterConfigFile: "", + loadingLock: sync.Mutex{}, + nebulaClusterConfig: nil, + }, + expectedErr: true, + }, + { + factory: &factoryImpl{ + nebulaClusterName: "", + nebulaClusterConfigFile: "", + loadingLock: sync.Mutex{}, + nebulaClusterConfig: nil, + }, + expectedErr: true, + }, + } + + defer func() { + for _, tc := range testcases { + _ = os.Remove(tc.factory.nebulaClusterConfigFile) + } + }() + + for i, tc := range testcases { + configFile, err := os.CreateTemp("", "") + if err != nil { + t.Error(err) + } + tc.factory.nebulaClusterConfigFile = configFile.Name() + + if tc.config != nil { + err := tc.config.SaveToFile(tc.factory.nebulaClusterConfigFile) + if err != nil { + t.Error(err) + } + } + + _, err = tc.factory.getNebulaClusterConfig() + if (err != nil) == tc.expectedErr { + t.Errorf("%d: Expected: \n%#v\n but actual: \n%#v\n", i, tc.expectedErr, err != nil) + } + } +} diff --git a/pkg/ngctl/cmd/version/version.go b/pkg/ngctl/cmd/version/version.go index 166d9329..39a8c765 100644 --- a/pkg/ngctl/cmd/version/version.go +++ b/pkg/ngctl/cmd/version/version.go @@ -24,6 +24,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes" + "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubernetes/pkg/apis/core" "github.com/vesoft-inc/nebula-operator/pkg/label" @@ -32,12 +33,7 @@ import ( operatorversion "github.com/vesoft-inc/nebula-operator/pkg/version" ) -const ( - versionExample = ` - # Print the cli and nebula operator version - ngctl version -` -) +var versionExample = templates.Examples(`# Print the cli and nebula operator version、ngctl version`) type ( // Options is a struct to support version command @@ -62,6 +58,7 @@ func NewCmdVersion(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *co cmd := &cobra.Command{ Use: "version", Short: "Print the cli and nebula operator version", + Long: "Print the cli and nebula operator version", Example: versionExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f)) diff --git a/pkg/ngctl/config/config_test.go b/pkg/ngctl/config/config_test.go new file mode 100644 index 00000000..70ad63a3 --- /dev/null +++ b/pkg/ngctl/config/config_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2021 Vesoft Inc. + +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 config + +import ( + "os" + "testing" +) + +func TestLoadFromFileAndSaveToFile(t *testing.T) { + testcases := []struct { + desc string + filename string + namespace string + nebulaCluster string + config NebulaClusterConfig + expected NebulaClusterConfig + }{ + { + desc: "", + filename: "", + config: NebulaClusterConfig{ + ClusterName: "nebula", + Namespace: "nebula-system", + }, + expected: NebulaClusterConfig{ + ClusterName: "nebula", + Namespace: "nebula-system", + }, + }, + { + desc: "", + filename: "", + config: NebulaClusterConfig{ + ClusterName: "nebula", + Namespace: "", + }, + expected: NebulaClusterConfig{ + ClusterName: "nebula", + Namespace: "", + }, + }, + } + + defer func() { + for _, tc := range testcases { + _ = os.Remove(tc.filename) + } + }() + + for i, tc := range testcases { + configFile, err := os.CreateTemp("", "") + if err != nil { + t.Error(err) + } + + tc.filename = configFile.Name() + + if err := tc.config.SaveToFile(tc.filename); err != nil { + t.Error(err) + } + + tc.config = NebulaClusterConfig{} + + if err := tc.config.LoadFromFile(tc.filename); err != nil { + t.Error(err) + } + if tc.config != tc.expected { + t.Errorf("%d: Expected: \n%#v\n but actual: \n%#v\n", i, tc.expected, tc.config) + } + } +}