cmd/hub/state/merge.go (229 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 (
"fmt"
"log"
"strings"
"time"
"github.com/epam/hubctl/cmd/hub/config"
"github.com/epam/hubctl/cmd/hub/parameters"
"github.com/epam/hubctl/cmd/hub/storage"
"github.com/epam/hubctl/cmd/hub/util"
)
func MergeState(stateFiles *storage.Files,
componentName string, depends []string, order []string, isDeploy bool,
parameters parameters.LockedParameters, outputs parameters.CapturedOutputs, provides map[string][]string) (*StateManifest, error) {
state, err := ParseState(stateFiles)
if err != nil {
return nil, err
}
MergeParsedState(state, componentName, depends, order, isDeploy, parameters, outputs, provides)
return state, nil
}
func MergeParsedState(state *StateManifest,
componentName string, depends []string, order []string, isDeploy bool,
parameters parameters.LockedParameters, outputs parameters.CapturedOutputs, provides map[string][]string) {
MergeParsedStateParametersAndProvides(state, parameters, provides)
MergeParsedStateOutputs(state, componentName, depends, order, isDeploy, outputs)
}
func MergeParsedStateParametersAndProvides(state *StateManifest,
parameters parameters.LockedParameters, provides map[string][]string) {
if parameters != nil {
mergeStateParameters(parameters, state.StackParameters)
}
if provides != nil {
mergeStateProvides(provides, state.Provides)
}
}
func MergeParsedStateOutputs(state *StateManifest,
componentName string, depends []string, order []string, isDeploy bool,
outputs parameters.CapturedOutputs) {
if outputs != nil {
outputsToMerge, mergedTimestamp := chooseStateOutputsToMerge(state, componentName, order, isDeploy)
mergeStateOutputs(outputs, outputsToMerge)
mergeStateOutputsFromDependencies(outputs, depends, mergedTimestamp, state.Components)
}
}
func chooseStateOutputsToMerge(state *StateManifest, componentName string, order []string,
isDeploy bool) ([]parameters.CapturedOutput, time.Time) {
outputsToMerge := state.CapturedOutputs
mergedTimestamp := state.Timestamp
componentStateName := ""
componentStateExist := false
var componentState *StateStep
if state.Components != nil {
if componentName == "" && len(outputsToMerge) == 0 {
componentName = order[len(order)-1] // if global state is empty then start search with the last component
}
if componentName != "" {
found := -1
for i, name := range order {
if componentName == name {
found = i
break
}
}
if isDeploy {
if found == 0 {
// first component is a special case
outputsToMerge = nil
}
found--
}
for found >= 0 {
componentStateName = order[found]
st, exist := state.Components[componentStateName]
if exist && len(st.CapturedOutputs) > 0 {
break
}
found--
}
if found < 0 {
componentStateName = ""
}
}
if componentStateName != "" {
componentState, componentStateExist = state.Components[componentStateName]
}
}
if componentStateExist {
outputsToMerge = componentState.CapturedOutputs
mergedTimestamp = componentState.Timestamp
if config.Verbose {
log.Printf("Loading state after component `%s` deployment", componentStateName)
}
} else {
if config.Verbose && outputsToMerge != nil {
log.Print("Loading global state")
}
}
if len(outputsToMerge) == 0 && config.Verbose && outputsToMerge != nil {
label := "Global"
hint := ""
if componentStateExist {
label = fmt.Sprintf("Component `%s`", componentStateName)
hint = " (try --load-global-state / -g)"
}
log.Printf("%s outputs state is empty%s ", label, hint)
}
return outputsToMerge, mergedTimestamp
}
func mergeStateParameters(parameters parameters.LockedParameters, add []parameters.LockedParameter) {
for _, p := range add {
mergeStateParameter(parameters, p)
}
}
func mergeStateParameter(parameters parameters.LockedParameters, add parameters.LockedParameter) {
qName := add.QName()
current, exists := parameters[qName]
if exists {
curValue := util.String(current.Value)
addValue := util.String(add.Value)
if curValue != addValue {
if util.Empty(current.Value) {
util.Warn("Parameter `%s` empty value is replaced by value `%s` from state",
qName, util.Trim(util.MaybeMaskedValue(config.Trace, qName, addValue)))
current.Value = add.Value
} else {
util.Warn("Parameter `%s` current value `%s` does not match value `%s` from state - keeping current value",
qName,
util.Trim(util.MaybeMaskedValue(config.Trace, qName, curValue)),
util.Trim(util.MaybeMaskedValue(config.Trace, qName, addValue)))
}
}
if current.Env != add.Env {
if current.Env == "" {
if config.Debug {
log.Printf("Parameter `%s` environment variable setup is updated to `%s` from state",
qName, add.Env)
}
current.Env = add.Env
} else if add.Env != "" {
util.Warn("Parameter `%s` current environment variable setup `%s` does not match setup `%s` from state - keeping current setup",
qName, current.Env, add.Env)
}
}
parameters[qName] = current
} else {
// TODO review: should we really merge stack-level parameters from state?
parameters[qName] = add
}
}
func mergeStateOutputs(outputs parameters.CapturedOutputs, state []parameters.CapturedOutput) {
for _, o := range state {
parameters.MergeOutput(outputs, o)
}
}
func mergeStateOutputsFromDependencies(outputs parameters.CapturedOutputs, depends []string, mergedTimestamp time.Time,
components map[string]*StateStep) {
loading := make([]string, 0, len(depends))
for _, dependencyName := range depends {
// TODO review: always load outputs from dependencies?
if _, exist := components[dependencyName]; exist /* && dependency.Timestamp.After(mergedTimestamp) */ {
loading = append(loading, dependencyName)
}
}
if config.Verbose && len(loading) > 0 {
log.Printf("Additionally, loading state after component(s) %v due to `depends` declaration", loading)
}
for _, dependencyName := range util.Reverse(loading) {
if dependency, exist := components[dependencyName]; exist {
for _, output := range dependency.CapturedOutputs {
qName := output.QName()
current, exists := outputs[qName]
overwrite := false
outValue := util.String(output.Value)
if exists {
warn := !strings.HasPrefix(output.Name, "hub.")
curValue := util.String(current.Value)
if curValue != outValue {
if !util.Empty(current.Value) {
overwrite = true // TODO review overwrite logic
if warn {
util.Warn("Loaded output `%s` current value `%s` does not match new value `%s` loaded from dependency `%s` - using new value",
qName,
util.Trim(util.MaybeMaskedValue(config.Trace, qName, curValue)),
util.Trim(util.MaybeMaskedValue(config.Trace, qName, outValue)),
dependencyName)
}
} else if !util.Empty(output.Value) {
overwrite = true
if warn {
util.Warn("Loaded output `%s` empty value replaced by new value `%s` loaded from dependency `%s`",
qName, util.Trim(util.MaybeMaskedValue(config.Trace, qName, outValue)), dependencyName)
}
}
}
} else {
overwrite = true
}
if overwrite {
if config.Debug {
log.Printf("\t%s => %s", qName, util.Trim(util.MaybeMaskedValue(config.Trace, qName, outValue)))
}
outputs[qName] = output
}
}
}
}
}
func mergeStateProvides(provides map[string][]string, state map[string][]string) {
for prov, by := range state {
who, exist := provides[prov]
if !exist {
provides[prov] = by
} else {
merged := make([]string, len(who), len(who)+len(by))
copy(merged, who)
for _, comp := range by {
found := false
for _, comp2 := range who {
if comp == comp2 {
found = true
break
}
}
if !found {
merged = append(merged, comp)
}
}
provides[prov] = merged
}
}
}