cmd/hub/lifecycle/outputs.go (264 lines of code) (raw):

// Copyright (c) 2023 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 lifecycle import ( "bytes" "encoding/base64" "encoding/json" "fmt" "log" "os" "path/filepath" "strings" "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" ) const fileRefPrefix = "file://" var ( outputsMarker = []byte("Outputs:\n") outputSupportedEncodings = []string{"base64", "json"} ) func captureOutputs(componentName, componentDir string, componentManifest *manifest.Manifest, componentParameters parameters.LockedParameters, textOutput []byte, random []byte) (parameters.RawOutputs, parameters.CapturedOutputs, []string, []error) { tfOutputs := parseTextOutput(textOutput) secrets := extractSecrets(tfOutputs, random) if len(secrets) > 0 { if config.Trace { log.Printf("Parsing secrets chunk:\n%s", secrets) } outputs := make(map[string][]string) parseTextKV(secrets, outputs) if config.Trace { log.Print("Parsed secret outputs:") util.PrintMap2(outputs) } for k, v := range toRawOutputs(outputs) { tfOutputs[k] = v } } dynamicProvides := extractDynamicProvides(tfOutputs) outputs, errs := expandRequestedOutputs(componentName, componentDir, componentParameters, componentManifest.Outputs, tfOutputs) for k, o := range outputs { o.ComponentOrigin = componentManifest.Meta.Origin o.ComponentKind = componentManifest.Meta.Kind outputs[k] = o } if len(errs) > 0 { if len(tfOutputs) > 0 { log.Print("Raw outputs:") util.PrintMap(tfOutputs) } else { log.Print("No raw outputs captured") } } return tfOutputs, outputs, dynamicProvides, errs } func expandRequestedOutputs(componentName, componentDir string, componentParameters parameters.LockedParameters, requestedOutputs []manifest.Output, tfOutputs parameters.RawOutputs) (parameters.CapturedOutputs, []error) { kv := parameters.ParametersKV(componentParameters) outputs := make(parameters.CapturedOutputs) errs := make([]error, 0) for _, requestedOutput := range requestedOutputs { output := parameters.CapturedOutput{ Component: componentName, Name: requestedOutput.Name, Brief: requestedOutput.Brief, Kind: requestedOutput.Kind, } if requestedOutput.FromTfVar != "" { variable, encodings := head(requestedOutput.FromTfVar) value, exist := tfOutputs[variable] if !exist { errs = append(errs, fmt.Errorf("Unable to capture raw output `%s` for component `%s` output `%s`", variable, componentName, requestedOutput.Name)) value = "(unknown)" } else { if strings.HasPrefix(value, fileRefPrefix) && len(value) > len(fileRefPrefix) { filename := value[len(fileRefPrefix):] if !filepath.IsAbs(filename) { filename = filepath.Join(componentDir, filename) } bytes, err := os.ReadFile(filename) if err != nil { util.Warn("Unable to read raw output `%s` from `%s` for component `%s` output `%s`: %v", variable, filename, componentName, requestedOutput.Name, err) // pass value as is } else { value = string(bytes) if strings.Count(value, "\n") <= 1 { value = strings.Trim(value, " \r\n") } } } if len(encodings) > 0 { if unknown := util.OmitAll(encodings, outputSupportedEncodings); len(unknown) > 0 { errs = append(errs, fmt.Errorf("Unknown encoding(s) %v capturing output `%s` from raw output `%s`", unknown, requestedOutput.FromTfVar, variable)) } if util.Contains(encodings, "base64") { bValue, err := base64.StdEncoding.DecodeString(value) if err != nil { errs = append(errs, fmt.Errorf("Unable to decode base64 `%s` while capturing output `%s` from raw output `%s`: %v", util.Trim(value), requestedOutput.FromTfVar, variable, err)) } else { value = string(bValue) } } if util.Contains(encodings, "json") { var iValue interface{} err := json.Unmarshal([]byte(value), &iValue) if err != nil { errs = append(errs, fmt.Errorf("Unable to unmarshal JSON `%s` while capturing output `%s` from raw output `%s`: %v", util.Trim(value), requestedOutput.FromTfVar, variable, err)) } else { output.Value = iValue } } } } if output.Value == nil { // TODO decoded nil from JSON null? output.Value = value } } else { if util.Empty(requestedOutput.Value) { requestedOutput.Value = fmt.Sprintf("${%s}", requestedOutput.Name) } if parameters.RequireExpansion(requestedOutput.Value) { requestedOutputValue := requestedOutput.Value.(string) value := parameters.CurlyReplacement.ReplaceAllStringFunc(requestedOutputValue, func(match string) string { expr, isCel := parameters.StripCurly(match) var substitution interface{} if isCel { var err error substitution, err = parameters.CelEval(expr, componentName, nil, kv) if err != nil { errs = append(errs, err) } } else { var exist bool substitution, exist = parameters.FindValue(expr, componentName, nil, kv) if !exist { errs = append(errs, fmt.Errorf("Component `%s` output `%s = %s` refer to unknown substitution `%s`", componentName, requestedOutput.Name, requestedOutputValue, expr)) substitution = "(unknown)" } } if parameters.RequireExpansion(substitution) { errs = append(errs, fmt.Errorf("Component `%s` output `%s = %s` refer to substitution `%s` that expands to `%s` - this is surely a bug", componentName, requestedOutput.Name, requestedOutputValue, expr, substitution)) substitution = "(bug)" } return util.String(substitution) }) output.Value = value } else { output.Value = requestedOutput.Value } } outputs[output.QName()] = output kv[requestedOutput.Name] = output.Value } return outputs, errs } func parseTextOutput(textOutput []byte) parameters.RawOutputs { outputs := make(map[string][]string) chunk := 1 for { i := bytes.Index(textOutput, outputsMarker) if i == -1 { if config.Trace && len(outputs) > 0 { log.Print("Parsed raw outputs:") util.PrintMap2(outputs) } return toRawOutputs(outputs) } markerFound := i == 0 || (i > 0 && textOutput[i-1] == '\n') textOutput = textOutput[i+len(outputsMarker):] if !markerFound { continue } textFragment := textOutput i = bytes.Index(textFragment, []byte("\n\n")) if i > 0 { textFragment = textFragment[:i] } if config.Trace { log.Printf("Parsing output chunk #%d:\n%s", chunk, textFragment) chunk++ } parseTextKV(textFragment, outputs) } } func parseTextKV(text []byte, outputs map[string][]string) { lines := strings.Split(string(text), "\n") for _, line := range lines { if strings.HasPrefix(line, "#") { continue } kv := strings.SplitN(line, "=", 2) if len(kv) != 2 { continue } key := util.TrimColor(util.Trim(kv[0])) value := maybeUnquote(util.TrimColor(util.Trim(kv[1]))) // accumulate repeating keys list, exist := outputs[key] if exist { if !util.Contains(list, value) { outputs[key] = append(list, value) } } else { outputs[key] = []string{value} } } } func maybeUnquote(v string) string { if len(v) > 1 && strings.HasPrefix(v, "\"") && strings.HasSuffix(v, "\"") { v = v[1 : len(v)-1] v = strings.Replace(v, "\\\"", "\"", -1) } return v } func toRawOutputs(outputs map[string][]string) parameters.RawOutputs { rawOutputs := make(parameters.RawOutputs) for k, list := range outputs { rawOutputs[k] = strings.Join(list, ",") } return rawOutputs } func extractDynamicProvides(rawOutputs parameters.RawOutputs) []string { key := "provides" if v, exist := rawOutputs[key]; exist && len(v) > 0 { return strings.Split(v, ",") } return nil } func extractSecrets(rawOutputs parameters.RawOutputs, random []byte) []byte { key := "secrets" if v, exist := rawOutputs[key]; exist && len(v) > 0 { decoded, err := util.OtpDecode(v, random) if err != nil { util.Warn("Unable to decode secret outputs: %v", err) return nil } return decoded } return nil } func gitOutputs(componentName, dir string, status bool) parameters.CapturedOutputs { keys, err := gitStatus(dir, status) if err != nil { util.Warn("Unable to capture `%s` Git status: %v", componentName, err) } if len(keys) > 0 { base := fmt.Sprintf("hub.components.%s.git", componentName) outputs := make(parameters.CapturedOutputs) for k, v := range keys { outputName := fmt.Sprintf("%s.%s", base, k) outputs[outputName] = parameters.CapturedOutput{Component: componentName, Name: outputName, Value: v} } return outputs } return nil }