github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/render/render.go (about)

     1  package render
     2  
     3  import (
     4  	"context"
     5  	"time"
     6  
     7  	"github.com/go-kit/kit/log"
     8  	"github.com/go-kit/kit/log/level"
     9  	"github.com/mitchellh/cli"
    10  	"github.com/pkg/errors"
    11  	"github.com/replicatedhq/libyaml"
    12  	"github.com/spf13/afero"
    13  
    14  	"github.com/replicatedhq/ship/pkg/api"
    15  	"github.com/replicatedhq/ship/pkg/constants"
    16  	"github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes"
    17  	"github.com/replicatedhq/ship/pkg/lifecycle/render/config"
    18  	pkgplanner "github.com/replicatedhq/ship/pkg/lifecycle/render/planner"
    19  	"github.com/replicatedhq/ship/pkg/state"
    20  )
    21  
    22  var (
    23  	ProgressRead   = daemontypes.StringProgress("render", "reading application release")
    24  	ProgressRender = daemontypes.StringProgress("render", "rendering assets and configuration values")
    25  )
    26  
    27  // A headlessrenderer takes a resolved spec, collects config values, and renders assets
    28  // this was the old plain "renderer", but now its only used in headless workflows, so renaming it for clarity
    29  type headlessrenderer struct {
    30  	Logger         log.Logger
    31  	ConfigResolver config.Resolver
    32  	Planner        pkgplanner.Planner
    33  	StateManager   state.Manager
    34  	Fs             afero.Afero
    35  	UI             cli.Ui
    36  	StatusReceiver daemontypes.StatusReceiver
    37  	Now            func() time.Time
    38  }
    39  
    40  // Execute renders the assets and config
    41  func (r *headlessrenderer) Execute(ctx context.Context, release *api.Release, step *api.Render) error {
    42  	defer r.StatusReceiver.ClearProgress()
    43  
    44  	debug := level.Debug(log.With(r.Logger, "step.type", "render"))
    45  	debug.Log("event", "step.execute")
    46  
    47  	debug.Log("event", "try.load")
    48  	previousState, err := r.StateManager.CachedState()
    49  	if err != nil {
    50  		return err
    51  	}
    52  
    53  	r.StatusReceiver.SetProgress(ProgressRead)
    54  
    55  	debug.Log("event", "resolve.config")
    56  	currentConfig, err := previousState.CurrentConfig()
    57  	if err != nil {
    58  		return errors.Wrap(err, "get current config")
    59  	}
    60  
    61  	templateContext, err := r.ConfigResolver.ResolveConfig(ctx, release, currentConfig)
    62  	if err != nil {
    63  		return errors.Wrap(err, "resolve config")
    64  	}
    65  
    66  	r.StatusReceiver.SetProgress(ProgressRender)
    67  
    68  	// this should probably happen even higher up, like to the validation stage where we assign IDs to lifecycle steps, but moving it up here for now
    69  	if step.Root == "" {
    70  		step.Root = constants.InstallerPrefixPath
    71  	}
    72  
    73  	assets := release.Spec.Assets.V1
    74  	if step.Assets != nil && step.Assets.V1 != nil {
    75  		assets = step.Assets.V1
    76  	}
    77  
    78  	debug.Log("event", "render.plan")
    79  	pln, dests, err := r.Planner.Build(step.Root, assets, release.Spec.Config.V1, release.Metadata, templateContext)
    80  	if err != nil {
    81  		return errors.Wrap(err, "build plan")
    82  	}
    83  
    84  	debug.Log("event", "clean.start")
    85  	// cleanup dests
    86  	err = removeDests(&r.Fs, dests)
    87  	if err != nil {
    88  		return errors.Wrap(err, "clean destination")
    89  	}
    90  
    91  	if step.Root != "." && step.Root != "./" {
    92  		debug.Log("event", "backup.start")
    93  		err = r.backupIfPresent(step.Root)
    94  		if err != nil {
    95  			return errors.Wrapf(err, "backup existing install directory %s", constants.InstallerPrefixPath)
    96  		}
    97  	}
    98  
    99  	debug.Log("event", "execute.plan")
   100  	r.StatusReceiver.SetStepName(ctx, daemontypes.StepNameConfirm)
   101  	err = r.Planner.Execute(ctx, pln)
   102  	if err != nil {
   103  		return errors.Wrap(err, "execute plan")
   104  	}
   105  
   106  	currentConfig, err = previousState.CurrentConfig()
   107  	if err != nil {
   108  		return errors.Wrap(err, "get current config")
   109  	}
   110  
   111  	stateTemplateContext := make(map[string]interface{})
   112  	for _, configGroup := range release.Spec.Config.V1 {
   113  		for _, configItem := range configGroup.Items {
   114  			if valueNotOverriddenByDefault(configItem, templateContext, currentConfig) {
   115  				stateTemplateContext[configItem.Name] = templateContext[configItem.Name]
   116  			}
   117  		}
   118  	}
   119  
   120  	// edge case: empty config section of app yaml,
   121  	// persist data from previous state.json
   122  	if len(release.Spec.Config.V1) == 0 {
   123  		stateTemplateContext = templateContext
   124  	}
   125  
   126  	debug.Log("event", "commit")
   127  	if err := r.StateManager.SerializeConfig(release.Spec.Assets.V1, release.Metadata, stateTemplateContext); err != nil {
   128  		return errors.Wrap(err, "serialize state")
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  func valueNotOverriddenByDefault(item *libyaml.ConfigItem, templateContext map[string]interface{}, savedState map[string]interface{}) bool {
   135  	_, inSavedState := savedState[item.Name] // all values in savedState are non-default values
   136  
   137  	if templateContext[item.Name] == "" {
   138  		if inSavedState && savedState[item.Name] == "" {
   139  			// manually set value: "" in state.json
   140  			return true
   141  		} else {
   142  			// value overridden by default == ""?
   143  			return item.Default != ""
   144  		}
   145  	} else if templateContext[item.Name] == item.Default {
   146  		// the provided value is manually set to the default value
   147  		return inSavedState
   148  	} else {
   149  		// non-empty value != default. cannot have been overridden by default
   150  		return true
   151  	}
   152  }