diff --git a/e2e/application/application_test.go b/e2e/application/application_test.go index e0ebd2248..e5352f566 100644 --- a/e2e/application/application_test.go +++ b/e2e/application/application_test.go @@ -24,6 +24,7 @@ var _ = ginkgo.Describe("Application", func() { e2e.TraitManualScalerAttachContext("vela attach trait", traitAlias, applicationName) //e2e.ApplicationListContext("app ls", applicationName, traitAlias) e2e.ApplicationShowContext("app show", applicationName, workloadType) - e2e.ApplicationStatusContext("comp status", applicationName, workloadType) + e2e.ApplicationStatusContext("app status", applicationName, workloadType) + e2e.ApplicationCompStatusContext("comp status", applicationName, workloadType) e2e.WorkloadDeleteContext("delete", applicationName) }) diff --git a/e2e/commonContext.go b/e2e/commonContext.go index a5c76b921..0ac7d09ee 100644 --- a/e2e/commonContext.go +++ b/e2e/commonContext.go @@ -177,6 +177,18 @@ var ( ApplicationStatusContext = func(context string, applicationName string, workloadType string) bool { return ginkgo.Context(context, func() { ginkgo.It("should get status for the application", func() { + cli := fmt.Sprintf("vela app status %s", applicationName) + output, err := Exec(cli) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(output).To(gomega.ContainSubstring(applicationName)) + // TODO(roywang) add more assertion to check health status + }) + }) + } + + ApplicationCompStatusContext = func(context string, applicationName string, workloadType string) bool { + return ginkgo.Context(context, func() { + ginkgo.It("should get status for the component", func() { cli := fmt.Sprintf("vela comp status %s", applicationName) output, err := Exec(cli) gomega.Expect(err).NotTo(gomega.HaveOccurred()) diff --git a/go.mod b/go.mod index 3ff0a01f5..54c45dbc2 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,8 @@ require ( github.com/Azure/go-autorest v12.2.0+incompatible // Don't remove. https://github.com/kubernetes/client-go/issues/628 github.com/coreos/prometheus-operator v0.41.1 github.com/crossplane/crossplane-runtime v0.9.0 - github.com/crossplane/oam-kubernetes-runtime v0.0.9 + github.com/crossplane/oam-kubernetes-runtime v0.1.1-0.20200909070723-78b84f2c4799 + github.com/fatih/color v1.9.0 github.com/gertd/go-pluralize v0.1.7 github.com/ghodss/yaml v1.0.0 github.com/gin-gonic/gin v1.6.3 @@ -15,11 +16,11 @@ require ( github.com/google/go-cmp v0.5.2 github.com/google/go-github/v32 v32.1.0 github.com/gosuri/uitable v0.0.4 - github.com/oam-dev/catalog/traits/metricstrait v0.0.0-20200826071236-d96c1d64e221 github.com/oam-dev/trait-injector v0.0.0-20200331033130-0a27b176ffc4 github.com/onsi/ginkgo v1.11.0 github.com/onsi/gomega v1.8.1 github.com/pkg/errors v0.9.1 + github.com/rs/xid v1.2.1 // indirect github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 @@ -36,6 +37,7 @@ require ( k8s.io/cli-runtime v0.18.6 k8s.io/client-go v12.0.0+incompatible k8s.io/klog v1.0.0 + k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29 // indirect k8s.io/kubectl v0.18.6 // indirect k8s.io/utils v0.0.0-20200414100711-2df71ebbae66 rsc.io/letsencrypt v0.0.3 // indirect diff --git a/go.sum b/go.sum index 54e10d354..a5501a911 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,7 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v10.8.1+incompatible h1:u0jVQf+a6k6x8A+sT60l6EY9XZu+kHdnZVPAYqpVRo0= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v11.2.8+incompatible h1:Q2feRPMlcfVcqz3pF87PJzkm5lZrL+x6BDtzhODzNJM= github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v12.2.0+incompatible h1:2Fxszbg492oAJrcvJlgyVaTqnQYRkxmEK6VPCLLVpBI= github.com/Azure/go-autorest v12.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= @@ -237,7 +238,6 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/prometheus-operator v0.40.0/go.mod h1:QOoL5cVI3b1OHgpw8s+pH+Ok4AFRp2HOUvBpqs7UWcg= github.com/coreos/prometheus-operator v0.41.1 h1:MEhY9syliPlQg+VlFRUfNodUEVXRXJ2n1pFG0aBp+mI= github.com/coreos/prometheus-operator v0.41.1/go.mod h1:LhLfEBydppl7nvfEA1jIqlF3xJ9myHCnzrU+HHDxRd4= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= @@ -251,9 +251,10 @@ github.com/crossplane/crossplane-runtime v0.8.0/go.mod h1:gNY/21MLBaz5KNP7hmfXbB github.com/crossplane/crossplane-runtime v0.9.0 h1:K6/tLhXKzhsEUUddTvEWWnQLLrawWyw1ptNK7NBDpDU= github.com/crossplane/crossplane-runtime v0.9.0/go.mod h1:gNY/21MLBaz5KNP7hmfXbBXp8reYRbwY5B/97Kp4tgM= github.com/crossplane/crossplane-tools v0.0.0-20200219001116-bb8b2ce46330/go.mod h1:C735A9X0x0lR8iGVOOxb49Mt70Ua4EM2b7PGaRPBLd4= -github.com/crossplane/oam-kubernetes-runtime v0.0.7/go.mod h1:f5xqmo0B2WtaOTZh8jhP+0f0XuzqhJG2xRtxfMZR3jA= github.com/crossplane/oam-kubernetes-runtime v0.0.9 h1:cZMT7p1jZ6MsJqAuzVIZwvOxVdD+PGEQgCYHHuwR7Pc= github.com/crossplane/oam-kubernetes-runtime v0.0.9/go.mod h1:f5xqmo0B2WtaOTZh8jhP+0f0XuzqhJG2xRtxfMZR3jA= +github.com/crossplane/oam-kubernetes-runtime v0.1.1-0.20200909070723-78b84f2c4799 h1:424LLFb7C8Qvy3wFZZ7HzmawlCeF32PNRTXXK5rKOk0= +github.com/crossplane/oam-kubernetes-runtime v0.1.1-0.20200909070723-78b84f2c4799/go.mod h1:UZ4eXkl/e4lKrAhK81Pz1sR90wqeuE9PgdwVXr8kDgI= github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= @@ -874,7 +875,6 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/oam-dev/catalog/traits/metricstrait v0.0.0-20200826071236-d96c1d64e221/go.mod h1:mpYuuccw79F/yddIODjdDCzUyvXNnuksPWVx/z5vZBw= github.com/oam-dev/stern v1.13.0-alpha h1:EVjM8Qvh6LssB6t4RZrjf9DtCq1cz+/cy6OF7fpy9wk= github.com/oam-dev/stern v1.13.0-alpha/go.mod h1:AOkvfFUv0Arz7GBi0jz7S0Jsu4K/kdvSjNsnRt1+BIg= github.com/oam-dev/trait-injector v0.0.0-20200331033130-0a27b176ffc4 h1:fc41JCTc9w140pE/WPtpmL3uiCjC3DEfzbtvHT6h6xY= diff --git a/pkg/cmd/app.go b/pkg/cmd/app.go index aa6fc8744..126b58b3f 100644 --- a/pkg/cmd/app.go +++ b/pkg/cmd/app.go @@ -19,6 +19,7 @@ func NewAppsCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { } cmd.AddCommand(NewAppListCommand(c, ioStreams), + NewAppStatusCommand(c, ioStreams), NewDeleteCommand(c, ioStreams), NewAppShowCommand(ioStreams), NewRunCommand(c, ioStreams)) diff --git a/pkg/cmd/delete.go b/pkg/cmd/delete.go index e3f0c81c3..8fd61b96a 100644 --- a/pkg/cmd/delete.go +++ b/pkg/cmd/delete.go @@ -89,9 +89,9 @@ func NewCompDeleteCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Comm if err != nil { return err } - if appName != ""{ + if appName != "" { o.AppName = appName - }else { + } else { o.AppName = o.CompName } diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index a5891a539..25d3bdbf2 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -2,10 +2,14 @@ package cmd import ( "context" + "fmt" "os" + "strings" "time" "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" + "github.com/fatih/color" + "github.com/gosuri/uitable" "github.com/cloud-native-application/rudrx/pkg/application" @@ -16,12 +20,50 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func NewCompStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { +// HealthStatus represents health status strings. +type HealthStatus = v1alpha2.HealthStatus + +const ( + // StatusNotFound means there's no health check info returned from the scope. + StatusNotFound HealthStatus = "NOT DIAGNOSED" +) + +const ( + // StatusHealthy represents healthy status. + StatusHealthy = v1alpha2.StatusHealthy + // StatusUnhealthy represents unhealthy status. + StatusUnhealthy = v1alpha2.StatusUnhealthy + // StatusUnknown represents unknown status. + StatusUnknown = v1alpha2.StatusUnknown +) + +// WorkloadHealthCondition holds health status of any resource +type WorkloadHealthCondition = v1alpha2.WorkloadHealthCondition + +// ScopeHealthCondition holds health condition of a scope +type ScopeHealthCondition = v1alpha2.ScopeHealthCondition + +const ( + firstElemPrefix = `├─` + lastElemPrefix = `└─` + indent = " " + pipe = `│ ` +) + +var ( + gray = color.New(color.FgHiBlack) + red = color.New(color.FgRed) + green = color.New(color.FgGreen) + yellow = color.New(color.FgYellow) + white = color.New(color.Bold, color.FgWhite) +) + +func NewAppStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { ctx := context.Background() cmd := &cobra.Command{ Use: "status ", Short: "get status of an application", - Long: "get status of an application, including its workload and trait", + Long: "get status of an application, including workloads and traits of each components.", Example: `vela status `, RunE: func(cmd *cobra.Command, args []string) error { argsLength := len(args) @@ -29,6 +71,119 @@ func NewCompStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Comm ioStreams.Errorf("Hint: please specify an application") os.Exit(1) } + appName := args[0] + env, err := GetEnv(cmd) + if err != nil { + ioStreams.Errorf("Error: failed to get Env: %s", err) + return err + } + newClient, err := client.New(c.Config, client.Options{Scheme: c.Schema}) + if err != nil { + return err + } + return printAppStatus(ctx, newClient, ioStreams, appName, env) + }, + Annotations: map[string]string{ + types.TagCommandType: types.TypeApp, + }, + } + cmd.SetOut(ioStreams.Out) + return cmd +} + +func printAppStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOStreams, appName string, env *types.EnvMeta) error { + app, err := application.Load(env.Name, appName) + if err != nil { + return err + } + namespace := env.Name + tbl := uitable.New() + tbl.Separator = " " + tbl.AddRow( + white.Sprint("NAMESPCAE"), + white.Sprint("NAME"), + white.Sprint("INFO")) + + tbl.AddRow( + namespace, + fmt.Sprintf("%s/%s", + "Application", + appName)) + + components := app.GetComponents() + // get a map coantaining all workloads health condition + wlConditionsMap, err := getWorkloadHealthConditions(ctx, c, app, namespace) + if err != nil { + return err + } + + for cIndex, compName := range components { + var cPrefix string + switch cIndex { + case len(components) - 1: + cPrefix = lastElemPrefix + default: + cPrefix = firstElemPrefix + } + + wlHealthCondition := wlConditionsMap[compName] + wlHealthStatus := wlHealthCondition.HealthStatus + healthColor := getHealthStatusColor(wlHealthStatus) + + // print component info + tbl.AddRow("", + fmt.Sprintf("%s%s/%s", + gray.Sprint(printPrefix(cPrefix)), + "Component", + compName), + healthColor.Sprintf("%s %s", wlHealthStatus, wlHealthCondition.Diagnosis)) + } + ioStreams.Info(tbl) + return nil +} + +// map componentName <=> WorkloadHealthCondition +func getWorkloadHealthConditions(ctx context.Context, c client.Client, app *application.Application, ns string) (map[string]*WorkloadHealthCondition, error) { + + hs := &v1alpha2.HealthScope{} + // only use default health scope + hsName := application.FormatDefaultHealthScopeName(app.Name) + if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: hsName}, hs); err != nil { + return nil, err + } + wlConditions := hs.Status.WorkloadHealthConditions + r := map[string]*WorkloadHealthCondition{} + components := app.GetComponents() + for _, compName := range components { + for _, wlhc := range wlConditions { + if wlhc.ComponentName == compName { + r[compName] = wlhc + break + } + } + if r[compName] == nil { + r[compName] = &WorkloadHealthCondition{ + HealthStatus: StatusNotFound, + } + } + } + + return r, nil +} + +func NewCompStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { + ctx := context.Background() + cmd := &cobra.Command{ + Use: "status ", + Short: "get status of a component", + Long: "get status of a component, including its workload and health status", + Example: `vela comp status `, + RunE: func(cmd *cobra.Command, args []string) error { + argsLength := len(args) + if argsLength == 0 { + ioStreams.Errorf("Hint: please specify a component") + os.Exit(1) + } compName := args[0] env, err := GetEnv(cmd) if err != nil { @@ -67,16 +222,103 @@ func printComponentStatus(ctx context.Context, c client.Client, ioStreams cmduti if err = c.Get(ctx, client.ObjectKey{Namespace: env.Namespace, Name: application.FormatDefaultHealthScopeName(app.Name)}, &health); err != nil { return err } - ioStreams.Info("Component Status:") - //TODO(wonderflow): add more information from health scope - ioStreams.Infof("\n %s \n\n", health.Status.Health) + + ioStreams.Infof(white.Sprint("Component Status:\n")) + var wlhc *v1alpha2.WorkloadHealthCondition + for _, v := range health.Status.WorkloadHealthConditions { + if v.ComponentName == compName { + wlhc = v + } + } + var ( + healthColor *color.Color + healthStatus HealthStatus + healthInfo string + workloadType string + ) + if wlhc == nil { + workloadType = "" + healthStatus = StatusNotFound + healthInfo = fmt.Sprintf("%s %s", healthStatus, "Cannot get health status") + } else { + workloadType = wlhc.TargetWorkload.Kind + healthStatus = wlhc.HealthStatus + healthInfo = fmt.Sprintf("%s %s", healthStatus, wlhc.Diagnosis) + } + healthColor = getHealthStatusColor(healthStatus) + + ioStreams.Infof("\tName: %s %s(type) %s \n", compName, workloadType, healthColor.Sprint(healthInfo)) + + traits, err := app.GetTraits(compName) + if err != nil { + return err + } + if len(traits) > 0 { + // print tree structure of Traits + tbl := uitable.New() + tbl.Separator = " " + traitNames := []string{} + for k := range traits { + traitNames = append(traitNames, k) + } + for tIndex, tName := range traitNames { + var tPrefix string + switch tIndex { + case len(traitNames) - 1: + tPrefix = lastElemPrefix + default: + tPrefix = firstElemPrefix + } + tbl.AddRow( + "\t", + fmt.Sprintf("%s%s%s/%s", + indent, + gray.Sprint(printPrefix(tPrefix)), + "Trait", + tName)) + } + ioStreams.Info("\tTraits") + ioStreams.Info(tbl) + } var appConfig v1alpha2.ApplicationConfiguration if err = c.Get(ctx, client.ObjectKey{Namespace: env.Namespace, Name: app.Name}, &appConfig); err != nil { return err } - ioStreams.Infof("Last Deployment:\n\n") - ioStreams.Infof("\tCreated at:\t%v\n", appConfig.CreationTimestamp) - ioStreams.Infof("\tUpdated at:\t%v\n", app.UpdateTime.Format(time.RFC3339)) + ioStreams.Infof(white.Sprint("\nLast Deployment:\n")) + ioStreams.Infof("\tCreated at: %v\n", appConfig.CreationTimestamp) + ioStreams.Infof("\tUpdated at: %v\n", app.UpdateTime.Format(time.RFC3339)) return nil } + +func printPrefix(p string) string { + if strings.HasSuffix(p, firstElemPrefix) { + p = strings.Replace(p, firstElemPrefix, pipe, strings.Count(p, firstElemPrefix)-1) + } else { + p = strings.ReplaceAll(p, firstElemPrefix, pipe) + } + + if strings.HasSuffix(p, lastElemPrefix) { + p = strings.Replace(p, lastElemPrefix, strings.Repeat(" ", len([]rune(lastElemPrefix))), strings.Count(p, lastElemPrefix)-1) + } else { + p = strings.ReplaceAll(p, lastElemPrefix, strings.Repeat(" ", len([]rune(lastElemPrefix)))) + } + return p +} + +func getHealthStatusColor(s HealthStatus) *color.Color { + var c *color.Color + switch s { + case StatusHealthy: + c = green + case StatusUnhealthy: + c = red + case StatusUnknown: + c = yellow + case StatusNotFound: + c = yellow + default: + c = red + } + return c +} diff --git a/pkg/cmd/workloads.go b/pkg/cmd/workloads.go index 92dfccf07..d93dab18a 100644 --- a/pkg/cmd/workloads.go +++ b/pkg/cmd/workloads.go @@ -38,4 +38,3 @@ func printWorkloadList(workloadList []types.Capability, ioStreams cmdutil.IOStre ioStreams.Info(table.String()) return nil } -