Skip to content

Commit

Permalink
Expand kudo get command (#1658)
Browse files Browse the repository at this point in the history
Extends the kudo get command. It now supports
* Get "instances", "operators", "operatorversions" and "all".
* --output yaml or --output json

Signed-off-by: Andreas Neumann <aneumann@mesosphere.com>
  • Loading branch information
ANeumann82 authored Sep 3, 2020
1 parent ae5b2e9 commit ecaf66e
Show file tree
Hide file tree
Showing 18 changed files with 513 additions and 54 deletions.
20 changes: 18 additions & 2 deletions pkg/kudoctl/cmd/get.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
package cmd

import (
"io"

"github.com/spf13/cobra"

"github.com/kudobuilder/kudo/pkg/kudoctl/cmd/get"
"github.com/kudobuilder/kudo/pkg/kudoctl/env"
)

const getExample = ` # Get all available instances
kubectl kudo get instances
`

// newGetCmd creates a command that lists the instances in the cluster
func newGetCmd() *cobra.Command {
func newGetCmd(out io.Writer) *cobra.Command {
opts := get.CmdOpts{
Out: out,
}

getCmd := &cobra.Command{
Use: "get instances",
Short: "Gets all available instances.",
Example: getExample,
RunE: func(cmd *cobra.Command, args []string) error {
return get.Run(args, &Settings)
client, err := env.GetClient(&Settings)
if err != nil {
return err
}
opts.Client = client
opts.Namespace = Settings.Namespace

return get.Run(args, opts)
},
}

getCmd.Flags().StringVarP(opts.Output.AsStringPtr(), "output", "o", "", "Output format for command results.")

return getCmd
}
138 changes: 115 additions & 23 deletions pkg/kudoctl/cmd/get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,152 @@ package get
import (
"errors"
"fmt"
"log"
"io"

"github.com/xlab/treeprint"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"

"github.com/kudobuilder/kudo/pkg/kudoctl/env"
"github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1"
"github.com/kudobuilder/kudo/pkg/kudoctl/cmd/output"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo"
)

type CmdOpts struct {
Out io.Writer
Client *kudo.Client

Output output.Type
Namespace string
}

const (
All = "all"

Instances = "instances"
Operators = "operators"
OperatorVersions = "operatorversions"
)

// Run returns the errors associated with cmd env
func Run(args []string, settings *env.Settings) error {
func Run(args []string, opts CmdOpts) error {
if err := opts.Output.Validate(); err != nil {
return err
}

err := validate(args)
if err != nil {
return err
}

kc, err := env.GetClient(settings)
var objs []runtime.Object
switch args[0] {
case Instances:
objs, err = opts.Client.ListInstances(opts.Namespace)
case Operators:
objs, err = opts.Client.ListOperators(opts.Namespace)
case OperatorVersions:
objs, err = opts.Client.ListOperatorVersions(opts.Namespace)
case All:
return runGetAll(opts)
}
if err != nil {
return fmt.Errorf("creating kudo client: %w", err)
return fmt.Errorf("failed to retrieve objects: %v", err)
}

p, err := getInstances(kc, settings)
if err != nil {
log.Printf("Error: %v", err)
if opts.Output.IsFormattedOutput() {
outObj := []interface{}{}
for _, o := range objs {
outObj = append(outObj, o)
}
return output.WriteObjects(outObj, opts.Output, opts.Out)
}

tree := treeprint.New()

for _, plan := range p {
tree.AddBranch(plan)
metadataAccessor := meta.NewAccessor()
for _, obj := range objs {
name, err := metadataAccessor.Name(obj)
if err != nil {
return fmt.Errorf("failed to retrieve name from %v: %v", obj, err)
}
tree.AddBranch(name)
}
fmt.Printf("List of current installed instances in namespace \"%s\":\n", settings.Namespace)
fmt.Println(tree.String())
_, _ = fmt.Fprintf(opts.Out, "List of current installed %s in namespace %q:\n", args[0], opts.Namespace)
_, _ = fmt.Fprintln(opts.Out, tree.String())
return err
}

func validate(args []string) error {
if len(args) != 1 {
return errors.New(`expecting exactly one argument - "instances"`)
func runGetAll(opts CmdOpts) error {
instances, err := opts.Client.ListInstances(opts.Namespace)
if err != nil {
return fmt.Errorf("failed to get instances")
}
operatorversions, err := opts.Client.ListOperatorVersions(opts.Namespace)
if err != nil {
return fmt.Errorf("failed to get operatorversions")
}
operators, err := opts.Client.ListOperators(opts.Namespace)
if err != nil {
return fmt.Errorf("failed to get operators")
}

if args[0] != "instances" {
return fmt.Errorf(`expecting "instances" and not %q`, args[0])
if opts.Output.IsFormattedOutput() {
outObj := []interface{}{}
for _, o := range operators {
outObj = append(outObj, o)
}
for _, o := range operatorversions {
outObj = append(outObj, o)
}
for _, o := range instances {
outObj = append(outObj, o)
}
return output.WriteObjects(outObj, opts.Output, opts.Out)
}

return printAllTree(opts, operators, operatorversions, instances)
}

func printAllTree(opts CmdOpts, operators, operatorversions, instances []runtime.Object) error {

rootTree := treeprint.New()
for _, o := range operators {
op, _ := o.(*v1beta1.Operator)
opTree := rootTree.AddBranch(op.Name)

for _, ovo := range operatorversions {
ov, _ := ovo.(*v1beta1.OperatorVersion)
if ov.Spec.Operator.Name == op.Name {
ovTree := opTree.AddBranch(ov.Name)

for _, io := range instances {
i, _ := io.(*v1beta1.Instance)
if i.Spec.OperatorVersion.Name == ov.Name {
ovTree.AddBranch(i.Name)
}
}
}
}
}

_, _ = fmt.Fprintf(opts.Out, "List of current installed operators including versions and instances in namespace %q:\n", opts.Namespace)
_, _ = fmt.Fprintln(opts.Out, rootTree.String())
return nil

}

func getInstances(kc *kudo.Client, settings *env.Settings) ([]string, error) {

instanceList, err := kc.ListInstances(settings.Namespace)
if err != nil {
return nil, fmt.Errorf("getting instances: %w", err)
func validate(args []string) error {
if len(args) != 1 {
return errors.New(`expecting exactly one argument - "instances, operators, operatorversions or all"`)
}

return instanceList, nil
switch args[0] {
case Instances, Operators, OperatorVersions:
fallthrough
case All:
return nil
default:
return fmt.Errorf(`expecting one of "instances, operators, operatorversions or all" and not %q`, args[0])
}
}
128 changes: 112 additions & 16 deletions pkg/kudoctl/cmd/get/get_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package get

import (
"bytes"
"flag"
"io/ioutil"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -10,19 +14,21 @@ import (

"github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1"
"github.com/kudobuilder/kudo/pkg/client/clientset/versioned/fake"
"github.com/kudobuilder/kudo/pkg/kudoctl/env"
"github.com/kudobuilder/kudo/pkg/kudoctl/cmd/output"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo"
)

var updateGolden = flag.Bool("update", false, "update .golden files")

func TestValidate(t *testing.T) {
tests := []struct {
arg []string
err string
}{
{nil, "expecting exactly one argument - \"instances\""}, // 1
{[]string{"arg", "arg2"}, "expecting exactly one argument - \"instances\""}, // 2
{[]string{}, "expecting exactly one argument - \"instances\""}, // 3
{[]string{"somethingelse"}, "expecting \"instances\" and not \"somethingelse\""}, // 4
{nil, "expecting exactly one argument - \"instances, operators, operatorversions or all\""}, // 1
{[]string{"arg", "arg2"}, "expecting exactly one argument - \"instances, operators, operatorversions or all\""}, // 2
{[]string{}, "expecting exactly one argument - \"instances, operators, operatorversions or all\""}, // 3
{[]string{"somethingelse"}, "expecting one of \"instances, operators, operatorversions or all\" and not \"somethingelse\""}, // 4
}

for _, tt := range tests {
Expand All @@ -45,27 +51,117 @@ func TestGetInstances(t *testing.T) {
Labels: map[string]string{
"operator": "test",
},
Name: "test",
Name: "test",
Namespace: "default",
},
Spec: v1beta1.InstanceSpec{
OperatorVersion: v1.ObjectReference{
Name: "test-1.0",
Name: "some-operator-0.1.0",
},
},
}

testOperator := &v1beta1.Operator{
TypeMeta: metav1.TypeMeta{
APIVersion: "kudo.dev/v1beta1",
Kind: "Operator",
},
ObjectMeta: metav1.ObjectMeta{
Name: "some-operator",
Namespace: "default",
},
Spec: v1beta1.OperatorSpec{
Description: "A fancy Operator",
KudoVersion: "0.16.0",
},
}

testOperatorVersion := &v1beta1.OperatorVersion{
TypeMeta: metav1.TypeMeta{
APIVersion: "kudo.dev/v1beta1",
Kind: "OperatorVersion",
},
ObjectMeta: metav1.ObjectMeta{
Name: "some-operator-0.1.0",
Namespace: "default",
},
Spec: v1beta1.OperatorVersionSpec{
Operator: v1.ObjectReference{
APIVersion: "kudo.dev/v1beta1",
Kind: "Operator",
Name: "some-operator",
},
Version: "0.1.0",
},
}

kc := newTestClient()
if _, err := kc.InstallInstanceObjToCluster(testInstance, "default"); err != nil {
t.Fatal(err)
}
if _, err := kc.InstallOperatorObjToCluster(testOperator, "default"); err != nil {
t.Fatal(err)
}
if _, err := kc.InstallOperatorVersionObjToCluster(testOperatorVersion, "default"); err != nil {
t.Fatal(err)
}

tests := []struct {
instances []string
name string
arg string
goldenFile string
output output.Type
expectedError string
}{
{[]string{"test"}},
{name: "human readable instances", arg: "instances", goldenFile: "get-instances.txt", output: ""},
{name: "yaml instances", arg: "instances", goldenFile: "get-instances.yaml", output: output.TypeYAML},
{name: "json instances", arg: "instances", goldenFile: "get-instances.json", output: output.TypeJSON},
{name: "human readable operators", arg: "operators", goldenFile: "get-operators.txt", output: ""},
{name: "yaml operators", arg: "operators", goldenFile: "get-operators.yaml", output: output.TypeYAML},
{name: "json operators", arg: "operators", goldenFile: "get-operators.json", output: output.TypeJSON},
{name: "human readable operatorversions", arg: "operatorversions", goldenFile: "get-operatorversions.txt", output: ""},
{name: "yaml operatorversions", arg: "operatorversions", goldenFile: "get-operatorversions.yaml", output: output.TypeYAML},
{name: "json operatorversions", arg: "operatorversions", goldenFile: "get-operatorversions.json", output: output.TypeJSON},
{name: "human readable all", arg: "all", goldenFile: "get-all.txt", output: ""},
{name: "yaml all", arg: "all", goldenFile: "get-all.yaml", output: output.TypeYAML},
{name: "json all", arg: "all", goldenFile: "get-all.json", output: output.TypeJSON},

{name: "invalid output", arg: "instances", expectedError: output.InvalidOutputError, output: "invalid"},
}

for _, tt := range tests {
kc := newTestClient()
if _, err := kc.InstallInstanceObjToCluster(testInstance, "default"); err != nil {
t.Fatal(err)
}
instanceList, err := getInstances(kc, env.DefaultSettings)
assert.NoError(t, err)
assert.EqualValues(t, tt.instances, instanceList, "missing instances")
tt := tt

t.Run(tt.name, func(t *testing.T) {
out := &bytes.Buffer{}
cmd := CmdOpts{Out: out, Output: tt.output, Namespace: "default", Client: kc}

if err := Run([]string{tt.arg}, cmd); err != nil {
if tt.expectedError != "" {
assert.Equal(t, tt.expectedError, err.Error())
} else {
t.Fatalf("unexpected error: %v", err)
}
}

if tt.goldenFile != "" {
gp := filepath.Join("testdata", tt.goldenFile+".golden")

if *updateGolden {
t.Log("update golden file")

//nolint:gosec
if err := ioutil.WriteFile(gp, out.Bytes(), 0644); err != nil {
t.Fatalf("failed to update golden file: %s", err)
}
}
g, err := ioutil.ReadFile(gp)
if err != nil {
t.Fatalf("failed reading .golden: %s", err)
}

assert.Equal(t, string(g), out.String(), "output does not match .golden file %s", gp)
}
})
}
}
Loading

0 comments on commit ecaf66e

Please sign in to comment.