Feat: get raw Application yaml, json or jsonpath (#4415)

* Feat: get raw Application

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Fix: add gvk

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Feat: git rid of managedFields

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Style: fix typos

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Test: add test and make changes according to comments

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Feat: more help text

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>

* Style: format imports

Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>
This commit is contained in:
Charlie Chiang
2022-07-22 14:36:50 +08:00
committed by GitHub
parent 68967f7af8
commit 5890b58aea
3 changed files with 214 additions and 11 deletions
+59 -11
View File
@@ -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
}
+65
View File
@@ -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
}
+90
View File
@@ -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")
}