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..485bce21f 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,162 @@ 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("HEALTH"), + white.Sprint("INFO")) + + tbl.AddRow(namespace, fmt.Sprintf("%s/%s", + "Application", + color.New(color.Bold, color.FgGreen).Sprint(appName))) + + components := app.GetComponents() + // get 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 + } + + // print component info + tbl.AddRow(namespace, + fmt.Sprintf("%s%s/%s", + gray.Sprint(printPrefix(cPrefix)), + "Component", + color.New(color.Bold, color.FgBlue).Sprint(compName))) + traits, err := app.GetTraits(compName) + if err != nil { + return err + } + traitsNames := make([]string, 0, len(traits)) + + // print component's workload info + var wPrefix string + if len(traits) > 0 { + wPrefix = firstElemPrefix + } else { + wPrefix = lastElemPrefix + } + workloadType, _ := app.GetWorkload(compName) + wlHealthStatus := wlConditionsMap[compName].HealthStatus + + healthColor := getHealthStatusColor(wlHealthStatus) + + tbl.AddRow(namespace, + fmt.Sprintf("%s%s%s/%s", + indent, + gray.Sprint(printPrefix(wPrefix)), + "Workload", + color.New(color.Bold).Sprint(workloadType)), + healthColor.Sprint(wlHealthStatus), + wlConditionsMap[compName].Diagnosis) + + // print component's traits info + traitsInfo := getTraitsInfo(traits) + for k := range traits { + traitsNames = append(traitsNames, k) + } + for tIndex, tName := range traitsNames { + var tPrefix string + switch tIndex { + case len(traitsNames) - 1: + tPrefix = lastElemPrefix + default: + tPrefix = firstElemPrefix + } + tbl.AddRow(namespace, + fmt.Sprintf("%s%s%s/%s", + indent, + gray.Sprint(printPrefix(tPrefix)), + "Trait", + color.New(color.Bold).Sprint(tName)), + "", + traitsInfo[tName]) + } + } + 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 +265,89 @@ 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) + + var wlhc *v1alpha2.WorkloadHealthCondition + for _, v := range health.Status.WorkloadHealthConditions { + if v.ComponentName == compName { + wlhc = v + } + } + ioStreams.Info(white.Sprint("Component Status:\n")) + if wlhc == nil { + ioStreams.Infof("\tHealth Status: Cannot get health status from HealthScope:%s \n\n", health.Name) + } else { + ioStreams.Infof("\tWorkload: %s/%s \n", wlhc.TargetWorkload.Kind, wlhc.TargetWorkload.Name) + + healthColor := getHealthStatusColor(wlhc.HealthStatus) + ioStreams.Infof("\tHealth Status: %s \n", healthColor.Sprint(wlhc.HealthStatus)) + ioStreams.Infof("\tDiagnosis Info: %s \n", wlhc.Diagnosis) + if wlhc.HealthStatus == StatusUnknown { + ioStreams.Infof("\tWorkload Status: %s \n", wlhc.WorkloadStatus) + } + } + traits, err := app.GetTraits(compName) + if err != nil { + return err + } + traitsInfo := getTraitsInfo(traits) + for tName, tInfo := range traitsInfo { + ioStreams.Infof("\tTrait/%s : %s \n", tName, tInfo) + } 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 +} + +// map traitName <=> traitInfo +func getTraitsInfo(traits map[string]map[string]interface{}) map[string]string { + r := map[string]string{} + for tName, tInfo := range traits { + var tmp []string + for field, value := range tInfo { + if field == "name" { + continue + } + tmp = append(tmp, fmt.Sprintf("%s=%v", field, value)) + } + r[tName] = strings.Join(tmp, "; ") + } + return r +} 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 } -