Files
kubevela/references/cli/status.go

349 lines
11 KiB
Go

package cli
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam/util"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/appfile"
"github.com/oam-dev/kubevela/references/appfile/api"
)
// HealthStatus represents health status strings.
type HealthStatus = v1alpha2.HealthStatus
const (
// HealthStatusNotDiagnosed means there's no health scope referred or unknown health status returned
HealthStatusNotDiagnosed HealthStatus = "NOT DIAGNOSED"
)
const (
// HealthStatusHealthy represents healthy status.
HealthStatusHealthy = v1alpha2.StatusHealthy
// HealthStatusUnhealthy represents unhealthy status.
HealthStatusUnhealthy = v1alpha2.StatusUnhealthy
// HealthStatusUnknown represents unknown status.
HealthStatusUnknown = v1alpha2.StatusUnknown
)
// WorkloadHealthCondition holds health status of any resource
type WorkloadHealthCondition = v1alpha2.WorkloadHealthCondition
// ScopeHealthCondition holds health condition of a scope
type ScopeHealthCondition = v1alpha2.ScopeHealthCondition
// CompStatus represents the status of a component during "vela init"
type CompStatus int
// Enums of CompStatus
const (
compStatusDeploying CompStatus = iota
compStatusDeployFail
compStatusDeployed
compStatusHealthChecking
compStatusHealthCheckDone
compStatusUnknown
)
// Error msg used in `status` command
const (
ErrNotLoadAppConfig = "cannot load the application"
ErrFmtNotInitialized = "service: %s not ready"
ErrServiceNotFound = "service %s not found in app"
)
const (
trackingInterval time.Duration = 1 * time.Second
deployTimeout time.Duration = 10 * time.Second
healthCheckBufferTime time.Duration = 120 * time.Second
)
// NewAppStatusCommand creates `status` command for showing status
func NewAppStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
ctx := context.Background()
cmd := &cobra.Command{
Use: "status APP_NAME",
Short: "Show status of an application",
Long: "Show status of an application, including workloads and traits of each service.",
Example: `vela status APP_NAME`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return c.SetConfig()
},
RunE: func(cmd *cobra.Command, args []string) error {
argsLength := len(args)
if argsLength == 0 {
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 := c.GetClient()
if err != nil {
return err
}
return printAppStatus(ctx, newClient, ioStreams, appName, env, cmd, c)
},
Annotations: map[string]string{
types.TagCommandType: types.TypeApp,
},
}
cmd.Flags().StringP("svc", "s", "", "service name")
cmd.SetOut(ioStreams.Out)
return cmd
}
func printAppStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOStreams, appName string, env *types.EnvMeta, cmd *cobra.Command, velaC types.Args) error {
app, err := appfile.LoadApplication(env.Namespace, appName, velaC)
if err != nil {
return err
}
namespace := env.Namespace
cmd.Printf("About:\n\n")
table := newUITable()
table.AddRow(" Name:", appName)
table.AddRow(" Namespace:", namespace)
table.AddRow(" Created at:", app.CreationTimestamp.String())
cmd.Printf("%s\n\n", table.String())
cmd.Printf("Services:\n\n")
return loopCheckStatus(ctx, c, ioStreams, appName, env)
}
func loadRemoteApplication(c client.Client, ns string, name string) (*v1alpha2.Application, error) {
app := new(v1alpha2.Application)
err := c.Get(context.Background(), client.ObjectKey{
Namespace: ns,
Name: name,
}, app)
return app, err
}
func loopCheckStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOStreams, appName string, env *types.EnvMeta) error {
remoteApp, err := loadRemoteApplication(c, env.Namespace, appName)
if err != nil {
return err
}
for _, comp := range remoteApp.Spec.Components {
compName := comp.Name
healthStatus, healthInfo, err := healthCheckLoop(ctx, c, compName, appName, env)
if err != nil {
ioStreams.Info(healthInfo)
return err
}
ioStreams.Infof(white.Sprintf(" - Name: %s\n", compName))
ioStreams.Infof(" Type: %s\n", comp.WorkloadType)
healthColor := getHealthStatusColor(healthStatus)
healthInfo = strings.ReplaceAll(healthInfo, "\n", "\n\t") // format healthInfo output
ioStreams.Infof(" %s %s\n", healthColor.Sprint(healthStatus), healthColor.Sprint(healthInfo))
// load it again after health check
remoteApp, err = loadRemoteApplication(c, env.Namespace, appName)
if err != nil {
return err
}
// workload Must found
ioStreams.Infof(" Traits:\n")
workloadStatus, _ := getWorkloadStatusFromApp(remoteApp, compName)
for _, tr := range workloadStatus.Traits {
if tr.Message != "" {
if tr.Healthy {
ioStreams.Infof(" - %s%s: %s", emojiSucceed, white.Sprint(tr.Type), tr.Message)
} else {
ioStreams.Infof(" - %s%s: %s", emojiFail, white.Sprint(tr.Type), tr.Message)
}
continue
}
var message string
for _, v := range comp.Traits {
if v.Name == tr.Type {
traitData, _ := util.RawExtension2Map(&v.Properties)
for k, v := range traitData {
message += fmt.Sprintf("%v=%v\n\t\t", k, v)
}
break
}
}
ioStreams.Infof(" - %s%s: %s", emojiSucceed, white.Sprint(tr.Type), message)
}
ioStreams.Info("")
ioStreams.Infof(" Last Deployment:\n")
ioStreams.Infof(" Created at: %v\n", remoteApp.CreationTimestamp)
}
return nil
}
func healthCheckLoop(ctx context.Context, c client.Client, compName, appName string, env *types.EnvMeta) (HealthStatus, string, error) {
// Health Check Loop For Workload
var healthInfo string
var healthStatus HealthStatus
var err error
sHealthCheck := newTrackingSpinner("Checking health status ...")
sHealthCheck.Start()
defer sHealthCheck.Stop()
HealthCheckLoop:
for {
time.Sleep(trackingInterval)
var healthcheckStatus CompStatus
healthcheckStatus, healthStatus, healthInfo, err = trackHealthCheckingStatus(ctx, c, compName, appName, env)
if err != nil {
healthInfo = red.Sprintf("Health checking failed!")
return "", healthInfo, err
}
if healthcheckStatus == compStatusHealthCheckDone {
break HealthCheckLoop
}
}
return healthStatus, healthInfo, nil
}
func printTrackingDeployStatus(c types.Args, ioStreams cmdutil.IOStreams, appName string, env *types.EnvMeta) (CompStatus, error) {
sDeploy := newTrackingSpinnerWithDelay("Checking Status ...", trackingInterval)
sDeploy.Start()
defer sDeploy.Stop()
TrackDeployLoop:
for {
time.Sleep(trackingInterval)
deployStatus, failMsg, err := TrackDeployStatus(c, appName, env)
if err != nil {
return compStatusUnknown, err
}
switch deployStatus {
case compStatusDeploying:
continue
case compStatusDeployed:
ioStreams.Info(green.Sprintf("\n%sApplication Deployed Successfully!", emojiSucceed))
break TrackDeployLoop
case compStatusDeployFail:
ioStreams.Info(red.Sprintf("\n%sApplication Failed to Deploy!", emojiFail))
ioStreams.Info(red.Sprintf("Reason: %s", failMsg))
return compStatusDeployFail, nil
default:
continue
}
}
return compStatusDeployed, nil
}
// TrackDeployStatus will only check AppConfig is deployed successfully,
func TrackDeployStatus(c types.Args, appName string, env *types.EnvMeta) (CompStatus, string, error) {
appObj, err := appfile.LoadApplication(env.Namespace, appName, c)
if err != nil {
return compStatusUnknown, "", err
}
if appObj == nil {
return compStatusUnknown, "", errors.New(ErrNotLoadAppConfig)
}
condition := appObj.Status.Conditions
if len(condition) < 1 {
return compStatusDeploying, "", nil
}
// If condition is true, we can regard appConfig is deployed successfully
if appObj.Status.Phase == v1alpha2.ApplicationRunning {
return compStatusDeployed, "", nil
}
// if not found workload status in AppConfig
// then use age to check whether the workload controller is running
if time.Since(appObj.GetCreationTimestamp().Time) > deployTimeout {
return compStatusDeployFail, condition[0].Message, nil
}
return compStatusDeploying, "", nil
}
// trackHealthCheckingStatus will check health status from health scope
func trackHealthCheckingStatus(ctx context.Context, c client.Client, compName, appName string, env *types.EnvMeta) (CompStatus, HealthStatus, string, error) {
app, err := loadRemoteApplication(c, env.Namespace, appName)
if err != nil {
return compStatusUnknown, HealthStatusNotDiagnosed, "", err
}
if len(app.Status.Conditions) < 1 {
// still reconciling
return compStatusUnknown, HealthStatusUnknown, "", nil
}
// check whether referenced a HealthScope
var healthScopeName string
for _, v := range app.Spec.Components {
if len(v.Scopes) > 0 {
healthScopeName = v.Scopes[api.DefaultHealthScopeKey]
}
}
var healthStatus HealthStatus
if healthScopeName != "" {
var healthScope v1alpha2.HealthScope
if err = c.Get(ctx, client.ObjectKey{Namespace: env.Namespace, Name: healthScopeName}, &healthScope); err != nil {
return compStatusUnknown, HealthStatusUnknown, "", err
}
var wlhc *v1alpha2.WorkloadHealthCondition
for _, v := range healthScope.Status.WorkloadHealthConditions {
if v.ComponentName == compName {
wlhc = v
}
}
if wlhc == nil {
return compStatusUnknown, HealthStatusUnknown, "", fmt.Errorf("cannot get health condition from the health scope: %s", healthScope.Name)
}
healthStatus = wlhc.HealthStatus
if healthStatus == HealthStatusHealthy {
return compStatusHealthCheckDone, healthStatus, wlhc.Diagnosis, nil
}
if healthStatus == HealthStatusUnhealthy {
cTime := app.GetCreationTimestamp()
if time.Since(cTime.Time) <= healthCheckBufferTime {
return compStatusHealthChecking, HealthStatusUnknown, "", nil
}
return compStatusHealthCheckDone, healthStatus, wlhc.Diagnosis, nil
}
}
return compStatusHealthCheckDone, HealthStatusNotDiagnosed, "", nil
}
func getWorkloadStatusFromApp(app *v1alpha2.Application, compName string) (v1alpha2.ApplicationComponentStatus, bool) {
foundWlStatus := false
wlStatus := v1alpha2.ApplicationComponentStatus{}
if app == nil {
return wlStatus, foundWlStatus
}
for _, v := range app.Status.Services {
if v.Name == compName {
wlStatus = v
foundWlStatus = true
break
}
}
return wlStatus, foundWlStatus
}
func getHealthStatusColor(s HealthStatus) *color.Color {
var c *color.Color
switch s {
case HealthStatusHealthy:
c = green
case HealthStatusUnknown, HealthStatusNotDiagnosed:
c = yellow
default:
c = red
}
return c
}