From 5890b58aead00d55a5c44ae17f8d15b3b830bb13 Mon Sep 17 00:00:00 2001 From: Charlie Chiang Date: Fri, 22 Jul 2022 14:36:50 +0800 Subject: [PATCH] Feat: get raw Application yaml, json or jsonpath (#4415) * Feat: get raw Application Signed-off-by: Charlie Chiang * Fix: add gvk Signed-off-by: Charlie Chiang * Feat: git rid of managedFields Signed-off-by: Charlie Chiang * Style: fix typos Signed-off-by: Charlie Chiang * Test: add test and make changes according to comments Signed-off-by: Charlie Chiang * Feat: more help text Signed-off-by: Charlie Chiang * Style: format imports Signed-off-by: Charlie Chiang --- references/cli/status.go | 70 +++++++++++++++++++++++----- references/cli/utils.go | 65 ++++++++++++++++++++++++++ references/cli/utils_test.go | 90 ++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 references/cli/utils_test.go diff --git a/references/cli/status.go b/references/cli/status.go index 1ceb50c7b..a5bdd2c85 100644 --- a/references/cli/status.go +++ b/references/cli/status.go @@ -19,6 +19,7 @@ package cli import ( "context" "fmt" + "io" "os" "strings" "time" @@ -28,6 +29,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/term" + pkgtypes "k8s.io/apimachinery/pkg/types" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" @@ -94,11 +96,22 @@ const ( // NewAppStatusCommand creates `status` command for showing status func NewAppStatusCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command { ctx := context.Background() + var outputFormat string cmd := &cobra.Command{ - Use: "status APP_NAME", - Short: "Show status of an application.", - Long: "Show status of vela application.", - Example: `vela status APP_NAME`, + Use: "status APP_NAME", + Short: "Show status of an application.", + Long: "Show status of vela application.", + Example: ` # Get basic app info + vela status APP_NAME + + # Show detailed info in tree + vela status first-vela-app --tree --detail --detail-format list + + # Get raw Application yaml (without managedFields) + vela status first-vela-app -o yaml + + # Get raw Application status using jsonpath + vela status first-vela-app -o jsonpath='{.status}'`, RunE: func(cmd *cobra.Command, args []string) error { // check args argsLength := len(args) @@ -118,6 +131,9 @@ func NewAppStatusCommand(c common.Args, order string, ioStreams cmdutil.IOStream if err != nil { return err } + if outputFormat != "" { + return printRawApplication(context.Background(), c, outputFormat, cmd.OutOrStdout(), namespace, appName) + } showEndpoints, err := cmd.Flags().GetBool("endpoint") if showEndpoints && err == nil { component, _ := cmd.Flags().GetString("component") @@ -137,8 +153,9 @@ func NewAppStatusCommand(c common.Args, order string, ioStreams cmdutil.IOStream cmd.Flags().BoolP("endpoint", "p", false, "show all service endpoints of the application") cmd.Flags().StringP("component", "c", "", "filter service endpoints by component name") cmd.Flags().BoolP("tree", "t", false, "display the application resources into tree structure") - cmd.Flags().BoolP("detail", "d", false, "display the realtime details of application resources") - cmd.Flags().StringP("detail-format", "", "inline", "the format for displaying details. Can be one of inline (default), wide, list, table, raw.") + cmd.Flags().BoolP("detail", "d", false, "display the realtime details of application resources, must be used with --tree") + cmd.Flags().StringP("detail-format", "", "inline", "the format for displaying details, must be used with --detail. Can be one of inline, wide, list, table, raw.") + cmd.Flags().StringVarP(&outputFormat, "output", "o", "", "raw Application output format. One of: (json, yaml, jsonpath)") addNamespaceAndEnvArg(cmd) return cmd } @@ -224,11 +241,11 @@ func printWorkflowStatus(c client.Client, ioStreams cmdutil.IOStreams, appName s ioStreams.Infof(" Terminated: %t\n", workflowStatus.Terminated) ioStreams.Info(" Steps") for _, step := range workflowStatus.Steps { - ioStreams.Infof(" - id:%s\n", step.ID) - ioStreams.Infof(" name:%s\n", step.Name) - ioStreams.Infof(" type:%s\n", step.Type) - ioStreams.Infof(" phase:%s \n", getWfStepColor(step.Phase).Sprint(step.Phase)) - ioStreams.Infof(" message:%s\n", step.Message) + ioStreams.Infof(" - id: %s\n", step.ID) + ioStreams.Infof(" name: %s\n", step.Name) + ioStreams.Infof(" type: %s\n", step.Type) + ioStreams.Infof(" phase: %s \n", getWfStepColor(step.Phase).Sprint(step.Phase)) + ioStreams.Infof(" message: %s\n", step.Message) } ioStreams.Infof("\n") } @@ -431,3 +448,34 @@ func printApplicationTree(c common.Args, cmd *cobra.Command, appName string, app options.PrintResourceTree(cmd.OutOrStdout(), placements, currentRT, historyRTs) return nil } + +// printRawApplication prints raw Application in yaml/json/jsonpath (without managedFields). +func printRawApplication(ctx context.Context, c common.Args, format string, out io.Writer, ns, appName string) error { + var err error + app := &v1beta1.Application{} + + k8sClient, err := c.GetClient() + if err != nil { + return fmt.Errorf("cannot get k8s client: %w", err) + } + + err = k8sClient.Get(ctx, pkgtypes.NamespacedName{ + Namespace: ns, + Name: appName, + }, app) + if err != nil { + return fmt.Errorf("cannot get application %s in namespace %s: %w", appName, ns, err) + } + + // Set GVK, we need it + // because the object returned from client.Get() has empty GVK + // (since the type info is inherent in the typed object, so GVK is empty) + app.SetGroupVersionKind(v1beta1.ApplicationKindVersionKind) + str, err := formatApplicationString(format, app) + if err != nil { + return err + } + + _, err = out.Write([]byte(str)) + return err +} diff --git a/references/cli/utils.go b/references/cli/utils.go index d2b2e94ba..f9bfc6316 100644 --- a/references/cli/utils.go +++ b/references/cli/utils.go @@ -18,7 +18,9 @@ package cli import ( "bufio" + "bytes" "context" + "encoding/json" "fmt" "io" "log" @@ -30,9 +32,13 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/jsonpath" + "k8s.io/kubectl/pkg/cmd/get" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/utils/common" ) @@ -115,3 +121,62 @@ func (ui *UserInput) read() (string, error) { resultStr := strings.TrimSuffix(line, "\n") return resultStr, err } + +// formatApplicationString formats an Application to string in yaml/json/jsonpath for printing (without managedFields). +// +// format = "yaml" / "json" / "jsonpath={.field}" +func formatApplicationString(format string, app *v1beta1.Application) (string, error) { + var ret string + + if format == "" { + return "", fmt.Errorf("no format provided") + } + + // No, we don't want managedFields, get rid of it. + app.ManagedFields = nil + + switch format { + case "yaml": + b, err := yaml.Marshal(app) + if err != nil { + return "", err + } + ret = string(b) + case "json": + b, err := json.MarshalIndent(app, "", " ") + if err != nil { + return "", err + } + ret = string(b) + default: + // format is not any of json/yaml/jsonpath, not supported + if !strings.HasPrefix(format, "jsonpath") { + return "", fmt.Errorf("output %s is not supported", format) + } + + // format = jsonpath + s := strings.Split(format, "=") + if len(s) < 2 { + return "", fmt.Errorf("jsonpath template format specified but no template given") + } + path, err := get.RelaxedJSONPathExpression(s[1]) + if err != nil { + return "", err + } + + jp := jsonpath.New("").AllowMissingKeys(true) + err = jp.Parse(path) + if err != nil { + return "", err + } + + buf := &bytes.Buffer{} + err = jp.Execute(buf, app) + if err != nil { + return "", err + } + ret = buf.String() + } + + return ret, nil +} diff --git a/references/cli/utils_test.go b/references/cli/utils_test.go new file mode 100644 index 000000000..43b991b86 --- /dev/null +++ b/references/cli/utils_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2022 The KubeVela 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 cli + +import ( + "strings" + "testing" + + "gotest.tools/assert" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" +) + +func TestFormatApplicationString(t *testing.T) { + var ( + str string + err error + ) + + app := &v1beta1.Application{} + app.SetGroupVersionKind(v1beta1.ApplicationKindVersionKind) + // This should not be preset in the formatted string + app.ManagedFields = []v1.ManagedFieldsEntry{ + { + Manager: "", + Operation: "", + APIVersion: "", + Time: nil, + FieldsType: "", + FieldsV1: nil, + Subresource: "", + }, + } + app.SetName("app-name") + + _, err = formatApplicationString("", app) + assert.ErrorContains(t, err, "no format", "no format provided, should error out") + + _, err = formatApplicationString("invalid", app) + assert.ErrorContains(t, err, "not supported", "invalid format provided, should error out") + + str, err = formatApplicationString("yaml", app) + assert.NilError(t, err) + assert.Equal(t, true, strings.Contains(str, `apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + creationTimestamp: null + name: app-name +spec: + components: null +status: {} +`), "formatted yaml is not correct") + + str, err = formatApplicationString("json", app) + assert.NilError(t, err) + assert.Equal(t, true, strings.Contains(str, `{ + "kind": "Application", + "apiVersion": "core.oam.dev/v1beta1", + "metadata": { + "name": "app-name", + "creationTimestamp": null + }, + "spec": { + "components": null + }, + "status": {} +}`), "formatted json is not correct") + + _, err = formatApplicationString("jsonpath", app) + assert.ErrorContains(t, err, "jsonpath template", "no jsonpath template provided, should not pass") + + str, err = formatApplicationString("jsonpath={.apiVersion}", app) + assert.NilError(t, err) + assert.Equal(t, str, "core.oam.dev/v1beta1") +}