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 }