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

Add --output parameter to plan status to allow json/yaml output #1634

Merged
merged 5 commits into from
Aug 10, 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
44 changes: 12 additions & 32 deletions pkg/kudoctl/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import (
"errors"
"fmt"
"io"
"strings"

"github.com/spf13/afero"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"

"github.com/kudobuilder/kudo/pkg/kudoctl/clog"
"github.com/kudobuilder/kudo/pkg/kudoctl/cmd/output"
"github.com/kudobuilder/kudo/pkg/kudoctl/kube"
"github.com/kudobuilder/kudo/pkg/kudoctl/kudohome"
"github.com/kudobuilder/kudo/pkg/kudoctl/kudoinit"
Expand Down Expand Up @@ -266,19 +266,18 @@ func (initCmd *initCmd) runUpgrade(installer *setup.Installer) error {
return nil
}

func (initCmd *initCmd) runYamlOutput(installer *setup.Installer) error {
//TODO: implement output=yaml|json (define a type for output to constrain)
//define an Encoder to replace YAMLWriter
if strings.ToLower(initCmd.output) == "yaml" {
manifests, err := installer.AsYamlManifests()
if err != nil {
return err
}
if err := initCmd.YAMLWriter(initCmd.out, manifests); err != nil {
return err
}
func (initCmd *initCmd) runYamlOutput(installer kudoinit.Artifacter) error {
if initCmd.output == "" {
return nil
}
return nil

r := installer.Resources()
res := []interface{}{}
for _, rr := range r {
res = append(res, rr)
}

return output.WriteObjects(res, initCmd.output, initCmd.out)
}

// verifyExistingInstallation checks if the current installation is valid and as expected
Expand Down Expand Up @@ -325,25 +324,6 @@ func (initCmd *initCmd) preUpgradeVerify(v kudoinit.InstallVerifier) (bool, erro
return true, nil
}

// YAMLWriter writes yaml to writer. Looked into using https://godoc.org/gopkg.in/yaml.v2#NewEncoder which
// looks like a better way, however the omitted JSON elements are encoded which results in a very verbose output.
//TODO: Write a Encoder util which uses the "sigs.k8s.io/yaml" library for marshalling
func (initCmd *initCmd) YAMLWriter(w io.Writer, manifests []string) error {
for _, manifest := range manifests {
if _, err := fmt.Fprintln(w, "---"); err != nil {
return err
}

if _, err := fmt.Fprintln(w, manifest); err != nil {
return err
}
}

// YAML ending document boundary marker
_, err := fmt.Fprintln(w, "...")
return err
}

func (initCmd *initCmd) ensureClient() error {

if err := ensureDirectories(initCmd.fs, initCmd.home); err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/kudoctl/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ func TestInitCmd_yamlOutput(t *testing.T) {
{"custom namespace", "deploy-kudo-ns.yaml", map[string]string{"dry-run": "true", "output": "yaml", "namespace": "foo", "version": "dev"}},
{"yaml output", "deploy-kudo.yaml", map[string]string{"dry-run": "true", "output": "yaml", "version": "dev"}},
{"service account", "deploy-kudo-sa.yaml", map[string]string{"dry-run": "true", "output": "yaml", "service-account": "safoo", "namespace": "foo", "version": "dev"}},
{"json output", "deploy-kudo.json", map[string]string{"dry-run": "true", "output": "json", "version": "dev"}},
}

for _, tt := range tests {
Expand Down
66 changes: 66 additions & 0 deletions pkg/kudoctl/cmd/output/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package output

import (
"encoding/json"
"fmt"
"io"
"strings"

"sigs.k8s.io/yaml"
)

const (
TypeYAML = "yaml"
TypeJSON = "json"
)

func WriteObjects(objs []interface{}, outputType string, out io.Writer) error {
zen-dog marked this conversation as resolved.
Show resolved Hide resolved
if strings.ToLower(outputType) == TypeYAML {
// Write YAML objects with separators
for _, obj := range objs {
if _, err := fmt.Fprintln(out, "---"); err != nil {
return err
}

if err := writeObjectYAML(obj, out); err != nil {
return err
}
}

// YAML ending document boundary marker
_, err := fmt.Fprintln(out, "...")
return err
} else if strings.ToLower(outputType) == TypeJSON {
return writeObjectJSON(objs, out)
}

return fmt.Errorf("invalid output format, only support yaml or json")
}

func WriteObject(obj interface{}, outputType string, out io.Writer) error {
if strings.ToLower(outputType) == TypeYAML {
return writeObjectYAML(obj, out)
} else if strings.ToLower(outputType) == TypeJSON {
return writeObjectJSON(obj, out)
}

return fmt.Errorf("invalid output format, only support yaml or json")
}

func writeObjectJSON(obj interface{}, out io.Writer) error {
o, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal to json: %v", err)
}
_, err = fmt.Fprintln(out, string(o))
return err
}

func writeObjectYAML(obj interface{}, out io.Writer) error {
o, err := yaml.Marshal(obj)
if err != nil {
return fmt.Errorf("failed to marshal to yaml: %v", err)
}
_, err = fmt.Fprintln(out, string(o))
return err
}
1 change: 1 addition & 0 deletions pkg/kudoctl/cmd/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func NewPlanStatusCmd(out io.Writer) *cobra.Command {

cmd.Flags().StringVar(&options.Instance, "instance", "", "The instance name available from 'kubectl get instances'")
cmd.Flags().BoolVar(&options.Wait, "wait", false, "Specify if the CLI should wait for the plan to complete before returning (default \"false\")")
cmd.Flags().StringVarP(&options.Output, "output", "o", "", "Output format")

if err := cmd.MarkFlagRequired("instance"); err != nil {
clog.Printf("Please choose the instance with '--instance=<instanceName>': %v", err)
Expand Down
1 change: 1 addition & 0 deletions pkg/kudoctl/cmd/plan/plan_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Options struct {
Out io.Writer
Instance string
Wait bool
Output string
}

var (
Expand Down
16 changes: 16 additions & 0 deletions pkg/kudoctl/cmd/plan/plan_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/xlab/treeprint"

"github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1"
"github.com/kudobuilder/kudo/pkg/kudoctl/cmd/output"
"github.com/kudobuilder/kudo/pkg/kudoctl/env"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo"
)
Expand All @@ -25,8 +26,23 @@ func Status(options *Options, settings *env.Settings) error {
return status(kc, options, settings.Namespace)
}

func statusFormatted(kc *kudo.Client, options *Options, ns string) error {
instance, err := kc.GetInstance(options.Instance, ns)
if err != nil {
return err
}
if instance == nil {
return fmt.Errorf("instance %s/%s does not exist", ns, options.Instance)
}
return output.WriteObject(instance.Status, options.Output, options.Out)
}

func status(kc *kudo.Client, options *Options, ns string) error {

if options.Output != "" {
return statusFormatted(kc, options, ns)
}

firstPass := true
start := time.Now()

Expand Down
87 changes: 53 additions & 34 deletions pkg/kudoctl/cmd/plan/plan_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package plan

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

Expand All @@ -16,7 +19,8 @@ import (
)

var (
testTime = time.Date(2019, 10, 17, 1, 1, 1, 1, time.UTC)
testTime = time.Date(2019, 10, 17, 1, 1, 1, 1, time.UTC)
updateGolden = flag.Bool("update", false, "update .golden files")
)

func TestStatus(t *testing.T) {
Expand Down Expand Up @@ -104,45 +108,60 @@ func TestStatus(t *testing.T) {
instanceNameArg string
errorMessage string
expectedOutput string
output string
goldenFile string
}{
{"nonexisting instance", nil, nil, "nonexisting", "Instance default/nonexisting does not exist", ""},
{"nonexisting ov", instance, nil, "test", "OperatorVersion test-1.0 from instance default/test does not exist", ""},
{"no plan run", instance, ov, "test", "", "No plan ever run for instance - nothing to show for instance test\n"},
{"fatal error in a plan", fatalErrInstance, ov, "test", "", `Plan(s) for "test" in namespace "default":
.
└── test (Operator-Version: "test-1.0" Active-Plan: "deploy")
├── Plan deploy ( strategy) [FATAL_ERROR], last updated 2019-10-17 01:01:01
│   └── Phase deploy ( strategy) [FATAL_ERROR]
│   └── Step deploy [FATAL_ERROR] (error detail)
├── Plan validate ( strategy) [NOT ACTIVE]
│   └── Phase validate ( strategy) [NOT ACTIVE]
│   └── Step validate [NOT ACTIVE]
└── Plan zzzinvalid ( strategy) [NOT ACTIVE]
└── Phase zzzinvalid ( strategy) [NOT ACTIVE]
└── Step zzzinvalid [NOT ACTIVE]

`},
{name: "nonexisting instance", instanceNameArg: "nonexisting", errorMessage: "Instance default/nonexisting does not exist"},
{name: "nonexisting ov", instance: instance, instanceNameArg: "test", errorMessage: "OperatorVersion test-1.0 from instance default/test does not exist"},
{name: "no plan run", instance: instance, ov: ov, instanceNameArg: "test", expectedOutput: "No plan ever run for instance - nothing to show for instance test\n"},
{name: "text output", instance: fatalErrInstance, ov: ov, instanceNameArg: "test", goldenFile: "planstatus.txt"},
{name: "json output", instance: fatalErrInstance, ov: ov, instanceNameArg: "test", output: "json", goldenFile: "planstatus.json"},
{name: "yaml output", instance: fatalErrInstance, ov: ov, instanceNameArg: "test", output: "yaml", goldenFile: "planstatus.yaml"},
}

for _, tt := range tests {
var buf bytes.Buffer
kc := kudo.NewClientFromK8s(fake.NewSimpleClientset(), kubefake.NewSimpleClientset())
if tt.instance != nil {
_, err := kc.InstallInstanceObjToCluster(tt.instance, "default")
if err != nil {
t.Errorf("%s: error when setting up a test - %v", tt.name, err)
tt := tt
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
kc := kudo.NewClientFromK8s(fake.NewSimpleClientset(), kubefake.NewSimpleClientset())
if tt.instance != nil {
_, err := kc.InstallInstanceObjToCluster(tt.instance, "default")
if err != nil {
t.Errorf("%s: error when setting up a test - %v", tt.name, err)
}
}
if tt.ov != nil {
_, err := kc.InstallOperatorVersionObjToCluster(tt.ov, "default")
if err != nil {
t.Errorf("%s: error when setting up a test - %v", tt.name, err)
}
}
}
if tt.ov != nil {
_, err := kc.InstallOperatorVersionObjToCluster(tt.ov, "default")
err := status(kc, &Options{Out: &buf, Instance: tt.instanceNameArg, Output: tt.output}, "default")
if err != nil {
t.Errorf("%s: error when setting up a test - %v", tt.name, err)
assert.Equal(t, err.Error(), tt.errorMessage)
}
if tt.goldenFile != "" {
gp := filepath.Join("testdata", tt.goldenFile+".golden")

if *updateGolden {
t.Logf("updating golden file %s", tt.goldenFile)

//nolint:gosec
if err := ioutil.WriteFile(gp, buf.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), buf.String(), "for golden file: %s, for test %s", gp, tt.name)
}
if tt.expectedOutput != "" {
assert.Equal(t, buf.String(), tt.expectedOutput)
}
}
err := status(kc, &Options{Out: &buf, Instance: tt.instanceNameArg}, "default")
if err != nil {
assert.Equal(t, err.Error(), tt.errorMessage)
}
assert.Equal(t, buf.String(), tt.expectedOutput)
})
}
}
22 changes: 22 additions & 0 deletions pkg/kudoctl/cmd/plan/testdata/planstatus.json.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"planStatus": {
"deploy": {
"name": "deploy",
"status": "FATAL_ERROR",
"lastUpdatedTimestamp": "2019-10-17T01:01:01Z",
"phases": [
{
"name": "deploy",
"status": "FATAL_ERROR",
"steps": [
{
"name": "deploy",
"message": "error detail",
"status": "FATAL_ERROR"
}
]
}
]
}
}
}
13 changes: 13 additions & 0 deletions pkg/kudoctl/cmd/plan/testdata/planstatus.txt.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Plan(s) for "test" in namespace "default":
.
└── test (Operator-Version: "test-1.0" Active-Plan: "deploy")
├── Plan deploy ( strategy) [FATAL_ERROR], last updated 2019-10-17 01:01:01
│   └── Phase deploy ( strategy) [FATAL_ERROR]
│   └── Step deploy [FATAL_ERROR] (error detail)
├── Plan validate ( strategy) [NOT ACTIVE]
│   └── Phase validate ( strategy) [NOT ACTIVE]
│   └── Step validate [NOT ACTIVE]
└── Plan zzzinvalid ( strategy) [NOT ACTIVE]
└── Phase zzzinvalid ( strategy) [NOT ACTIVE]
└── Step zzzinvalid [NOT ACTIVE]

13 changes: 13 additions & 0 deletions pkg/kudoctl/cmd/plan/testdata/planstatus.yaml.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
planStatus:
deploy:
lastUpdatedTimestamp: "2019-10-17T01:01:01Z"
name: deploy
phases:
- name: deploy
status: FATAL_ERROR
steps:
- message: error detail
name: deploy
status: FATAL_ERROR
status: FATAL_ERROR

Loading