Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand kudo get command #1658

Merged
merged 6 commits into from
Sep 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
nfnt marked this conversation as resolved.
Show resolved Hide resolved
}

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 {
nfnt marked this conversation as resolved.
Show resolved Hide resolved
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