cmd/hub/lifecycle/deploy.go (801 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 lifecycle
import (
"fmt"
"io"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
petname "github.com/dustinkirkland/golang-petname"
"github.com/google/uuid"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/epam/hubctl/cmd/hub/config"
"github.com/epam/hubctl/cmd/hub/ext"
"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"
)
const (
HubEnvVarNameComponentName = "HUB_COMPONENT"
HubEnvVarNameComponentDir = "HUB_COMPONENT_DIR"
HubEnvVarNameStackBasedir = "HUB_BASE_DIR"
HubEnvVarNameRandom = "HUB_RANDOM"
SkaffoldKubeContextEnvVarName = "SKAFFOLD_KUBE_CONTEXT"
HubEnvVarHubStackName = "HUB_STACK_NAME"
)
func Execute(request *Request, pipe io.WriteCloser) {
isDeploy := strings.HasPrefix(request.Verb, "deploy")
isUndeploy := strings.HasPrefix(request.Verb, "undeploy")
isSomeComponents := len(request.Components) > 0 || request.OffsetComponent != ""
stackManifest, componentsManifests, chosenManifestFilename, err := manifest.ParseManifest(request.ManifestFilenames)
if err != nil {
log.Fatalf("Unable to %s: %s", request.Verb, err)
}
if pipe != nil {
metricTags := fmt.Sprintf("stack:%s", stackManifest.Meta.Name)
pipe.Write([]byte(metricTags))
pipe.Close()
}
environment, err := util.ParseKvList(request.EnvironmentOverrides)
if err != nil {
log.Fatalf("Unable to parse environment settings `%s`: %v", request.EnvironmentOverrides, err)
}
order, err := manifest.GenerateLifecycleOrder(stackManifest)
if err != nil {
log.Fatal(err)
}
stackManifest.Lifecycle.Order = order
if config.Verbose {
printStartBlurb(request, chosenManifestFilename, stackManifest)
}
stackBaseDir := util.Basedir(request.ManifestFilenames)
componentsBaseDir := request.ComponentsBaseDir
if componentsBaseDir == "" {
componentsBaseDir = stackBaseDir
}
components := stackManifest.Components
checkComponentsManifests(components, componentsManifests)
checkLifecycleOrder(components, stackManifest.Lifecycle)
checkLifecycleRequires(components, stackManifest.Lifecycle.Requires)
checkComponentsDepends(components, stackManifest.Lifecycle.Order)
manifest.CheckComponentsExist(components, append(request.Components, request.OffsetComponent, request.LimitComponent)...)
optionalRequires := parseRequiresTunning(stackManifest.Lifecycle.Requires)
requiresOfOptionalComponents := calculateRequiresOfOptionalComponents(componentsManifests, &stackManifest.Lifecycle, stackManifest.Requires)
stackRequires := maybeOmitCloudRequires(stackManifest.Requires, request.EnabledClouds)
provides := checkStackRequires(stackRequires, optionalRequires, requiresOfOptionalComponents)
mergePlatformProvides(provides, stackManifest.Platform.Provides)
if config.Debug && len(provides) > 0 {
log.Print("Requirements provided by:")
util.PrintDeps(provides)
}
osEnv, err := initOsEnv(request.OsEnvironmentMode)
if err != nil {
log.Fatalf("Unable to parse OS environment setup: %v", err)
}
defer util.Done()
var stateManifest *state.StateManifest
var operationsHistory []state.LifecycleOperation
stateUpdater := func(interface{}) {}
var operationLogId string
if len(request.StateFilenames) > 0 {
stateFiles, errs := storage.Check(request.StateFilenames, "state")
if len(errs) > 0 {
util.MaybeFatalf("Unable to check state files: %s", util.Errors2(errs...))
}
storage.EnsureNoLockFiles(stateFiles)
parsed, err := state.ParseState(stateFiles)
if isUndeploy || isSomeComponents {
if err != nil {
if err != os.ErrNotExist {
log.Fatalf("Failed to read %v state files: %v", request.StateFilenames, err)
}
if isSomeComponents {
comps := request.OffsetComponent
if comps == "" {
comps = strings.Join(request.Components, ", ")
}
util.MaybeFatalf("Component `%s` is specified but failed to read %v state file(s): %v",
comps, request.StateFilenames, err)
}
} else {
stateManifest = parsed
}
} else { // full deploy copy state oplog if possible
if err == nil && parsed != nil {
if config.Debug {
log.Print("Preserving operations history loaded from existing state")
}
operationsHistory = parsed.Operations
if parsed.Meta.Name != "" && parsed.Meta.Name != stackManifest.Meta.Name {
util.Warn("State meta.name = `%s` does not match elaborate meta.name = `%s`",
parsed.Meta.Name, stackManifest.Meta.Name)
}
stateManifest = parsed
}
}
var syncer func(*state.StateManifest)
// TODO sync status if no state manifest on undeploy
if request.SyncStackInstance && request.StackInstance != "" {
syncer = hubSyncer(request)
}
stateUpdater = state.InitWriter(stateFiles, syncer)
u, err := uuid.NewRandom()
if err != nil {
log.Fatalf("Unable to generate operation Id random v4 UUID: %v", err)
}
operationLogId = u.String()
}
deploymentIdParameterName := "hub.deploymentId"
deploymentId := ""
if stateManifest != nil {
for _, p := range stateManifest.StackParameters {
if p.Name == deploymentIdParameterName {
deploymentId = util.String(p.Value)
break
}
}
}
if deploymentId == "" {
u, err := uuid.NewRandom()
if err != nil {
log.Fatalf("Unable to generate `hub.deploymentId` random v4 UUID: %v", err)
}
deploymentId = u.String()
}
stackNameParameterName := "hub.stackName"
stackName := ""
if stateManifest != nil {
for _, p := range stateManifest.StackParameters {
if p.Name == stackNameParameterName {
stackName = util.String(p.Value)
break
}
}
}
if stackName == "" {
stackName = os.Getenv(HubEnvVarHubStackName)
if stackName == "" {
suffix := rand.Intn(1000) + 1
name := petname.Generate(2, "-")
stackName = fmt.Sprintf("%s-%d", name, suffix)
}
}
extraExpansionValues := []manifest.Parameter{
{Name: deploymentIdParameterName, Value: deploymentId},
{Name: stackNameParameterName, Value: stackName},
}
// TODO state file has user-level parameters for undeploy operation
// should we just go with the state values if we cannot lock all parameters properly?
stackParameters, errs := parameters.LockParameters(
manifest.FlattenParameters(stackManifest.Parameters, chosenManifestFilename),
extraExpansionValues,
func(parameter manifest.Parameter) (interface{}, error) {
return AskParameter(parameter, environment,
request.Environment, request.StackInstance, request.Application,
isDeploy)
})
if len(errs) > 0 {
log.Fatalf("Failed to lock stack parameters:\n\t%s", util.Errors("\n\t", errs...))
}
allOutputs := make(parameters.CapturedOutputs)
if stateManifest != nil {
checkStateMatch(stateManifest, stackManifest, stackParameters)
state.MergeParsedStateParametersAndProvides(stateManifest, stackParameters, provides)
if isUndeploy && !isSomeComponents {
state.MergeParsedStateOutputs(stateManifest,
"", []string{}, stackManifest.Lifecycle.Order, false,
allOutputs)
}
}
if stateManifest == nil && isDeploy {
stateManifest = &state.StateManifest{
Meta: state.Metadata{
Kind: stackManifest.Kind,
Name: stackManifest.Meta.Name,
},
Operations: operationsHistory,
}
}
addLockedParameter(stackParameters, deploymentIdParameterName, "DEPLOYMENT_ID", deploymentId)
addLockedParameter(stackParameters, stackNameParameterName, "STACK_NAME", stackName)
stackParametersNoLinks := parameters.ParametersWithoutLinks(stackParameters)
order = stackManifest.Lifecycle.Order
if stateManifest != nil {
stateManifest.Lifecycle.Order = order
}
offsetGuessed := false
if isUndeploy {
order = util.Reverse(order)
// on undeploy, guess which component failed to deploy and start undeploy from it
if request.GuessComponent && stateManifest != nil && !isSomeComponents {
for i, component := range order {
if _, exist := stateManifest.Components[component]; exist {
if i == 0 {
break
}
if config.Verbose {
log.Printf("State file has a state for `%[1]s` - setting `--offset %[1]s`", component)
}
request.OffsetComponent = component
offsetGuessed = true
break
}
}
}
}
offsetComponentIndex := util.Index(order, request.OffsetComponent)
limitComponentIndex := util.Index(order, request.LimitComponent)
if offsetComponentIndex >= 0 && limitComponentIndex >= 0 &&
limitComponentIndex < offsetComponentIndex && !offsetGuessed {
log.Fatalf("Specified --limit %s (%d) is before specified --offset %s (%d) in component order",
request.LimitComponent, limitComponentIndex, request.OffsetComponent, offsetComponentIndex)
}
skipComponent := func(i int, name string) bool {
return (len(request.Components) > 0 && !util.Contains(request.Components, name)) ||
(offsetComponentIndex >= 0 && i < offsetComponentIndex) ||
(limitComponentIndex >= 0 && i > limitComponentIndex)
}
checkComponentsSourcesExist(order, components, stackBaseDir, componentsBaseDir, skipComponent)
checkLifecycleVerbs(order, components, componentsManifests, stackManifest.Lifecycle.Verbs, stackBaseDir, componentsBaseDir, skipComponent)
failedComponents := make([]string, 0)
// TODO handle ^C interrupt to update op log and stack status
// or expiry by time and set to `interrupted`
if stateManifest != nil {
stateManifest = state.UpdateOperation(stateManifest, operationLogId, request.Verb, "in-progress",
map[string]interface{}{"args": os.Args})
}
ctx := watchInterrupt()
NEXT_COMPONENT:
for componentIndex, componentName := range order {
if skipComponent(componentIndex, componentName) {
if config.Debug {
log.Printf("Skip %s", componentName)
}
continue
}
if config.Verbose {
log.Printf(util.HighlightColor("%s ***%s*** (%d/%d)"), maybeTestVerb(request.Verb, request.DryRun),
componentName, componentIndex+1, len(components))
}
component := manifest.ComponentRefByName(components, componentName)
componentManifest := manifest.ComponentManifestByRef(componentsManifests, component)
if stateManifest != nil && (componentIndex == offsetComponentIndex || len(request.Components) > 0) {
if len(request.Components) > 0 {
allOutputs = make(parameters.CapturedOutputs)
}
state.MergeParsedStateOutputs(stateManifest,
componentName, component.Depends, stackManifest.Lifecycle.Order, isDeploy,
allOutputs)
}
var updateStateComponentFailed func(string, bool)
if stateManifest != nil {
stateManifest = state.UpdateComponentStartTimestamp(stateManifest, componentName)
updateStateComponentFailed = func(msg string, final bool) {
stateManifest = state.UpdateComponentStatus(stateManifest, componentName, &componentManifest.Meta, "error", msg)
stateManifest = state.UpdatePhase(stateManifest, operationLogId, componentName, "error")
// Erasing provides of a failed component on redeploy has undesirable effect on undeploy, for example:
// Kubernetes Terraform failed safely - failed to download plugin, or failed to add minor resource - leaving
// Kubernetes fully operation, yet the `kubernetes` capability is removed from stack provides. Such stack
// will fail to undeploy due to components being unable to satisfy the [kubernetes] requirement, ie.
// requiring deploy to undeploy, which is dumb.
// eraseProvides(provides, componentName)
stateManifest.Provides = noEnvironmentProvides(provides)
if !config.Force && !optionalComponent(&stackManifest.Lifecycle, componentName) {
stateManifest = state.UpdateStackStatus(stateManifest, "incomplete", msg)
}
if final {
stateManifest = state.UpdateOperation(stateManifest, operationLogId, request.Verb, "error", nil)
}
stateUpdater(stateManifest)
}
}
if isDeploy && len(component.Depends) > 0 {
failed := make([]string, 0, len(component.Depends))
for _, dependency := range component.Depends {
if util.Contains(failedComponents, dependency) {
failed = append(failed, dependency)
}
}
if len(failed) > 0 {
maybeFatalIfMandatory(&stackManifest.Lifecycle, componentName,
fmt.Sprintf("Component `%s` failed to %s: depends on failed optional component `%s`",
componentName, request.Verb, strings.Join(failed, ", ")),
updateStateComponentFailed)
failedComponents = append(failedComponents, componentName)
continue NEXT_COMPONENT
}
}
expandedComponentParameters, expansionErrs := parameters.ExpandParameters(componentName, componentManifest.Meta.Kind, component.Depends,
stackParameters, allOutputs,
manifest.FlattenParameters(componentManifest.Parameters, componentManifest.Meta.Name))
expandedComponentParameters = addHubProvides(expandedComponentParameters, provides)
allParameters := parameters.MergeParameters(stackParametersNoLinks, expandedComponentParameters)
optionalParametersFalse := calculateOptionalFalseParameters(componentName, allParameters, optionalRequires)
if len(optionalParametersFalse) > 0 {
log.Printf("Surprisingly, let skip `%s` due to optional parameter %v evaluated to false", componentName, optionalParametersFalse)
if stateManifest != nil {
stateManifest = state.EraseComponentEmptyState(stateManifest, componentName)
}
continue NEXT_COMPONENT
}
if len(expansionErrs) > 0 {
log.Printf("Component `%s` failed to %s", componentName, request.Verb)
maybeFatalIfMandatory(&stackManifest.Lifecycle, componentName,
fmt.Sprintf("Component `%s` parameters expansion failed:\n\t%s",
componentName, util.Errors("\n\t", expansionErrs...)),
updateStateComponentFailed)
failedComponents = append(failedComponents, componentName)
continue NEXT_COMPONENT
}
componentParameters := parameters.MergeParameters(make(parameters.LockedParameters), expandedComponentParameters)
if optionalNotProvided, err := prepareComponentRequires(provides, componentManifest, allParameters, allOutputs, optionalRequires, request.EnabledClouds); len(optionalNotProvided) > 0 || err != nil {
if err != nil {
if request.Verb == "undeploy" {
// proceed without --force set to handle required component (depends on) being already undeployed via --component
util.Warn("%v", err)
} else {
maybeFatalIfMandatory(&stackManifest.Lifecycle, componentName, fmt.Sprintf("%v", err), updateStateComponentFailed)
continue NEXT_COMPONENT
}
}
if len(optionalNotProvided) > 0 {
log.Printf("Skip %s due to unsatisfied optional requirements %v", componentName, optionalNotProvided)
// there will be a gap in state file but `deploy -c` will be able to find some state from
// a preceding component
continue NEXT_COMPONENT
}
}
if stateManifest != nil {
if isDeploy {
stateManifest = state.UpdateState(stateManifest, componentName,
stackParameters, expandedComponentParameters,
nil, allOutputs, stackManifest.Outputs,
noEnvironmentProvides(provides),
false)
}
status := fmt.Sprintf("%sing", request.Verb)
stateManifest = state.UpdateComponentStatus(stateManifest, componentName, &componentManifest.Meta, status, "")
stateManifest = state.UpdateStackStatus(stateManifest, status, "")
stateManifest = state.UpdatePhase(stateManifest, operationLogId, componentName, "in-progress")
stateUpdater(stateManifest)
stateUpdater("sync")
}
randomSize := 128
if opts := componentManifest.Lifecycle.Options; opts != nil {
if rnd := opts.Random; rnd != nil && rnd.Bytes > 0 {
randomSize = rnd.Bytes
}
}
randomStr, random, err := util.Random(randomSize)
if err != nil {
util.Warn("Unable to set %s: %v", HubEnvVarNameRandom, err)
}
componentDir := manifest.ComponentSourceDirFromRef(component, stackBaseDir, componentsBaseDir)
verb := maybeTestVerb(request.Verb, request.DryRun)
preHookVerb := fmt.Sprintf("pre-%s", verb)
stdout, stderr, err := fireHooks(preHookVerb, stackBaseDir, component, componentParameters, osEnv)
if err != nil {
if stateManifest != nil && request.WriteOplogToStateOnError {
stateManifest = state.AppendOperationLog(stateManifest, operationLogId,
fmt.Sprintf("%v%s", err, formatStdoutStderr(stdout, stderr)))
}
util.MaybeFatalf2(updateStateComponentFailed, "One of %s hooks failed. See logs", preHookVerb)
}
stdout, stderr, err = delegate(verb,
component, componentManifest, componentParameters,
componentDir, osEnv, randomStr, stackBaseDir)
var rawOutputs parameters.RawOutputs
if err != nil {
if stateManifest != nil && request.WriteOplogToStateOnError {
stateManifest = state.AppendOperationLog(stateManifest, operationLogId,
fmt.Sprintf("%v%s", err, formatStdoutStderr(stdout, stderr)))
}
maybeFatalIfMandatory(&stackManifest.Lifecycle, componentName,
fmt.Sprintf("Component `%s` failed to %s: %v", componentName, request.Verb, err),
updateStateComponentFailed)
failedComponents = append(failedComponents, componentName)
} else if isDeploy {
rawOutputsCaptured, componentOutputs, dynamicProvides, errs := captureOutputs(componentName, componentDir, componentManifest, componentParameters,
stdout, random)
rawOutputs = rawOutputsCaptured
if len(errs) > 0 {
log.Printf("Component `%s` failed to %s", componentName, request.Verb)
maybeFatalIfMandatory(&stackManifest.Lifecycle, componentName,
fmt.Sprintf("Component `%s` outputs capture failed:\n\t%s",
componentName, util.Errors("\n\t", errs...)),
updateStateComponentFailed)
failedComponents = append(failedComponents, componentName)
}
if len(componentOutputs) > 0 &&
(config.Debug || (config.Verbose && len(request.Components) == 1)) {
log.Print("Component outputs:")
parameters.PrintCapturedOutputs(componentOutputs)
}
parameters.MergeOutputs(allOutputs, componentOutputs)
if request.GitOutputs {
if config.Debug || (config.Verbose && request.GitOutputsStatus) {
log.Print("Checking Git status")
}
git := gitOutputs(componentName, componentDir, request.GitOutputsStatus)
if config.Debug && len(git) > 0 {
log.Print("Implicit Git outputs added:")
parameters.PrintCapturedOutputs(git)
}
parameters.MergeOutputs(allOutputs, git)
}
componentComplexOutputs := captureProvides(component, stackBaseDir, componentsBaseDir,
componentManifest.Provides, componentOutputs)
if len(componentComplexOutputs) > 0 &&
(config.Debug || (config.Verbose && len(request.Components) == 1)) {
log.Print("Component additional outputs captured:")
parameters.PrintCapturedOutputs(componentComplexOutputs)
}
parameters.MergeOutputs(allOutputs, componentComplexOutputs)
mergeProvides(provides, componentName, append(dynamicProvides, componentManifest.Provides...), componentOutputs)
} else if isUndeploy {
eraseProvides(provides, componentName)
if stateManifest != nil {
stateManifest.Provides = noEnvironmentProvides(provides)
}
}
postHookVerb := fmt.Sprintf("post-%s", verb)
stdout, stderr, err = fireHooks(postHookVerb, stackBaseDir, component, componentParameters, osEnv)
if err != nil {
if stateManifest != nil && request.WriteOplogToStateOnError {
stateManifest = state.AppendOperationLog(stateManifest, operationLogId,
fmt.Sprintf("%v%s", err, formatStdoutStderr(stdout, stderr)))
}
util.MaybeFatalf2(updateStateComponentFailed, "One of %s hooks failed. See logs", postHookVerb)
}
if ctx.Err() != nil {
break
}
if stateManifest != nil && isDeploy {
final := componentIndex == len(order)-1 || (len(request.Components) > 0 && request.LoadFinalState)
stateManifest = state.UpdateState(stateManifest, componentName,
stackParameters, expandedComponentParameters,
rawOutputs, allOutputs, stackManifest.Outputs,
noEnvironmentProvides(provides), final)
}
if err == nil && isDeploy {
err = waitForReadyConditions(ctx, componentManifest.Lifecycle.ReadyConditions, componentParameters, allOutputs, component.Depends)
if err != nil {
log.Printf("Component `%s` failed to %s", componentName, request.Verb)
maybeFatalIfMandatory(&stackManifest.Lifecycle, componentName,
fmt.Sprintf("Component `%s` ready condition failed: %v", componentName, err),
updateStateComponentFailed)
failedComponents = append(failedComponents, componentName)
}
}
if err == nil && config.Verbose {
log.Printf("Component `%s` completed %s", componentName, request.Verb)
}
if stateManifest != nil {
if !util.Contains(failedComponents, componentName) {
stateManifest = state.UpdateComponentStatus(stateManifest, componentName, &componentManifest.Meta,
fmt.Sprintf("%sed", request.Verb), "")
stateManifest = state.UpdatePhase(stateManifest, operationLogId, componentName, "success")
stateUpdater(stateManifest)
}
}
// end of component cycle
}
stackReadyConditionFailed := false
if isDeploy {
err := waitForReadyConditions(ctx, stackManifest.Lifecycle.ReadyConditions, stackParameters, allOutputs, nil)
if err != nil {
message := fmt.Sprintf("Stack ready condition failed: %v", err)
if stateManifest != nil {
stateManifest = state.UpdateStackStatus(stateManifest, "incomplete", message)
stateManifest = state.UpdateOperation(stateManifest, operationLogId, request.Verb, "error", nil)
stateUpdater(stateManifest)
}
util.MaybeFatalf("%s", message)
stackReadyConditionFailed = true
}
}
if stateManifest != nil {
if !stackReadyConditionFailed {
status, message := calculateStackStatus(stackManifest, stateManifest, request.Verb)
stateManifest = state.UpdateStackStatus(stateManifest, status, message)
stateManifest = state.UpdateOperation(stateManifest, operationLogId, request.Verb, "success", nil)
stateUpdater(stateManifest)
}
stateUpdater("sync")
}
var stackOutputs []parameters.ExpandedOutput
if stateManifest != nil {
stackOutputs = stateManifest.StackOutputs
} else {
stackOutputs = parameters.ExpandRequestedOutputs(stackParameters, allOutputs, stackManifest.Outputs, false)
}
if config.Verbose {
if isDeploy {
provides2 := noEnvironmentProvides(provides)
if len(provides2) > 0 {
log.Printf("%s provides:", cases.Title(language.Und).String(stackManifest.Kind))
util.PrintDeps(provides2)
}
printStackOutputs(stackOutputs)
}
}
if config.Verbose {
printEndBlurb(request, stackManifest)
}
}
func optionalComponent(lifecycle *manifest.Lifecycle, componentName string) bool {
return (len(lifecycle.Mandatory) > 0 && !util.Contains(lifecycle.Mandatory, componentName)) ||
util.Contains(lifecycle.Optional, componentName)
}
func maybeFatalIfMandatory(lifecycle *manifest.Lifecycle, componentName string, msg string, cleanup func(string, bool)) {
if optionalComponent(lifecycle, componentName) {
util.Warn("%s", msg)
if cleanup != nil {
cleanup(msg, false)
}
} else {
util.MaybeFatalf2(cleanup, "%s", msg)
}
}
func addLockedParameter(params parameters.LockedParameters, name, env, value string) {
if p, exist := params[name]; !exist || util.Empty(p.Value) {
if exist && p.Env != "" {
env = p.Env
}
if config.Debug {
log.Printf("Adding implicit parameter %s = `%s` (env: %s)", name, value, env)
}
params[name] = parameters.LockedParameter{Name: name, Value: value, Env: env}
}
}
func addLockedParameter2(params []parameters.LockedParameter, name, env, value string) []parameters.LockedParameter {
for i := range params {
p := ¶ms[i]
if p.Name == name {
if p.Env == "" {
p.Env = env
}
if util.Empty(p.Value) {
p.Value = value
}
return params
}
}
if config.Debug {
log.Printf("Adding implicit parameter %s = `%s` (env: %s)", name, value, env)
}
return append(params, parameters.LockedParameter{Name: name, Value: value, Env: env})
}
func addHubProvides(params []parameters.LockedParameter, provides map[string][]string) []parameters.LockedParameter {
return addLockedParameter2(params, "hub.provides", "HUB_PROVIDES", strings.Join(util.SortedKeys2(provides), " "))
}
func maybeTestVerb(verb string, test bool) string {
if test {
return verb + "-test"
}
return verb
}
func fireHooks(trigger string, stackBaseDir string, component *manifest.ComponentRef,
componentParameters parameters.LockedParameters, osEnv []string,
) ([]byte, []byte, error) {
hooks := findHooksByTrigger(trigger, component.Hooks)
if len(hooks) == 0 {
return nil, nil, nil
}
extensions := ext.GetExtensionLocations()
paths := filepath.SplitList(os.Getenv("PATH"))
searchDirs := []string{
component.Source.Dir,
stackBaseDir,
filepath.Join(stackBaseDir, "bin"),
filepath.Join(stackBaseDir, ".hub"),
filepath.Join(stackBaseDir, ".hub", "bin"),
}
searchDirs = append(searchDirs, extensions...)
searchDirs = append(searchDirs, paths...)
for _, hook := range hooks {
var err error
script, err := findScript(hook.File, searchDirs...)
if err != nil || script == "" {
// script file not found
log.Printf("Error: unable to locate hook script `%s:` %v", hook.File, err)
return nil, nil, err
}
log.Printf("Running %s script: %s", trigger, util.HighlightColor(hook.File))
if config.Verbose && len(componentParameters) > 0 {
log.Print("Environment:")
parameters.PrintLockedParameters(componentParameters)
}
stdout, stderr, err := delegateHook(script, stackBaseDir, component, componentParameters, osEnv)
if err != nil {
// script found but can't be executed because of permissions
if strings.Contains(err.Error(), "permission denied") {
log.Print("Error: permission denied (file not executable)")
return stdout, stderr, err
} else if hook.Error == "ignore" {
log.Printf("Error ignored: %s", err.Error())
continue
}
log.Printf("Error: %s", err.Error())
return stdout, stderr, err
}
}
return nil, nil, nil
}
func findHooksByTrigger(trigger string, hooks []manifest.Hook) []manifest.Hook {
matches := make([]manifest.Hook, 0)
for i := range hooks {
hook := hooks[i]
for t := range hook.Triggers {
glob := hook.Triggers[t]
matched, _ := filepath.Match(glob, trigger)
if matched {
matches = append(matches, hook)
break
}
}
}
return matches
}
func findScript(script string, dirs ...string) (string, error) {
var result string
// special case for absolute filenames - just check if it exists
if filepath.IsAbs(script) {
dir, f := filepath.Split(script)
found, err := probeScript(dir, f)
if err != nil {
return "", err
}
if found == "" {
return "", os.ErrNotExist
}
result = filepath.Join(dir, found)
} else {
var search []string
for _, dir := range dirs {
search = append(search, filepath.Join(dir, script))
}
for _, path := range search {
dir, f := filepath.Split(path)
found, _ := probeScript(dir, f)
if found != "" {
result = filepath.Join(dir, found)
break
}
}
}
if result == "" {
return "", os.ErrNotExist
}
if !filepath.IsAbs(result) {
var err error
result, err = filepath.Abs(result)
if err != nil {
return "", err
}
}
return result, nil
}
func delegateHook(script string, stackDir string, component *manifest.ComponentRef, componentParameters parameters.LockedParameters, osEnv []string) ([]byte, []byte, error) {
var err error
componentDir := component.Source.Dir
// components usually stored as relative paths
if !filepath.IsAbs(componentDir) {
componentDir = filepath.Join(stackDir, componentDir)
componentDir, err = filepath.Abs(componentDir)
if err != nil {
return nil, nil, err
}
}
processEnv := parametersInEnv(component, componentParameters, stackDir)
command := &exec.Cmd{
Path: script,
Dir: componentDir,
Env: mergeOsEnviron(osEnv, processEnv),
}
return execImplementation(command, false, true)
}
func delegate(verb string, component *manifest.ComponentRef, componentManifest *manifest.Manifest,
componentParameters parameters.LockedParameters,
dir string, osEnv []string, random string, baseDir string,
) ([]byte, []byte, error) {
if config.Debug && len(componentParameters) > 0 {
log.Print("Component parameters:")
parameters.PrintLockedParameters(componentParameters)
}
componentName := manifest.ComponentQualifiedNameFromRef(component)
errs := processTemplates(component, &componentManifest.Templates, componentParameters, nil, dir)
if len(errs) > 0 {
return nil, nil, fmt.Errorf("Failed to process templates:\n\t%s", util.Errors("\n\t", errs...))
}
processEnv := parametersInEnv(component, componentParameters, baseDir)
impl, err := findImplementation(dir, verb, componentManifest)
if err != nil {
if componentManifest.Lifecycle.Bare == "allow" {
if config.Verbose {
log.Printf("Skip `%s`: %v", componentName, err)
}
return nil, nil, nil
}
return nil, nil, err
}
skaffoldEnvironment := skaffoldEnv(impl, processEnv)
impl.Env = mergeOsEnviron(osEnv, processEnv, randomEnv(random), skaffoldEnvironment)
if config.Debug && len(processEnv) > 0 {
log.Print("Component environment:")
printEnvironment(processEnv)
printEnvironment(skaffoldEnvironment)
if config.Trace {
log.Print("Full process environment:")
printEnvironment(impl.Env)
}
}
stdout, stderr, err := execImplementation(impl, false, true)
return stdout, stderr, err
}
func randomEnv(random string) []string {
if random == "" {
return nil
}
return []string{fmt.Sprintf("%s=%s", HubEnvVarNameRandom, random)}
}
func skaffoldEnv(impl *exec.Cmd, processEnv []string) []string {
if len(impl.Args) > 0 && impl.Args[0] == "skaffold" {
for _, envEntry := range processEnv {
if strings.HasPrefix(envEntry, SkaffoldKubeContextEnvVarName+"=") {
return nil
}
}
for _, envEntry := range processEnv {
for _, domainVar := range []string{"DOMAIN_NAME", "DOMAIN"} {
if strings.HasPrefix(envEntry, domainVar+"=") {
kv := strings.SplitN(envEntry, "=", 2)
return []string{fmt.Sprintf("%s=%s", SkaffoldKubeContextEnvVarName, kv[1])}
}
}
}
}
return nil
}
func parametersInEnv(component *manifest.ComponentRef, componentParameters parameters.LockedParameters, baseDir string) []string {
envParameters := make([]string, 0)
envSetBy := make(map[string]string)
envValue := make(map[string]string)
for _, parameter := range componentParameters {
if parameter.Env == "" {
continue
}
currentValue := strings.TrimSpace(util.MaybeJson(parameter.Value))
envParameters = append(envParameters, fmt.Sprintf("%s=%s", parameter.Env, currentValue))
name := parameter.QName()
setBy, exist := envSetBy[parameter.Env]
if exist {
prevValue := envValue[parameter.Env]
if prevValue != currentValue { /*||
(!strings.HasPrefix(setBy, name+"|") && !strings.HasPrefix(name, setBy+"|")) {*/
util.Warn("Env var `%s=%s` set by `%s` overriden by `%s` to `%s`",
parameter.Env, prevValue, setBy, name, currentValue)
}
}
envSetBy[parameter.Env] = name
envValue[parameter.Env] = currentValue
}
envComponentName := "COMPONENT_NAME"
if setBy, exist := envSetBy[envComponentName]; !exist {
envParameters = append(envParameters, fmt.Sprintf("%s=%s", envComponentName, component.Name))
} else if config.Debug && envValue[envComponentName] != component.Name {
log.Printf("Component `%s` env var `%s=%s` set by `%s`",
component.Name, envComponentName, envValue[envComponentName], setBy)
}
if !filepath.IsAbs(baseDir) {
t, err := filepath.Abs(baseDir)
if err != nil {
util.Warn("Unable to take absolute path for %s (%s): %v", HubEnvVarNameStackBasedir, baseDir, err)
} else {
baseDir = t
}
}
var componentDir string
if filepath.IsAbs(component.Source.Dir) {
componentDir = component.Source.Dir
} else {
componentDir = filepath.Join(baseDir, component.Source.Dir)
if !filepath.IsAbs(componentDir) {
t, err := filepath.Abs(componentDir)
if err != nil {
util.Warn("Unable to set absolute path for HUB_COMPONENT_DIR (%s): %v", component.Name, err)
util.Warn("Falling back to %s directory: %s", component.Name, componentDir)
} else {
componentDir = t
}
}
}
// for `hub render`
envParameters = append(envParameters, fmt.Sprintf("%s=%s", HubEnvVarNameComponentName, component.Name))
envParameters = append(envParameters, fmt.Sprintf("%s=%s", HubEnvVarNameComponentDir, componentDir))
envParameters = append(envParameters, fmt.Sprintf("%s=%s", HubEnvVarNameStackBasedir, baseDir))
return mergeOsEnviron(envParameters) // sort
}