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

     1  package resolve
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"strconv"
     9  
    10  	"github.com/go-kit/kit/log"
    11  	"github.com/go-kit/kit/log/level"
    12  	"github.com/pkg/errors"
    13  	"github.com/replicatedhq/libyaml"
    14  	"github.com/replicatedhq/ship/pkg/api"
    15  	"github.com/replicatedhq/ship/pkg/templates"
    16  	"github.com/spf13/viper"
    17  )
    18  
    19  func NewRenderer(
    20  	logger log.Logger,
    21  	v *viper.Viper,
    22  	builderBuilder *templates.BuilderBuilder,
    23  ) *APIConfigRenderer {
    24  	return &APIConfigRenderer{
    25  		Logger:         logger,
    26  		Viper:          v,
    27  		BuilderBuilder: builderBuilder,
    28  	}
    29  }
    30  
    31  const MissingRequiredValue = "MISSING_REQUIRED_VALUE"
    32  
    33  // APIConfigRenderer resolves config values via API
    34  type APIConfigRenderer struct {
    35  	Logger         log.Logger
    36  	Viper          *viper.Viper
    37  	BuilderBuilder *templates.BuilderBuilder
    38  }
    39  
    40  type ValidationError struct {
    41  	Message   string `json:"message"`
    42  	Name      string `json:"name"`
    43  	ErrorCode string `json:"error_code"`
    44  }
    45  
    46  func isReadOnly(item *libyaml.ConfigItem) bool {
    47  	if item.ReadOnly {
    48  		return true
    49  	}
    50  
    51  	// "" is an editable type because the default type is "text"
    52  	var EditableItemTypes = map[string]struct{}{
    53  		"":            {},
    54  		"bool":        {},
    55  		"file":        {},
    56  		"password":    {},
    57  		"select":      {},
    58  		"select_many": {},
    59  		"select_one":  {},
    60  		"text":        {},
    61  		"textarea":    {},
    62  	}
    63  
    64  	_, editable := EditableItemTypes[item.Type]
    65  	return !editable
    66  }
    67  
    68  func (r *APIConfigRenderer) shouldOverrideValueWithDefault(item *libyaml.ConfigItem, savedState map[string]interface{}, firstPass bool) bool {
    69  	// resolve config runs before values are saved in interactive mode.
    70  	// this first pass should override any hidden, empty values with
    71  	// non-empty defaults
    72  	if firstPass {
    73  		return item.Hidden && item.Value == "" && item.Default != ""
    74  	}
    75  	// vendor can't override a default with "" in interactive mode
    76  	_, ok := savedState[item.Name]
    77  	return !ok && item.Value == "" && item.Default != ""
    78  }
    79  
    80  func isRequired(item *libyaml.ConfigItem) bool {
    81  	return item.Required
    82  }
    83  
    84  func isEmpty(item *libyaml.ConfigItem) bool {
    85  	return item.Value == "" && item.Default == ""
    86  }
    87  
    88  func isHidden(item *libyaml.ConfigItem) bool {
    89  	return item.Hidden
    90  }
    91  
    92  func deepCopyMap(original map[string]interface{}) (map[string]interface{}, error) {
    93  	var buf bytes.Buffer
    94  	enc := json.NewEncoder(&buf)
    95  	dec := json.NewDecoder(&buf)
    96  	err := enc.Encode(original)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	var updatedValues map[string]interface{}
   101  	err = dec.Decode(&updatedValues)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	return updatedValues, nil
   106  }
   107  
   108  // given a set of input values ('liveValues') and the config ('configGroups') returns a map of configItem names to values, with all config option template functions resolved
   109  func (r *APIConfigRenderer) resolveConfigValuesMap(
   110  	meta api.ReleaseMetadata,
   111  	liveValues map[string]interface{},
   112  	configGroups []libyaml.ConfigGroup,
   113  ) (map[string]interface{}, error) {
   114  	// make a deep copy of the live values map
   115  	updatedValues, err := deepCopyMap(liveValues)
   116  	if err != nil {
   117  		return nil, errors.Wrap(err, "deep copy live values")
   118  	}
   119  
   120  	//recalculate builder with new values
   121  	builder, err := r.BuilderBuilder.FullBuilder(
   122  		meta,
   123  		configGroups,
   124  		updatedValues,
   125  	)
   126  	if err != nil {
   127  		return nil, errors.Wrap(err, "init builder")
   128  	}
   129  
   130  	configItemsByName := make(map[string]*libyaml.ConfigItem)
   131  	for _, configGroup := range configGroups {
   132  		for _, configItem := range configGroup.Items {
   133  			configItemsByName[configItem.Name] = configItem
   134  		}
   135  	}
   136  
   137  	// Build config values in order & add them to the template builder
   138  	deps := depGraph{
   139  		BuilderBuilder: r.BuilderBuilder,
   140  	}
   141  	err = deps.ParseConfigGroup(configGroups)
   142  	if err != nil {
   143  		return nil, errors.Wrap(err, "parse config groups")
   144  	}
   145  	var headNodes []string
   146  
   147  	headNodes, err = deps.GetHeadNodes()
   148  
   149  	for (len(headNodes) > 0) && (err == nil) {
   150  		for _, node := range headNodes {
   151  			deps.ResolveDep(node)
   152  
   153  			configItem := configItemsByName[node]
   154  
   155  			if !isReadOnly(configItem) {
   156  				// if item is editable and the live state is valid, skip the rest of this
   157  				val, ok := updatedValues[node]
   158  				if ok && val != "" {
   159  					continue
   160  				}
   161  			}
   162  
   163  			// build "default" and "value"
   164  			builtDefault, _ := builder.String(configItem.Default)
   165  			builtValue, _ := builder.String(configItem.Value)
   166  
   167  			if builtValue != "" {
   168  				updatedValues[node] = builtValue
   169  			} else {
   170  				updatedValues[node] = builtDefault
   171  			}
   172  		}
   173  
   174  		//recalculate builder with new values
   175  		builder, err = r.BuilderBuilder.FullBuilder(
   176  			meta,
   177  			configGroups,
   178  			updatedValues,
   179  		)
   180  		if err != nil {
   181  			return nil, errors.Wrap(err, "re-init builder")
   182  		}
   183  		headNodes, err = deps.GetHeadNodes()
   184  	}
   185  	if err != nil {
   186  		//dependencies could not be resolved for some reason
   187  		//return the empty config
   188  		//TODO: Better error messaging
   189  		return updatedValues, err
   190  	}
   191  
   192  	return updatedValues, nil
   193  }
   194  
   195  // ResolveConfig will get all the config values specified in the spec, in JSON format
   196  func (r *APIConfigRenderer) ResolveConfig(
   197  	ctx context.Context,
   198  	release *api.Release,
   199  	savedState map[string]interface{},
   200  	liveValues map[string]interface{},
   201  	firstPass bool,
   202  ) ([]libyaml.ConfigGroup, error) {
   203  	resolvedConfig := make([]libyaml.ConfigGroup, 0)
   204  	configCopy, err := r.deepCopyConfig(release.Spec.Config.V1)
   205  	if err != nil {
   206  		return resolvedConfig, errors.Wrap(err, "deep copy config")
   207  	}
   208  
   209  	combinedState, err := deepCopyMap(liveValues)
   210  	if err != nil {
   211  		return resolvedConfig, errors.Wrap(err, "deep copy state")
   212  	}
   213  	for key, val := range savedState {
   214  		if _, ok := combinedState[key]; !ok {
   215  			combinedState[key] = val
   216  		}
   217  	}
   218  
   219  	updatedValues, err := r.resolveConfigValuesMap(release.Metadata, combinedState, configCopy)
   220  
   221  	if err != nil {
   222  		return resolvedConfig, errors.Wrap(err, "resolve configCopy values map")
   223  	}
   224  
   225  	builder, err := r.BuilderBuilder.FullBuilder(release.Metadata, resolvedConfig, updatedValues)
   226  	if err != nil {
   227  		return resolvedConfig, errors.Wrap(err, "initialize tpl builder")
   228  	}
   229  
   230  	for _, configGroup := range configCopy {
   231  		resolvedItems := make([]*libyaml.ConfigItem, 0)
   232  		for _, configItem := range configGroup.Items {
   233  			if !isReadOnly(configItem) {
   234  				if val, ok := combinedState[configItem.Name]; ok {
   235  					configItem.Value = fmt.Sprintf("%s", val)
   236  				}
   237  			}
   238  
   239  			resolvedItem, err := r.applyConfigItemFieldTemplates(ctx, *builder, configItem, updatedValues)
   240  			if err != nil {
   241  				return resolvedConfig, errors.Wrapf(err, "resolve item %s", configItem.Name)
   242  			}
   243  
   244  			if r.shouldOverrideValueWithDefault(configItem, savedState, firstPass) {
   245  				configItem.Value = configItem.Default
   246  			}
   247  
   248  			resolvedItems = append(resolvedItems, resolvedItem)
   249  		}
   250  
   251  		configGroup.Items = resolvedItems
   252  
   253  		resolvedGroup, err := r.applyConfigGroupFieldTemplates(ctx, *builder, configGroup)
   254  		if err != nil {
   255  			return resolvedConfig, errors.Wrapf(err, "resolve group %s", configGroup.Name)
   256  		}
   257  
   258  		resolvedConfig = append(resolvedConfig, resolvedGroup)
   259  	}
   260  
   261  	return resolvedConfig, nil
   262  }
   263  
   264  // ValidateConfig validates a list of resolved config items
   265  func ValidateConfig(
   266  	resolvedConfig []libyaml.ConfigGroup,
   267  ) []*ValidationError {
   268  	var validationErrs []*ValidationError
   269  	for _, configGroup := range resolvedConfig {
   270  		// hidden is set if when resolves to false
   271  
   272  		if hidden := configGroupIsHidden(configGroup); hidden {
   273  			continue
   274  		}
   275  
   276  		for _, configItem := range configGroup.Items {
   277  
   278  			if invalidItem := validateConfigItem(configItem); invalidItem != nil {
   279  				validationErrs = append(validationErrs, invalidItem)
   280  			}
   281  		}
   282  	}
   283  	return validationErrs
   284  }
   285  
   286  func configGroupIsHidden(
   287  	configGroup libyaml.ConfigGroup,
   288  ) bool {
   289  	// if all the items in the config group are hidden,
   290  	// we know when is set. thus config group is hidden
   291  	for _, configItem := range configGroup.Items {
   292  		if !isHidden(configItem) {
   293  			return false
   294  		}
   295  	}
   296  	return true
   297  }
   298  
   299  func validateConfigItem(
   300  	configItem *libyaml.ConfigItem,
   301  ) *ValidationError {
   302  	var validationErr *ValidationError
   303  	if isRequired(configItem) && !(isReadOnly(configItem) || isHidden(configItem)) {
   304  		if isEmpty(configItem) {
   305  			validationErr = &ValidationError{
   306  				Message:   fmt.Sprintf("Config item %s is required", configItem.Name),
   307  				Name:      configItem.Name,
   308  				ErrorCode: MissingRequiredValue,
   309  			}
   310  		}
   311  	}
   312  	return validationErr
   313  }
   314  
   315  func (r *APIConfigRenderer) applyConfigGroupFieldTemplates(ctx context.Context, builder templates.Builder, configGroup libyaml.ConfigGroup) (libyaml.ConfigGroup, error) {
   316  	// configgroup doesn't have a hidden attribute, so if the config group is hidden, we should
   317  	// set all items as hidden. this is called after applyConfigItemFieldTemplates and will override all hidden
   318  	// values in items if when is set
   319  	builtWhen, err := builder.String(configGroup.When)
   320  	if err != nil {
   321  		level.Error(r.Logger).Log("msg", "unable to build 'when' on configgroup", "group_name", configGroup.Name, "err", err)
   322  		return libyaml.ConfigGroup{}, err
   323  	}
   324  	configGroup.When = builtWhen
   325  
   326  	if builtWhen != "" {
   327  		builtWhenBool, err := builder.Bool(builtWhen, true)
   328  		if err != nil {
   329  			level.Error(r.Logger).Log("msg", "unable to build 'when' bool", "err", err)
   330  			return libyaml.ConfigGroup{}, err
   331  		}
   332  
   333  		for _, configItem := range configGroup.Items {
   334  			// if the config group is not hidden, don't override the value in the item
   335  			if !builtWhenBool {
   336  				configItem.Hidden = true
   337  			}
   338  		}
   339  	}
   340  
   341  	return configGroup, nil
   342  }
   343  
   344  func (r *APIConfigRenderer) applyConfigItemFieldTemplates(ctx context.Context, builder templates.Builder, configItem *libyaml.ConfigItem, configValues map[string]interface{}) (*libyaml.ConfigItem, error) {
   345  	// type should default to "text"
   346  	if configItem.Type == "" {
   347  		configItem.Type = "text"
   348  	}
   349  	// build "default"
   350  	builtDefault, err := builder.String(configItem.Default)
   351  	if err != nil {
   352  		level.Error(r.Logger).Log("msg", "unable to build 'default'", "err", err)
   353  		return nil, err
   354  	}
   355  	configItem.Default = builtDefault
   356  
   357  	// build "value"
   358  	builtValue, err := builder.String(configItem.Value)
   359  	if err != nil {
   360  		level.Error(r.Logger).Log("msg", "unable to build 'value'", "err", err)
   361  		return nil, err
   362  	}
   363  	configItem.Value = builtValue
   364  
   365  	previousVal, ok := configValues[configItem.Name]
   366  
   367  	if ok {
   368  		// only use this for defaults/values that should exist
   369  		if configItem.Value != "" {
   370  			configItem.Value = fmt.Sprintf("%s", previousVal)
   371  		} else {
   372  			configItem.Default = fmt.Sprintf("%s", previousVal)
   373  		}
   374  	}
   375  
   376  	// build "when" (dropping support for the when: a=b style here from replicated v1)
   377  	builtWhen, err := builder.String(configItem.When)
   378  	if err != nil {
   379  		level.Error(r.Logger).Log("msg", "unable to build `when'", "err", err)
   380  		return nil, err
   381  	}
   382  	configItem.When = builtWhen
   383  
   384  	// build "runonsave"
   385  	if configItem.TestProc != nil {
   386  		builtRunOnSave, err := builder.Bool(configItem.TestProc.RunOnSave, false)
   387  		if err != nil {
   388  			level.Error(r.Logger).Log("msg", "unable to build 'runonsave'", "err", err)
   389  			return nil, err
   390  		}
   391  		configItem.TestProc.RunOnSave = strconv.FormatBool(builtRunOnSave)
   392  	}
   393  
   394  	// build "hidden" from "when" if it's present
   395  	if configItem.When != "" {
   396  		builtWhenBool, err := builder.Bool(configItem.When, true)
   397  		if err != nil {
   398  			level.Error(r.Logger).Log("msg", "unable to build 'when'", "err", err)
   399  			return nil, err
   400  		}
   401  
   402  		// don't override the hidden value if the when value is false
   403  		if !builtWhenBool {
   404  			configItem.Hidden = true
   405  		}
   406  	}
   407  
   408  	// build subitems
   409  	if configItem.Items != nil {
   410  		childItems := make([]*libyaml.ConfigChildItem, 0)
   411  		for _, item := range configItem.Items {
   412  			builtChildItem, err := r.resolveConfigChildItem(ctx, builder, item)
   413  			if err != nil {
   414  				return nil, err
   415  			}
   416  
   417  			childItems = append(childItems, builtChildItem)
   418  		}
   419  
   420  		configItem.Items = childItems
   421  	}
   422  
   423  	return configItem, nil
   424  }
   425  
   426  func (r *APIConfigRenderer) resolveConfigChildItem(ctx context.Context, builder templates.Builder, configChildItem *libyaml.ConfigChildItem) (*libyaml.ConfigChildItem, error) {
   427  	// TODO
   428  	return configChildItem, nil
   429  }
   430  func (r *APIConfigRenderer) deepCopyConfig(groups []libyaml.ConfigGroup) ([]libyaml.ConfigGroup, error) {
   431  	var buf bytes.Buffer
   432  	enc := json.NewEncoder(&buf)
   433  	dec := json.NewDecoder(&buf)
   434  	err := enc.Encode(groups)
   435  	if err != nil {
   436  		return nil, errors.Wrapf(err, "encode group")
   437  	}
   438  
   439  	level.Debug(r.Logger).Log("event", "deepCopyConfig.encode", "encoded", buf.String())
   440  
   441  	var groupsCopy []libyaml.ConfigGroup
   442  	err = dec.Decode(&groupsCopy)
   443  	if err != nil {
   444  		return nil, errors.Wrapf(err, "decode group")
   445  	}
   446  	return groupsCopy, nil
   447  }