cmd/hub/api/sync.go (275 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/. //go:build api package api import ( "log" "strings" "github.com/epam/hubctl/cmd/hub/config" "github.com/epam/hubctl/cmd/hub/kube" "github.com/epam/hubctl/cmd/hub/parameters" "github.com/epam/hubctl/cmd/hub/state" "github.com/epam/hubctl/cmd/hub/storage" "github.com/epam/hubctl/cmd/hub/util" ) func SyncStackInstance(selector, status string, stateFilenames []string) { patch := StackInstancePatch{Status: &StackInstanceStatus{Status: status}} replace := false if len(stateFilenames) > 0 { replace = true state := state.MustParseStateFiles(stateFilenames) patch = TransformStateToApi(state) if status != "" { patch.Status.Status = status } remoteStatePaths := storage.RemoteStoragePaths(stateFilenames) if len(remoteStatePaths) > 0 { patch.StateFiles = remoteStatePaths } } if config.Verbose { log.Print("Syncing Stack Instance state to HubCTL") } patched, err := PatchStackInstance(selector, patch, replace) if err != nil { log.Fatalf("Unable to sync Stack Instance: %v", err) } errs := formatStackInstanceEntity(patched, false, false, false, make([]error, 0)) if len(errs) > 0 { config.AggWarnings = false util.Warn("%s", util.Errors2(errs...)) } } func TransformStateToApi(state *state.StateManifest) StackInstancePatch { return StackInstancePatch{ ComponentsEnabled: state.Lifecycle.Order, Parameters: transformStackParametersToApi(state.StackParameters), Status: &StackInstanceStatus{ Status: state.Status, Components: transformComponentsToApi(state.Lifecycle.Order, state.Components), }, InflightOperations: transformOperationsToApi(state.Operations), Outputs: transformStackOutputsToApi(appendKubernetesOutputs(state.StackOutputs, state.Components)), Provides: state.Provides, } } func transformStackParametersToApi(lockedParams []parameters.LockedParameter) []Parameter { kv := make(map[string][]parameters.LockedParameter) // group parameters by plain name for _, p := range lockedParams { if strings.HasPrefix(p.Name, "hub.") || util.Empty(p.Value) { // TODO how to preserve user supplied hub.deploymentId? // what about empty: allow? continue } if list, exist := kv[p.Name]; exist { kv[p.Name] = append(list, p) } else { kv[p.Name] = []parameters.LockedParameter{p} } } out := make([]Parameter, 0, len(lockedParams)) for _, params := range kv { // find value of parameter without component: qualifier var wildcardValue string var wildcardExist bool for _, p := range params { if p.Component == "" { wildcardExist = true wildcardValue = util.String(p.Value) break } } uniqParams := params if wildcardExist { // if parameter without component: qualifier exist, then // erase all parameters with component: qualifier having same value as wildcard uniqParams = uniqParams[:0] for _, p := range params { if p.Component == "" || util.String(p.Value) != wildcardValue { uniqParams = append(uniqParams, p) } } } // transform into API representation for _, p := range uniqParams { var value interface{} kind := "" if util.LooksLikeSecret(p.Name) || util.Contains(kube.KubernetesSecretParameters, p.Name) { secretKind := guessSecretKind("", p.Name) value = map[string]string{ "kind": secretKind, secretKind: util.String(p.Value), } kind = "secret" } else { value = p.Value } out = append(out, Parameter{ Name: p.Name, Component: p.Component, Kind: kind, Value: value, Messenger: "cli", }) } } return out } func transformComponentsToApi(order []string, stateComponents map[string]*state.StateStep) []ComponentStatus { components := make([]ComponentStatus, 0, len(stateComponents)) var prevOutputs []parameters.CapturedOutput for _, name := range order { if component, exist := stateComponents[name]; exist && component.Status != "" { noSecretOutputs := filterOutSecretOutputs(component.CapturedOutputs) outputs := transformComponentOutputsToApi(state.DiffOutputs(noSecretOutputs, prevOutputs)) if len(noSecretOutputs) > 0 { prevOutputs = noSecretOutputs } version := component.Meta.Version if version == "" && component.Version != "" { version = component.Version } components = append(components, ComponentStatus{ Name: name, Status: component.Status, Version: version, // TODO deprecate in favor of Meta.Version Meta: &ComponentMetadata{ Origin: component.Meta.Origin, Kind: component.Meta.Kind, Title: component.Meta.Title, Brief: component.Meta.Brief, Description: component.Meta.Description, Version: component.Meta.Version, Maturity: component.Meta.Maturity, Icon: component.Meta.Icon, }, Message: component.Message, Outputs: outputs, Timestamps: &Timestamps{Start: component.Timestamps.Start, End: component.Timestamps.End}, }) } } return components } func filterOutSecretOutputs(outputs []parameters.CapturedOutput) []parameters.CapturedOutput { secrets := 0 for _, o := range outputs { if strings.HasPrefix(o.Kind, "secret") { secrets++ } } if secrets == 0 { return outputs } if len(outputs) == secrets { return []parameters.CapturedOutput{} } filtered := make([]parameters.CapturedOutput, 0, len(outputs)-secrets) for _, o := range outputs { if !strings.HasPrefix(o.Kind, "secret") { filtered = append(filtered, o) } } return filtered } func transformComponentOutputsToApi(outputs []parameters.CapturedOutput) []ComponentOutput { apiOutputs := make([]ComponentOutput, 0, len(outputs)) for _, output := range outputs { apiOutputs = append(apiOutputs, ComponentOutput{ Name: output.Name, Value: output.Value, Brief: output.Brief, }) } return apiOutputs } func appendKubernetesOutputs(outputs []parameters.ExpandedOutput, components map[string]*state.StateStep) []parameters.ExpandedOutput { // make sure Kubernetes keys are added to stack outputs if not already there for _, providerName := range kube.KubernetesDefaultProviders { if provider, exist := components[providerName]; exist { next_output: for _, outputName := range kube.KubernetesParameters { outputQName := parameters.OutputQualifiedName(outputName, providerName) for _, stackOutput := range outputs { if stackOutput.Name == outputName || stackOutput.Name == outputQName { continue next_output // already present on stack outputs } } for _, output := range provider.CapturedOutputs { if output.Name == outputName { outputs = append(outputs, parameters.ExpandedOutput{Name: outputQName, Value: output.Value}) } } } break } } return outputs } func transformStackOutputsToApi(stackOutputs []parameters.ExpandedOutput) []Output { outputs := make([]Output, 0, len(stackOutputs)) for _, o := range stackOutputs { name := o.Name component := "" if strings.Contains(name, ":") { parts := strings.SplitN(name, ":", 2) if len(parts) > 1 { component = parts[0] name = parts[1] } } var value interface{} kind := "" if strings.HasPrefix(o.Kind, "secret") || util.LooksLikeSecret(name) || util.Contains(kube.KubernetesSecretParameters, name) { secretKind := guessSecretKind(o.Kind, name) value = map[string]string{ "kind": secretKind, secretKind: util.String(o.Value), } kind = "secret" } else { value = o.Value } outputs = append(outputs, Output{ Name: name, Component: component, Value: value, Kind: kind, Brief: o.Brief, Messenger: "cli", }) } return outputs } func guessSecretKind(outputKind, name string) string { if strings.Contains(outputKind, "/") { parts := strings.SplitN(outputKind, "/", 2) if len(parts) > 1 && parts[1] != "" { return parts[1] } } kind := "text" if strings.HasSuffix(name, ".key") || strings.HasSuffix(name, "Key") { kind = "privateKey" } else if strings.HasSuffix(name, ".cert") || strings.HasSuffix(name, "Cert") { kind = "certificate" } else if strings.HasSuffix(name, ".token") || strings.HasSuffix(name, "Token") { kind = "token" } else if strings.HasSuffix(name, ".password") || strings.HasSuffix(name, "Password") { kind = "password" } return kind } func transformOperationsToApi(ops []state.LifecycleOperation) []InflightOperation { apiOps := make([]InflightOperation, 0, len(ops)) for _, op := range ops { phases := make([]LifecyclePhase, 0, len(op.Phases)) for _, phase := range op.Phases { phases = append(phases, LifecyclePhase{Phase: phase.Phase, Status: phase.Status}) } apiOps = append(apiOps, InflightOperation{ Id: op.Id, Operation: op.Operation, Timestamp: op.Timestamp, Status: op.Status, Description: op.Description, Initiator: op.Initiator, Logs: op.Logs, Phases: phases, }) } return apiOps }