cmd/hub/state/explain.go (354 lines of code) (raw):

// Copyright (c) 2022 EPAM Systems, Inc. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package state import ( "encoding/json" "fmt" "log" "os" "sort" "strings" "time" "github.com/logrusorgru/aurora" "gopkg.in/yaml.v2" "github.com/epam/hubctl/cmd/hub/config" "github.com/epam/hubctl/cmd/hub/manifest" "github.com/epam/hubctl/cmd/hub/parameters" "github.com/epam/hubctl/cmd/hub/util" ) type ExplainedComponent struct { Timestamp time.Time `yaml:",omitempty" json:"timestamp,omitempty"` Timestamps Timestamps `yaml:",omitempty" json:"timestamps,omitempty"` Status string `yaml:",omitempty" json:"status,omitempty"` Message string `yaml:",omitempty" json:"message,omitempty"` Parameters map[string]string `yaml:",omitempty" json:"parameters,omitempty"` Outputs map[string]string `yaml:",omitempty" json:"outputs,omitempty"` RawOutputs map[string]string `yaml:"rawOutputs,omitempty" json:"rawOutputs,omitempty"` } type ExplainedState struct { Meta Metadata `yaml:",omitempty" json:"meta,omitempty"` Timestamp time.Time `yaml:",omitempty" json:"timestamp,omitempty"` Status string `yaml:",omitempty" json:"status,omitempty"` Message string `yaml:",omitempty" json:"message,omitempty"` StackParameters map[string]string `yaml:"stackParameters,omitempty" json:"stackParameters,omitempty"` StackOutputs map[string]string `yaml:"stackOutputs,omitempty" json:"stackOutputs,omitempty"` Provides map[string][]string `yaml:",omitempty" json:"provides,omitempty"` Components map[string]ExplainedComponent `yaml:",omitempty" json:"components,omitempty"` } func Explain(elaborateManifests, stateFilenames []string, opLog, global bool, componentName string, rawOutputs bool, format string /*text, kv, sh, json, yaml*/, color bool) { if (color || config.Tty) && format == "text" { headColor = func(str string) string { return aurora.Green(str).String() } } if format != "text" && config.Verbose && !config.Debug { config.Verbose = false } if opLog && format != "text" { log.Fatal("Lifecycle operations log can only be explained in text format") } state := MustParseStateFiles(stateFilenames) components := state.Lifecycle.Order if opLog { printOpLog(state) return } var stackManifest *manifest.Manifest if len(elaborateManifests) > 0 { var err error stackManifest, _, _, err = manifest.ParseManifest(elaborateManifests) if err != nil { util.Warn("Unable to parse: %v", err) } else if stackManifest != nil { order, err := manifest.GenerateLifecycleOrder(stackManifest) if err != nil { log.Fatal(err) } stackManifest.Lifecycle.Order = order components = stackManifest.Lifecycle.Order } } var prevOutputs []parameters.CapturedOutput if componentName != "" { if stackManifest != nil { manifest.CheckComponentsExist(stackManifest.Components, componentName) } for i, c := range components { if c == componentName { if i > 0 { prevComponentState, exist := state.Components[components[i-1]] if exist { prevOutputs = prevComponentState.CapturedOutputs } } } } components = []string{componentName} } if format == "text" { if global || componentName == "" { fmt.Printf("Kind: %s\n", state.Meta.Kind) fmt.Printf("Name: %s\n", state.Meta.Name) fmt.Printf("Timestamp: %v\n", state.Timestamp.Truncate(time.Second)) fmt.Printf("Status: %s\n", state.Status) if state.Message != "" { fmt.Printf("Message: %s\n", state.Message) } fmt.Print(headColor("Stack parameters:\n")) printLockedParameters(state.StackParameters) printStackOutputs(state.StackOutputs) printProvides(state.Provides) } if !global || componentName != "" { for _, component := range components { if step, exist := state.Components[component]; exist { fmt.Printf("Component: %s\n", headColor(component)) printComponenentState(component, step, prevOutputs, rawOutputs) prevOutputs = step.CapturedOutputs } } } } else { explained := ExplainedState{ Meta: state.Meta, Timestamp: state.Timestamp, Status: state.Status, Message: state.Message, StackParameters: make(map[string]string), StackOutputs: make(map[string]string), Components: make(map[string]ExplainedComponent), } if global || componentName == "" { for _, parameter := range state.StackParameters { explained.StackParameters[parameter.QName()] = util.String(parameter.Value) } for _, output := range state.StackOutputs { explained.StackOutputs[output.Name] = util.String(output.Value) } explained.Provides = state.Provides } if !global || componentName != "" { for _, component := range components { if step, exist := state.Components[component]; exist { comp := ExplainedComponent{ Timestamp: step.Timestamp, Timestamps: step.Timestamps, Status: step.Status, Message: step.Message, Parameters: make(map[string]string), Outputs: make(map[string]string), RawOutputs: make(map[string]string), } for _, parameter := range step.Parameters { comp.Parameters[parameter.Name] = util.String(parameter.Value) } for _, output := range DiffOutputs(step.CapturedOutputs, prevOutputs) { comp.Outputs[output.Name] = util.String(output.Value) } prevOutputs = step.CapturedOutputs if rawOutputs { for _, output := range step.RawOutputs { comp.RawOutputs[output.Name] = output.Value } } explained.Components[component] = comp } } } var bytes []byte var err error switch format { case "json": bytes, err = json.MarshalIndent(&explained, "", " ") case "yaml": bytes, err = yaml.Marshal(&explained) // case "sh": default: log.Fatalf("`%s` output format is not implemented", format) } if err != nil { log.Fatalf("Unable to explain in `%s` format: %v", format, err) } written, err := os.Stdout.Write(bytes) if err != nil || written != len(bytes) { log.Fatalf("Error writting output (wrote %d of ouf %d bytes): %v", written, len(bytes), err) } } } var headColor = func(str string) string { return str } func printComponenentState(componentName string, step *StateStep, prevOutputs []parameters.CapturedOutput, rawOutputs bool) { fmt.Printf("-- Timestamp: %v\n", step.Timestamp.Truncate(time.Second)) if t := step.Timestamps; !t.End.IsZero() && !t.Start.IsZero() { fmt.Printf("-- Duration: %v\n", t.End.Sub(t.Start).Round(time.Second).String()) } fmt.Printf("-- Status: %s\n", step.Status) if step.Meta.Origin != "" && step.Meta.Origin != componentName { fmt.Printf("-- Origin: %s\n", step.Meta.Origin) } if step.Meta.Kind != "" && step.Meta.Kind != step.Meta.Origin { fmt.Printf("-- Kind: %s\n", step.Meta.Kind) } if step.Meta.Title != "" { fmt.Printf("-- Title: %s\n", step.Meta.Title) } version := step.Meta.Version if version == "" && step.Version != "" { version = step.Version } if version != "" { fmt.Printf("-- Version: %s\n", version) } if step.Message != "" { fmt.Printf("-- Message: %s\n", step.Message) } fmt.Print("-- Parameters:\n") printLockedParameters(step.Parameters) if rawOutputs && len(step.RawOutputs) > 0 { fmt.Print("-- Raw outputs:\n") printRawOutputs(step.RawOutputs) } fmt.Print("-- Outputs:\n") printDiffOutputs(step.CapturedOutputs, prevOutputs) } func printLockedParameters(parameters []parameters.LockedParameter) { for _, parameter := range parameters { qName := parameter.QName() env := "" if parameter.Env != "" { env = fmt.Sprintf(" (env:%s)", parameter.Env) } fmt.Printf("\t%s => `%s`%s\n", qName, util.Wrap(util.String(parameter.Value)), env) } } func printDiffOutputs(curr, prev []parameters.CapturedOutput) { keys := make(map[string]string) for _, p := range prev { str := util.String(p.Value) keys[p.QName()] = str keys[p.Name] = str } for _, c := range curr { if strings.HasPrefix(c.Name, "hub.components.") { continue } qName := c.QName() _, exist := keys[qName] if !exist { over, overExist := keys[c.Name] kind := "" if c.Kind != "" { kind = fmt.Sprintf("[%s] ", c.Kind) } brief := "" if c.Brief != "" { brief = fmt.Sprintf(" [%s]", c.Brief) } value := util.Wrap(util.String(c.Value)) if !overExist { fmt.Printf("\t%s%s%s => `%s`\n", kind, c.Name, brief, value) } else if util.String(c.Value) != over { fmt.Printf("\t%s%s%s => `%s` (was: `%s`)\n", kind, c.Name, brief, value, util.Wrap(over)) } else { fmt.Printf("\t%s%s%s => `%s`\n", kind, qName, brief, value) } } } } func DiffOutputs(curr, prev []parameters.CapturedOutput) []parameters.CapturedOutput { keys := make(map[string]struct{}) for _, p := range prev { keys[p.QName()] = struct{}{} } sz := len(curr) - len(prev) if sz < 0 { sz = 0 } diff := make([]parameters.CapturedOutput, 0, sz) for _, c := range curr { if strings.HasPrefix(c.Name, "hub.components.") { continue } if _, exist := keys[c.QName()]; !exist { diff = append(diff, c) } } return diff } func printRawOutputs(rawOutputs []parameters.RawOutput) { for _, o := range rawOutputs { fmt.Printf("\t%s = %s\n", o.Name, o.Value) } } func printStackOutputs(expanded []parameters.ExpandedOutput) { if len(expanded) > 0 { fmt.Print(headColor("Stack outputs:\n")) for _, expandedOutput := range expanded { brief := "" if expandedOutput.Brief != "" { brief = fmt.Sprintf("[%s] ", expandedOutput.Brief) } kind := "" if expandedOutput.Kind != "" { kind = fmt.Sprintf("[%s] ", expandedOutput.Kind) } fmt.Printf("\t%s%s%s = %s\n", brief, kind, expandedOutput.Name, expandedOutput.Value) } } } func printProvides(deps map[string][]string) { if len(deps) > 0 { fmt.Print(headColor("Provides:\n")) keys := make([]string, 0, len(deps)) for name := range deps { keys = append(keys, name) } sort.Strings(keys) for _, name := range keys { fmt.Printf("\t%s => %s\n", name, strings.Join(deps[name], ", ")) } } } func printOpLog(st *StateManifest) { ops := st.Operations if len(ops) == 0 { fmt.Print("No operations log") } fmt.Print("Operations:\n") for _, op := range ops { fmt.Print(formatOperation(op, true)) } } func formatOperation(op LifecycleOperation, showLogs bool) string { ident := "\t" logs := "" if showLogs && op.Logs != "" { logs = fmt.Sprintf("%sLogs:\n%s\t%s\n", ident, ident, strings.Join(strings.Split(op.Logs, "\n"), "\n"+ident+"\t")) } initiator := "" if op.Initiator != "" { initiator = fmt.Sprintf(" by %s", op.Initiator) } options := "" if len(op.Options) > 0 { options = fmt.Sprintf("%sOptions: %v\n", ident, op.Options) } description := "" if op.Description != "" { description = fmt.Sprintf(" (%s)", op.Description) } phases := "" if len(op.Phases) > 0 { phases = fmt.Sprintf("%sPhases:\n%s\t%s\n", ident, ident, formatLifecyclePhases(op.Phases, ident)) } return fmt.Sprintf("%s%s %s - %s %v%s%s %s\n%s%s%s", ident, headColor("Operation:"), op.Operation, op.Status, op.Timestamp.Truncate(time.Second), initiator, description, op.Id, options, phases, logs) } func formatLifecyclePhases(phases []LifecyclePhase, ident string) string { str := make([]string, 0, len(phases)) for _, phase := range phases { str = append(str, fmt.Sprintf("%s - %s", phase.Phase, phase.Status)) } return strings.Join(str, "\n"+ident+"\t") }