cmd/hub/compose/elaborate.go (1,134 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 compose
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gopkg.in/yaml.v2"
"github.com/epam/hubctl/cmd/hub/config"
"github.com/epam/hubctl/cmd/hub/kube"
"github.com/epam/hubctl/cmd/hub/manifest"
"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"
)
// this must match to lifecycle.checkRequires()
var requirementProvidedByEnvironment = []string{
"aws", "gcp", "gcs", "azure", "kubectl", "kubernetes", "helm", "vault",
}
var defaultLifecycleVerbs = []string{"deploy", "undeploy"}
func Elaborate(manifestFilename string,
parametersFilenames []string, environmentOverrides, explicitProvides string,
stateManifests []string, useStateStackParameters bool, elaborateManifests []string, componentsBaseDir string,
pipe io.WriteCloser) {
if config.Verbose {
parametersFrom := ""
if len(parametersFilenames) > 0 {
parametersFrom = fmt.Sprintf(" with parameters from %s", strings.Join(parametersFilenames, ", "))
}
overrides := ""
if environmentOverrides != "" {
overrides = fmt.Sprintf(" with environment overrides: %s", environmentOverrides)
}
state := ""
if len(stateManifests) > 0 {
state = fmt.Sprintf(" with state from %v", stateManifests)
}
log.Printf("Assembling %v from `%s`%s%s%s", elaborateManifests, manifestFilename,
parametersFrom, overrides, state)
}
environment, err := util.ParseKvList(environmentOverrides)
if err != nil {
log.Fatalf("Unable to parse environment settings `%s`: %v", environmentOverrides, err)
}
wellKnown, err := manifest.GetWellKnownParametersManifest()
if err != nil {
log.Printf("No well-known parameters loaded: %v", err)
wellKnown = &manifest.WellKnownParametersManifest{}
}
wellKnownKV := make(map[string]manifest.Parameter)
for _, known := range wellKnown.Parameters {
wellKnownKV[known.Name] = known
}
var st *state.StateManifest
if len(stateManifests) > 0 {
st = state.MustParseStateFiles(stateManifests)
}
extraKubernetesParams := func(elaborated manifest.Manifest) []manifest.Parameter {
if st != nil && util.ContainsAny(elaborated.Requires, []string{"kubernetes", "kubectl"}) {
outputs := findKubernetesProvider(st)
if len(outputs) > 0 {
apiParameters := make([]string, 0, len(kube.KubernetesParameters))
for _, output := range outputs {
if util.Contains(kube.KubernetesParameters, output.Name) && !util.Empty(output.Value) {
apiParameters = append(apiParameters, output.Name)
}
}
return manifest.MakeParameters(util.Uniq(apiParameters))
}
}
return nil
}
stackManifest, componentsManifests := elaborate(manifestFilename, parametersFilenames, environment,
wellKnownKV, componentsBaseDir, []string{}, 0, extraKubernetesParams)
if pipe != nil {
metricTags := fmt.Sprintf("stack:%s", stackManifest.Meta.Name)
pipe.Write([]byte(metricTags))
pipe.Close()
}
isApplication := stackManifest.Kind == "application"
if isApplication {
checkApplicationNameClash(stackManifest)
}
platformProvides := util.SplitPaths(explicitProvides)
if len(platformProvides) > 0 {
stackManifest.Requires = connectExplicitProvides(stackManifest.Requires, platformProvides)
sort.Strings(platformProvides)
}
if st != nil {
// we might get in trouble here setting `dns.domain` from Kubernetes state on empty
// `kind: user` parameter with `fromEnv:`
// at least there will be a warning for mismatched values
setValuesFromState(stackManifest.Parameters, st, useStateStackParameters)
stackManifest.Requires = connectStateProvides(stackManifest.Requires, st.Provides)
platformProvides = util.MergeUnique(platformProvides, util.SortedKeys2(st.Provides))
}
if len(platformProvides) > 0 {
stackManifest.Platform.Provides = util.MergeUnique(stackManifest.Platform.Provides, platformProvides)
}
warnNoValue(stackManifest.Parameters)
warnFromEnvValueMismatch(stackManifest.Parameters)
if isApplication {
bare := stackManifest.Lifecycle.Bare
if bare != "" && bare != "allow" {
util.Warn("`lifecycle.bare` specify `%s` but the only value recognized is `allow`", bare)
}
componentsManifests = transformApplicationIntoComponent(stackManifest, componentsManifests)
}
setDefaultLifecycleVerbs(componentsManifests)
guessAndMarkSecrets(stackManifest.Outputs)
for i := range componentsManifests {
guessAndMarkSecrets(componentsManifests[i].Outputs)
}
err = writeStackManifest(elaborateManifests, stackManifest, componentsManifests)
if err != nil {
log.Fatalf("Unable to write: %v", err)
}
}
func elaborate(manifestFilename string, parametersFilenames []string, overrides map[string]string,
wellKnown map[string]manifest.Parameter, componentsBaseDir string,
excludedComponents []string, depth int,
maybeExtraParameters func(manifest.Manifest) []manifest.Parameter) (*manifest.Manifest, []manifest.Manifest) {
stackManifest := parseManifest(manifestFilename)
order, err := manifest.GenerateLifecycleOrder(stackManifest)
if err != nil {
log.Fatal(err)
}
stackManifest.Lifecycle.Order = order
parametersManifests, parametersFilenamesRead := parseParameters(parametersFilenames)
stackBaseDir := util.StripDotDirs(filepath.Dir(manifestFilename))
componentsBaseDirCurrent := componentsBaseDir
if componentsBaseDirCurrent == "" {
componentsBaseDirCurrent = stackBaseDir
}
if config.Debug {
log.Printf("Base directory for sources is `%s`", componentsBaseDirCurrent)
}
componentsManifests, err := manifest.ParseComponentsManifestsWithExclusion(stackManifest.Components, excludedComponents,
stackBaseDir, componentsBaseDirCurrent)
if err != nil {
log.Fatalf("Unable to load component manifest refered from `%s`: %v", manifestFilename, err)
}
checkComponentsNames(stackManifest.Components)
checkLifecycle(stackManifest.Components, stackManifest.Lifecycle)
isApplication := stackManifest.Kind == "application"
fromStack := stackManifest.Meta.FromStack != ""
fromStackName := ""
fromStackManifest := &manifest.Manifest{}
var fromStackComponentsManifests []manifest.Manifest
if fromStack {
if isApplication {
log.Fatalf("Application manifest %s cannot use `fromStack`", manifestFilename)
}
fromStackName = filepath.Base(stackManifest.Meta.FromStack)
fromStackFilename := filepath.Join(stackManifest.Meta.FromStack, "hub.yaml")
fromStackParams := scanParamsFiles(stackManifest.Meta.FromStack)
fromStackExcludedComponents := append(excludedComponents, manifest.ComponentsNamesFromRefs(stackManifest.Components)...)
fromStackManifest, fromStackComponentsManifests = elaborate(fromStackFilename, fromStackParams, overrides,
wellKnown, componentsBaseDir, fromStackExcludedComponents, depth+1, nil)
}
if config.Verbose {
components := "with no sub-components"
if len(stackManifest.Components) > 0 {
components = fmt.Sprintf("with components: %s", strings.Join(stackManifest.Lifecycle.Order, ", "))
}
log.Printf("*** %s %s %s", cases.Title(language.Und).String(stackManifest.Kind), stackManifest.Meta.Name,
components)
}
parameters := unwrapComponentsParameters(componentsManifests)
checkParameters(parameters)
if fromStack {
parameters = append(parameters, fromStackManifest.Parameters) // already flat
}
manifestsParameters := [][]manifest.Parameter{
manifest.FlattenParameters(stackManifest.Parameters, fmt.Sprintf("%s [%s]", stackManifest.Meta.Name, manifestFilename)),
}
manifestsParameters = append(manifestsParameters, unwrapManifestsParameters(parametersManifests, parametersFilenamesRead)...)
checkParameters(manifestsParameters)
var elaborated manifest.Manifest
nComponents := len(componentsManifests)
elaborated.Version = stackManifest.Version
elaborated.Kind = stackManifest.Kind
elaborated.Meta = stackManifest.Meta
elaborated.Meta.FromStack = ""
if fromStack {
elaborated.Meta.Annotations = mergeAnnotations(fromStackManifest.Meta.Annotations, stackManifest.Meta.Annotations)
parentBaseDir := stackManifest.Meta.FromStack
parentComponentsBaseDir := componentsBaseDir
if parentComponentsBaseDir == "" {
parentComponentsBaseDir = parentBaseDir
}
elaborated.Components = mergeComponentsRefs(parentBaseDir, parentComponentsBaseDir,
fromStackManifest.Components, stackManifest.Components)
elaborated.Lifecycle = mergeLifecycle(fromStackManifest.Lifecycle, stackManifest.Lifecycle)
elaborated.Outputs = mergeOutputs(fromStackManifest.Outputs, stackManifest.Outputs)
componentsManifests = mergeComponentsManifests(fromStackComponentsManifests, componentsManifests)
elaborated.Platform.Provides = util.MergeUnique(fromStackManifest.Platform.Provides, stackManifest.Platform.Provides)
} else {
elaborated.Components = stackManifest.Components
elaborated.Lifecycle = stackManifest.Lifecycle
elaborated.Outputs = stackManifest.Outputs
elaborated.Platform.Provides = stackManifest.Platform.Provides
}
parametersManifestsOutputs := unwrapManifestsOutputs(parametersManifests)
if len(parametersManifestsOutputs) > 0 {
elaborated.Outputs = mergeOutputs(elaborated.Outputs, parametersManifestsOutputs)
}
if isApplication {
elaborated.Templates = stackManifest.Templates
}
stackRequires := connectRequires(fromStackName, fromStackManifest.Provides,
stackManifest.Requires, componentsManifests, stackManifest.Lifecycle.Order)
elaborated.Requires = mergeRequires(fromStackManifest.Requires, stackRequires)
elaborated.Provides = mergeProvides(fromStackName, fromStackManifest.Provides,
stackManifest.Provides, componentsManifests)
if maybeExtraParameters != nil {
extra := maybeExtraParameters(elaborated)
if len(extra) > 0 {
parameters = append(parameters, extra)
}
}
parameters = append(parameters, manifestsParameters...)
elaborated.Parameters = mergeParameters(parameters, overrides, wellKnown,
manifest.ComponentsNamesFromRefs(elaborated.Components), nComponents, isApplication)
return &elaborated, componentsManifests
}
func parseManifest(manifestFilename string) *manifest.Manifest {
stackManifest, rest, _, err := manifest.ParseManifest([]string{manifestFilename})
if err != nil {
log.Fatalf("Unable to elaborate %s: %v", manifestFilename, err)
}
if len(rest) > 0 {
util.Warn("Manifest %s contains multiple YAML documents - using first document only", manifestFilename)
}
allowedKinds := []string{"stack", "application"}
if !util.Contains(allowedKinds, stackManifest.Kind) {
util.Warn("Manifest `kind` must be one of %v, found `%s`", allowedKinds, stackManifest.Kind)
}
return stackManifest
}
func parseParameters(parametersFilenames []string) ([]*manifest.ParametersManifest, []string) {
parametersManifests := make([]*manifest.ParametersManifest, 0, len(parametersFilenames))
parametersFilenamesRead := make([]string, 0, len(parametersFilenames))
for _, parametersFilename := range parametersFilenames {
parametersManifest, parametersFilenameRead, err := manifest.ParseParametersManifest(
util.SplitPaths(parametersFilename))
if err != nil {
log.Fatalf("Unable to load parameters %s: %v", parametersFilename, err)
}
parametersManifests = append(parametersManifests, parametersManifest)
parametersFilenamesRead = append(parametersFilenamesRead, parametersFilenameRead)
}
return parametersManifests, parametersFilenamesRead
}
func scanParamsFiles(baseDir string) []string {
params := []string{"params.yaml"}
env := os.Getenv("ENV")
if env != "" {
params = append(params, fmt.Sprintf("params-%s.yaml", env))
}
exists := make([]string, 0, len(params))
for _, filename := range params {
path := filepath.Join(baseDir, filename)
_, err := os.Stat(path)
if err != nil {
if !util.NoSuchFile(err) {
log.Fatalf("Unable to stat `%s`: %v", path, err)
}
} else {
exists = append(exists, path)
}
}
return exists
}
func nameWithoutVersion(name string) string {
if i := strings.Index(name, ":"); i > 0 {
return name[:i]
}
return name
}
func checkApplicationNameClash(manifest *manifest.Manifest) {
name := nameWithoutVersion(manifest.Meta.Name)
if util.Contains(manifest.Lifecycle.Order, name) {
log.Fatalf("Application name `%s` cannot clash with component name", name)
}
}
func checkComponentsNames(componentsManifests []manifest.ComponentRef) {
components := make(map[string]bool)
for _, component := range componentsManifests {
name := manifest.ComponentQualifiedNameFromRef(&component)
_, exist := components[name]
if exist {
log.Fatalf("Duplicate component name `%s` ", name)
}
components[name] = true
}
}
func checkLifecycle(components []manifest.ComponentRef, lifecycle manifest.Lifecycle) {
refs := manifest.ComponentsNamesFromRefs(components)
sorted := make([]string, len(refs))
copy(sorted, refs)
order := make([]string, len(lifecycle.Order))
copy(order, lifecycle.Order)
sort.Strings(sorted)
sort.Strings(order)
if !reflect.DeepEqual(sorted, order) {
lifecycleOrder := "(not specified)"
if len(lifecycle.Order) > 0 {
lifecycleOrder = strings.Join(lifecycle.Order, ", ")
}
log.Fatalf("Components: %s;\n\tdoes not match deployment order: %s",
strings.Join(refs, ", "), lifecycleOrder)
}
// not checking Mandatory and Optional as they could contain components from parent stack
}
func checkParameters(parametersAssorti [][]manifest.Parameter) {
for _, parameters := range parametersAssorti {
for _, parameter := range parameters {
if parameter.Kind != "" && !util.Contains([]string{"user", "tech", "link"}, parameter.Kind) {
util.Warn("Parameter `%s` specify unknown `kind: %s`",
parameter.QName(), parameter.Kind)
}
if parameter.Kind == "link" && util.Empty(parameter.Value) {
util.Warn("Parameter `%s` of kind `link` has no value assigned",
parameter.QName())
}
if parameter.Empty != "" && parameter.Empty != "allow" {
util.Warn("Parameter `%s` specify `empty: %s` but the only value recognized is `allow`",
parameter.QName(), parameter.Empty)
}
}
}
}
func findKubernetesProvider(st *state.StateManifest) []parameters.CapturedOutput {
apiParameters := make([]parameters.CapturedOutput, 0, len(kube.KubernetesParameters))
// first check stack outputs
for _, output := range st.StackOutputs {
name := output.Name
if i := strings.Index(name, ":"); i > 0 && i < len(name)-1 {
name = name[i+1:]
}
if util.Contains(kube.KubernetesParameters, name) {
apiParameters = append(apiParameters,
parameters.CapturedOutput{Name: name, Value: output.Value})
}
}
if len(apiParameters) >= 2 {
return apiParameters
}
// then check components providing `kubernetes`
// it might be `*platform*` though
var providers []string
if st.Provides != nil {
providers = st.Provides["kubernetes"]
}
for _, providerName := range util.MergeUnique(providers, kube.KubernetesDefaultProviders) {
provider, exist := st.Components[providerName]
if exist && provider != nil && len(provider.CapturedOutputs) > 0 {
return provider.CapturedOutputs
}
}
// then it's either `*platform*` or user-supplied parameters
apiParameters = apiParameters[:0]
for _, param := range st.StackParameters {
if util.Contains(kube.KubernetesParameters, param.Name) {
apiParameters = append(apiParameters,
parameters.CapturedOutput{Name: param.Name, Value: param.Value})
}
}
if len(apiParameters) >= 1 {
return apiParameters
}
return nil
}
func setValuesFromState(parameters []manifest.Parameter, st *state.StateManifest, useStateStackParameters bool) {
stateStackOutputs := make(map[string]interface{})
// for apps installed on overlay stack we must look into
// stack parameters to obtain kubernetes credentials
if useStateStackParameters {
for _, parameter := range st.StackParameters {
// should we filter out `link` parameters?
if parameter.Component == "" && !util.Empty(parameter.Value) {
stateStackOutputs[parameter.Name] = parameter.Value
}
}
}
for _, output := range st.StackOutputs {
name := output.Name
if i := strings.Index(name, ":"); i > 0 && i < len(name)-1 {
name = name[i+1:]
}
stateStackOutputs[name] = output.Value
}
kubeOutputs := findKubernetesProvider(st)
for i := range parameters {
parameter := ¶meters[i]
if strings.HasPrefix(parameter.Name, "hub.") {
continue
}
if util.Empty(parameter.Value) {
value, exist := stateStackOutputs[parameter.Name]
if exist {
if parameter.FromEnv == "" {
parameter.Value = value
} else {
if !util.Empty(parameter.Default) {
pDefault := util.String(parameter.Default)
pValue := util.String(value)
if pDefault != pValue {
qName := parameter.QName()
util.Warn("Overwriting empty parameter `%s` `default: %s` with state value `default: %s` (due to `fromEnv: %s`)",
qName,
util.Trim(util.MaybeMaskedValue(config.Trace, qName, pDefault)),
util.Trim(util.MaybeMaskedValue(config.Trace, qName, pValue)),
parameter.FromEnv)
}
}
parameter.Default = value
}
} else {
// a special case for Kubernetes
if len(kubeOutputs) > 0 && util.Contains(kube.KubernetesParameters, parameter.Name) {
for _, output := range kubeOutputs {
if output.Name == parameter.Name {
parameter.Value = output.Value
break
}
}
}
}
}
}
}
func warnNoValue(parameters []manifest.Parameter) {
for _, parameter := range parameters {
if parameter.Value == nil {
who := "Parameter"
noDefault := ""
if parameter.Kind == "user" {
if !util.Empty(parameter.Default) || parameter.FromEnv != "" || parameter.FromFile != "" {
continue
}
who = "User-level parameter"
noDefault = " nor default"
}
util.Warn("%s `%s` has no value%s assigned",
who, parameter.QName(), noDefault)
}
}
}
func warnFromEnvValueMismatch(parameters []manifest.Parameter) {
for _, parameter := range parameters {
if parameter.Kind == "user" && parameter.FromEnv != "" && !util.Empty(parameter.Value) {
paramValue := util.String(parameter.Value)
if envValue, exist := os.LookupEnv(parameter.FromEnv); exist && envValue != paramValue {
qName := parameter.QName()
util.Warn("Parameter `%s` value `%v` does not match value `%s` provided by `fromEnv:` environment variable `%s`",
qName,
util.Trim(util.MaybeMaskedValue(config.Trace, qName, paramValue)),
util.Trim(util.MaybeMaskedValue(config.Trace, qName, envValue)),
parameter.FromEnv)
}
}
}
}
func transformApplicationIntoComponent(stack *manifest.Manifest, components []manifest.Manifest) []manifest.Manifest {
name := nameWithoutVersion(stack.Meta.Name)
stack.Lifecycle.Order = append(stack.Lifecycle.Order, name)
applicationRef := manifest.ComponentRef{
Name: name,
Source: stack.Meta.Source,
}
stack.Components = append(stack.Components, applicationRef)
componentOutputs := make([]manifest.Output, 0, len(stack.Outputs))
stackOutputs := make([]manifest.Output, 0, len(stack.Outputs))
for _, output := range stack.Outputs {
if !util.Empty(output.Value) || output.FromTfVar != "" {
componentOutputs = append(componentOutputs, output)
stackOutput := manifest.Output{
Name: output.Name,
Value: fmt.Sprintf("${%s:%s}", name, output.Name),
Brief: output.Brief,
Description: output.Description,
}
stackOutputs = append(stackOutputs, stackOutput)
} else {
stackOutputs = append(stackOutputs, output)
}
}
componentParameters := make([]manifest.Parameter, 0, len(stack.Parameters))
stackParameters := make([]manifest.Parameter, 0, len(stack.Parameters))
for _, param := range stack.Parameters {
if param.Component == "" || param.Component == name {
componentParameters = append(componentParameters,
manifest.Parameter{
Name: param.Name,
Env: param.Env,
})
}
param.Env = ""
stackParameters = append(stackParameters, param)
}
componentManifest := manifest.Manifest{
Version: stack.Version,
Kind: "component",
Meta: stack.Meta,
Lifecycle: manifest.Lifecycle{
Bare: stack.Lifecycle.Bare,
Verbs: stack.Lifecycle.Verbs,
ReadyConditions: stack.Lifecycle.ReadyConditions,
Requires: stack.Lifecycle.Requires,
Options: stack.Lifecycle.Options,
},
Provides: stack.Provides,
Requires: stack.Requires,
Parameters: componentParameters,
Templates: stack.Templates,
Outputs: componentOutputs,
}
componentManifest.Meta.Name = name
components = append(components, componentManifest)
stack.Outputs = stackOutputs
stack.Parameters = stackParameters
stack.Templates = manifest.TemplateSetup{}
return components
}
func setDefaultLifecycleVerbs(components []manifest.Manifest) {
for i, component := range components {
if len(component.Lifecycle.Verbs) == 0 {
components[i].Lifecycle.Verbs = defaultLifecycleVerbs
}
}
}
func guessAndMarkSecrets(outputs []manifest.Output) {
for i, output := range outputs {
if output.Kind == "" && util.LooksLikeSecret(output.Name) {
outputs[i].Kind = "secret" // TODO guess secret kind, like api sync does?
}
}
}
func unwrapComponentsParameters(componentsManifests []manifest.Manifest) [][]manifest.Parameter {
parameters := make([][]manifest.Parameter, 0, len(componentsManifests))
for _, componentsManifest := range componentsManifests {
for i := range componentsManifest.Parameters {
componentsManifest.Parameters[i].Component = componentsManifest.Meta.Name
}
parameters = append(parameters, manifest.FlattenParameters(componentsManifest.Parameters, componentsManifest.Meta.Name))
}
return parameters
}
func unwrapManifestsParameters(parametersManifests []*manifest.ParametersManifest, parametersFilenames []string) [][]manifest.Parameter {
parameters := make([][]manifest.Parameter, 0, len(parametersManifests))
for i, parametersManifest := range parametersManifests {
parameters = append(parameters, manifest.FlattenParameters(parametersManifest.Parameters, parametersFilenames[i]))
}
return parameters
}
func unwrapManifestsOutputs(parametersManifests []*manifest.ParametersManifest) []manifest.Output {
outputs := make([]manifest.Output, 0)
for _, parametersManifest := range parametersManifests {
outputs = append(outputs, parametersManifest.Outputs...)
}
return outputs
}
func mergeAnnotations(parent, child map[string]string) map[string]string {
if len(parent) == 0 && len(child) == 0 {
return nil
}
if len(parent) == 0 && len(child) != 0 {
return child
}
if len(parent) != 0 && len(child) == 0 {
return parent
}
merged := make(map[string]string)
for k, v := range parent {
merged[k] = v
}
for k, v := range child {
merged[k] = v
}
return merged
}
func mergeParameters(parametersAssorti [][]manifest.Parameter,
overrides map[string]string,
wellKnown map[string]manifest.Parameter,
allComponentsNames []string, nComponents int,
isApplication bool) []manifest.Parameter {
kv := make(map[string]manifest.Parameter)
for docIndex, parameters := range parametersAssorti {
isComponentManifest := docIndex < nComponents
for _, parameter := range parameters {
parameter = enrichParameter(parameter, wellKnown)
parameter = updateKindIfFrom(parameter, isComponentManifest)
if isComponentManifest {
if parameter.Kind == "link" {
util.Warn("Parameter `%s` specify `kind: link` on hub-component.yaml level - this is not supported",
parameter.QName())
}
if parameter.Kind != "user" && util.Empty(parameter.Value) && !util.Empty(parameter.Default) {
util.Warn("Parameter `%s` specify `default:` on hub-component.yaml level - use `value:` instead",
parameter.QName())
}
// parameters from Stack Manifest and Parameters files are a special treat -
// they always go to the top level in elaborated
// component parameter is propagated to Stack Manifest only for kind == user
if parameter.Kind != "user" {
continue
}
}
// below are either hub.yaml / params.yaml top-level parameters or
// kind == user parameters from hub-component.yaml
// TODO global env var support is suspended, decide to resume or remove
if parameter.Env != "" { // && !util.Contains(globalEnvVarsAllowed, parameter.Env) {
if !isComponentManifest {
if !isApplication {
util.WarnOnce("Parameter `%s` specify `env: %s` on hub.yaml / params.yaml level",
parameter.QName(), parameter.Env)
}
} else {
if config.Trace {
log.Printf("User-level parameter `%s` specify `env: %s` on hub-component.yaml level - not propagated to global env",
parameter.QName(), parameter.Env)
}
}
parameter.Env = ""
}
if parameter.Component == "" {
qNames := make([]string, 0, 1+len(allComponentsNames))
qNames = append(qNames, parameter.Name)
for _, componentName := range allComponentsNames {
qNames = append(qNames, manifest.ParameterQualifiedName(parameter.Name, componentName))
}
for i, qName := range qNames {
p, exist := kv[qName]
if !exist {
if i == 0 { // plain parameter name
kv[qName] = parameter
}
} else {
if i != 0 {
currValue := util.String(p.Value)
newValue := util.String(parameter.Value)
if newValue != "" && currValue != "" && newValue != currValue {
util.Warn("Parameter `%s` value `%s` overwritten by a less specific parameter `%s` value `%s`",
qName, currValue, parameter.QName(), newValue)
}
}
kv[qName] = mergeParameter(p, parameter, overrides, false)
}
}
} else {
qName := parameter.QName()
p, exist := kv[qName]
if !exist {
kv[qName] = parameter
} else {
kv[qName] = mergeParameter(p, parameter, overrides, false)
}
}
}
}
return sortedParameters(kv)
}
func updateKindIfFrom(parameter manifest.Parameter, warning bool) manifest.Parameter {
if parameter.FromEnv != "" {
if parameter.Kind == "" {
parameter.Kind = "user"
}
if warning {
util.Warn("Parameter `%s` specify `fromEnv: %s` on hub-component.yaml level",
parameter.QName(), parameter.FromEnv)
}
}
if parameter.FromFile != "" {
if parameter.Kind == "" {
parameter.Kind = "user"
}
if warning {
util.Warn("Parameter `%s` specify `fromFile: %s` on hub-component.yaml level",
parameter.QName(), parameter.FromFile)
}
}
return parameter
}
func enrichParameter(parameter manifest.Parameter, wellKnownKV map[string]manifest.Parameter) manifest.Parameter {
wellKnown, exist := wellKnownKV[parameter.Name]
if !exist {
return parameter
}
return mergeParameter(wellKnown, parameter, nil, true)
}
func sortedParameters(kv map[string]manifest.Parameter) []manifest.Parameter {
names := make([]string, 0, len(kv))
for name := range kv {
names = append(names, name)
}
sort.Strings(names)
out := make([]manifest.Parameter, 0, len(names))
for _, name := range names {
out = append(out, kv[name])
}
return out
}
func mergeParameter(base, over manifest.Parameter, overrides map[string]string,
enrichment bool) manifest.Parameter {
if base.Name != over.Name {
log.Fatalf("Unable to merge parameters: `name` doesn't match\n%+v\n%+v", base, over)
}
// if !(base.Kind == over.Kind || (base.Kind == "tech" && over.Kind == "") || (base.Kind == "" && over.Kind == "tech")) {
// log.Fatalf("Unable to merge parameters: `kind` didn't match:\n\tfrom: %+v\n\tinto: %+v", base, over)
// }
kind := base.Kind
if over.Kind != "" {
if kind == "" || (kind == "tech" && over.Kind == "user") || enrichment {
kind = over.Kind
}
}
brief := mergeField(base.Brief, over.Brief)
description := mergeField(base.Description, over.Description)
env := mergeField(base.Env, over.Env)
fromEnv := mergeField(base.FromEnv, over.FromEnv)
fromFile := mergeField(base.FromFile, over.FromFile)
defaultValue := mergeValue(base.Default, over.Default)
value := mergeValue(base.Value, over.Value)
if fromEnv != "" && overrides != nil {
envValue, exist := overrides[fromEnv]
if exist {
value = envValue
}
}
// TODO process fromFile?
empty := mergeField(base.Empty, over.Empty)
if !util.Empty(value) {
empty = ""
}
merged := manifest.Parameter{
Name: over.Name,
Component: base.Component,
Kind: kind,
Brief: brief,
Description: description,
Default: defaultValue,
Env: env,
FromEnv: fromEnv,
FromFile: fromFile,
Value: value,
Empty: empty,
}
if config.Trace {
log.Printf("Parameters merged:\n\t--- %+v\n\t+++ %+v\n\t=== %+v", base, over, merged)
}
return merged
}
func mergeField(base string, over string) string {
if over != "" {
return over
}
return base
}
func mergeValue(base interface{}, over interface{}) interface{} {
if !util.Empty(over) {
return over
}
return base
}
func connectRequires(parentStackName string, parentStackProvides []string,
stackRequires []string, componentsManifests []manifest.Manifest, order []string) []string {
provides := make(map[string][]string)
addProv := func(name, prov string) {
who, exist := provides[prov]
if !exist {
provides[prov] = []string{name}
} else {
if config.Trace && (!strings.HasPrefix(who[0], "*") || len(who) > 1) {
log.Printf("`%s` already provides `%s`, but component `%s` also provides `%s`",
strings.Join(who, ", "), prov, name, prov)
}
provides[prov] = append(who, name)
}
}
requires := make(map[string][]string)
addReq := func(name, req string) {
by, exist := provides[req]
if exist {
if config.Debug {
log.Printf("Component `%s` requirement `%s` provided by `%s`",
name, req, strings.Join(by, ", "))
}
return
}
who, exist := requires[req]
if !exist {
requires[req] = []string{name}
} else {
requires[req] = append(who, name)
}
}
parentStack := fmt.Sprintf("*%s*", parentStackName)
for _, parentProvide := range parentStackProvides {
addProv(parentStack, parentProvide)
if parentProvide == "kubernetes" {
addProv(parentStack, "kubectl")
}
}
stack := "*stack*"
for _, req := range stackRequires {
addReq(stack, req)
}
components := make(map[string]manifest.Manifest)
for _, component := range componentsManifests {
name := manifest.ComponentQualifiedNameFromMeta(&component.Meta)
components[name] = component
}
for _, name := range order {
component := components[name]
for _, req := range component.Requires {
addReq(name, req)
}
for _, prov := range component.Provides {
addProv(name, prov)
if prov == "kubernetes" {
addProv(name, "kubectl")
}
}
}
if config.Debug && len(requires) > 0 {
log.Print("Stack requires:")
util.PrintDeps(requires)
}
keys := make([]string, 0, len(requires))
for k := range requires {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func connectExplicitProvides(requires []string, provides []string) []string {
genuine := make([]string, 0, len(requires))
for _, r := range requires {
if util.Contains(requirementProvidedByEnvironment, r) || !util.Contains(provides, r) {
genuine = append(genuine, r)
}
}
return genuine
}
func connectStateProvides(requires []string, provides map[string][]string) []string {
genuine := make([]string, 0, len(requires))
for _, r := range requires {
if !util.Contains(requirementProvidedByEnvironment, r) {
if providers, exist := provides[r]; exist {
if !util.Contains(providers, "*environment*") {
continue
}
}
}
genuine = append(genuine, r)
}
return genuine
}
func mergeRequires(parentStackRequires []string, stackRequires []string) []string {
return util.MergeUnique(parentStackRequires, stackRequires)
}
func mergeProvides(parentStackName string, parentProvides []string,
stackProvides []string, componentsManifests []manifest.Manifest) []string {
provides := make(map[string][]string)
add := func(name, prov string) {
who, exist := provides[prov]
if !exist {
provides[prov] = []string{name}
} else {
if config.Trace && (!strings.HasPrefix(who[0], "*") || len(who) > 1) {
log.Printf("`%s` already provides `%s`, but component `%s` also provides `%s`",
strings.Join(who, ", "), prov, name, prov)
}
provides[prov] = append(who, name)
}
}
parentStack := fmt.Sprintf("*%s*", parentStackName)
for _, prov := range parentProvides {
add(parentStack, prov)
}
stack := "*stack*"
for _, prov := range stackProvides {
add(stack, prov)
}
for _, component := range componentsManifests {
name := manifest.ComponentQualifiedNameFromMeta(&component.Meta)
for _, prov := range component.Provides {
add(name, prov)
}
}
if config.Debug && len(provides) > 0 {
log.Print("Stack provides:")
util.PrintDeps(provides)
}
keys := make([]string, 0, len(provides))
for k := range provides {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func mergeLifecycle(parent, child manifest.Lifecycle) manifest.Lifecycle {
return manifest.Lifecycle{
Bare: util.Value(parent.Bare, child.Bare),
Order: mergeOrder(parent.Order, child.Order),
ReadyConditions: mergeReadyCondition(parent.ReadyConditions, child.ReadyConditions),
Verbs: util.MergeUnique(parent.Verbs, child.Verbs),
Mandatory: util.MergeUnique(parent.Mandatory, child.Mandatory),
Optional: util.MergeUnique(parent.Optional, child.Optional),
Requires: mergeRequiresTuning(parent.Requires, child.Requires),
// Options:
}
}
func mergeOrder(parent, child []string) []string {
overridesFromChild := make([]int, 0, len(child))
overridesToParent := make([]int, 0, len(child))
prevOverridesToParent := 0
for i, component := range child {
for j, exist := range parent {
if component == exist {
if j < prevOverridesToParent {
log.Fatalf("Component `%s` must come after `%s` in child stack `lifecycle.order` - as defined by parent stack (fromStack)",
parent[prevOverridesToParent], component)
}
prevOverridesToParent = j
overridesFromChild = append(overridesFromChild, i)
overridesToParent = append(overridesToParent, j)
break
}
}
}
if config.Trace {
log.Printf("Lifecycle order overrides to parent indices: %v", overridesToParent)
log.Printf("Lifecycle order overrides from child indices: %v", overridesFromChild)
}
order := make([]string, 0, len(parent)+len(child))
if len(overridesFromChild) == 0 {
order = append(order, parent...)
order = append(order, child...)
return order
}
relative := func(indices []int) {
prev := 0
off := 0
for i, index := range indices {
indices[i] = index - prev - off
prev = index
off = 1
}
}
relative(overridesToParent)
relative(overridesFromChild)
if config.Trace {
log.Printf("Lifecycle order overrides to parent relative indices: %v", overridesToParent)
log.Printf("Lifecycle order overrides from child relative indices: %v", overridesFromChild)
}
parentBlocks := make([][]string, 0, len(child))
for _, cutAt := range overridesToParent {
parentBlocks = append(parentBlocks, parent[:cutAt])
if cutAt == len(parent)-1 {
parent = []string{}
} else {
parent = parent[cutAt+1:]
}
}
parentBlocks = append(parentBlocks, parent)
if config.Trace {
log.Printf("Lifecycle order overrides parent blocks: %v", parentBlocks)
}
childBlocks := make([][]string, 0, len(child))
for _, cutAt := range overridesFromChild {
childBlocks = append(childBlocks, child[:cutAt+1])
if cutAt == len(child) {
child = []string{}
} else {
child = child[cutAt+1:]
}
}
childBlocks = append(childBlocks, child)
if config.Trace {
log.Printf("Lifecycle order overrides child blocks: %v", childBlocks)
}
for i := range parentBlocks {
order = append(order, parentBlocks[i]...)
order = append(order, childBlocks[i]...)
}
return order
}
func mergeReadyCondition(parent, child []manifest.ReadyCondition) []manifest.ReadyCondition {
cond := make([]manifest.ReadyCondition, 0, len(parent)+len(child))
cond = append(cond, parent...)
cond = append(cond, child...)
return cond
}
func mergeRequiresTuning(parent, child manifest.RequiresTuning) manifest.RequiresTuning {
newestFirst := util.MergeUnique(util.Reverse(child.Optional), util.Reverse(parent.Optional))
merged := make([]string, 0, len(newestFirst))
eraseRequirement := make([]string, 0)
eraseComponent := make([]string, 0)
// go back to front skipping entries that are overriden by newer entries
for _, req := range newestFirst {
skip := false
i := strings.Index(req, ":")
if i == -1 { // plain requirement ie. `vault` which is effectively a `vault:*`
// we have seen a newer `requirement:` spec which means - forget about `requirement` tuning
if util.Contains(eraseRequirement, req) {
continue
}
for _, seen := range merged {
// skip if a fine-grained requirement is defined
if strings.HasPrefix(seen, req+":") {
skip = true
break
}
}
} else {
if req == ":" { // erase everything that is older
break
}
if i > 0 {
if i < len(req)-1 { // requirement:component
component := req[i+1:]
plainReq := req[:i]
if util.Contains(eraseComponent, component) || // seen `:component`
util.Contains(eraseRequirement, plainReq) ||
util.Contains(merged, plainReq) { // a req:* is specified
skip = true
}
} else { // requirement:
// skip all older specs for `requirement`
eraseRequirement = append(eraseRequirement, req[:i])
skip = true
}
} else { // :component
// skip all older specs for `component`
eraseComponent = append(eraseComponent, req[i:])
skip = true
}
}
if !skip {
merged = append(merged, req)
}
}
return manifest.RequiresTuning{Optional: util.Reverse(merged)}
}
func mergeOutputs(parent, child []manifest.Output) []manifest.Output {
outputs := make([]manifest.Output, 0, len(parent)+len(child))
outputs = append(outputs, parent...)
for _, output := range child {
found := false
for i, exist := range outputs {
if output.Name == exist.Name {
found = true
outputs[i] = manifest.Output{
Name: output.Name,
Value: mergeValue(exist.Value, output.Value),
FromTfVar: mergeField(exist.FromTfVar, output.FromTfVar),
Kind: mergeField(exist.Kind, output.Kind),
Brief: mergeField(exist.Brief, output.Brief),
Description: mergeField(exist.Description, output.Description),
}
break
}
}
if !found {
outputs = append(outputs, output)
}
}
return outputs
}
func mergeComponentsRefs(parentBaseDir, componentsBaseDir string,
parent, child []manifest.ComponentRef) []manifest.ComponentRef {
refs := make([]manifest.ComponentRef, 0, len(parent)+len(child))
for _, ref := range parent {
if ref.Source.Dir != "" {
ref.Source.Dir = filepath.Join(parentBaseDir, ref.Source.Dir)
}
if ref.Source.Git.LocalDir != "" {
if !filepath.IsAbs(ref.Source.Git.LocalDir) {
ref.Source.Git.LocalDir = filepath.Join(componentsBaseDir, ref.Source.Git.LocalDir)
}
} else if ref.Source.Git.Remote != "" && parentBaseDir == componentsBaseDir {
ref.Source.Git.LocalDir = filepath.Join(parentBaseDir, manifest.ComponentSourceDirNameFromRef(&ref))
}
refs = append(refs, ref)
}
for _, ref := range child {
found := false
for i, exist := range refs {
if ref.Name == exist.Name {
found = true
refs[i] = ref
break
}
}
if !found {
refs = append(refs, ref)
}
}
return refs
}
func mergeComponentsManifests(parent, child []manifest.Manifest) []manifest.Manifest {
manifests := make([]manifest.Manifest, 0, len(parent)+len(child))
manifests = append(manifests, parent...)
for _, manifest := range child {
found := false
for i, exist := range manifests {
if manifest.Meta.Name == exist.Meta.Name {
found = true
manifests[i] = manifest
break
}
}
if !found {
manifests = append(manifests, manifest)
}
}
return manifests
}
func writeStackManifest(elaborateManifests []string, stackManifest *manifest.Manifest, componentsManifest []manifest.Manifest) error {
elaborateFiles, errs := storage.Check(elaborateManifests, "elaborate")
if len(errs) > 0 {
log.Fatalf("Unable to check elaborate files: %v", util.Errors2(errs...))
}
var yamlBytes bytes.Buffer
yamlDocSeparator := []byte("---\n")
stackManifest.Document = ""
marshaled, err := yaml.Marshal(stackManifest)
if err != nil {
return err
}
yamlBytes.Write(yamlDocSeparator)
written, err := yamlBytes.Write(marshaled)
if err != nil || written != len(marshaled) {
return fmt.Errorf("Buffer write failed %v; wrote %d out of %d bytes", err, len(marshaled), written)
}
for _, componentManifest := range componentsManifest {
componentManifest.Document = ""
marshaled, err := yaml.Marshal(componentManifest)
if err != nil {
return err
}
yamlBytes.Write(yamlDocSeparator)
written, err := yamlBytes.Write(marshaled)
if err != nil || written != len(marshaled) {
return fmt.Errorf("Buffer write failed %v; wrote %d out of %d bytes", err, len(marshaled), written)
}
}
_, errs = storage.Write(yamlBytes.Bytes(), elaborateFiles)
if len(errs) > 0 {
log.Fatalf("Unable to write elaborate: %s", util.Errors2(errs...))
}
return nil
}