get.porter.sh/porter@v1.3.0/pkg/runtime/runtime_manifest.go (about) 1 package runtime 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "compress/gzip" 7 "context" 8 "encoding/base64" 9 "encoding/json" 10 "fmt" 11 "io" 12 "os" 13 "path/filepath" 14 "reflect" 15 "strings" 16 17 "get.porter.sh/porter/pkg" 18 "get.porter.sh/porter/pkg/cnab" 19 "get.porter.sh/porter/pkg/config" 20 "get.porter.sh/porter/pkg/experimental" 21 "get.porter.sh/porter/pkg/manifest" 22 "get.porter.sh/porter/pkg/tracing" 23 "get.porter.sh/porter/pkg/yaml" 24 "github.com/cbroglie/mustache" 25 "github.com/cnabio/cnab-go/bundle" 26 "github.com/cnabio/cnab-to-oci/relocation" 27 "github.com/hashicorp/go-multierror" 28 "github.com/opencontainers/go-digest" 29 yaml3 "gopkg.in/yaml.v3" 30 ) 31 32 const ( 33 // Path to the archive of the state from the previous run 34 statePath = "/porter/state.tgz" 35 ) 36 37 type RuntimeManifest struct { 38 config RuntimeConfig 39 *manifest.Manifest 40 41 Action string 42 43 // bundle is the executing bundle definition 44 bundle cnab.ExtendedBundle 45 46 // editor is the porter.yaml loaded into YQ so that we can 47 // do advanced stuff with the manifest, like just read out the yaml for a particular step. 48 editor *yaml.Editor 49 50 // bundles is map of the dependencies bundle definitions, keyed by the alias used in the root manifest 51 bundles map[string]cnab.ExtendedBundle 52 53 steps manifest.Steps 54 outputs map[string]string 55 sensitiveValues []string 56 } 57 58 func NewRuntimeManifest(cfg RuntimeConfig, action string, manifest *manifest.Manifest) *RuntimeManifest { 59 return &RuntimeManifest{ 60 config: cfg, 61 Action: action, 62 Manifest: manifest, 63 } 64 } 65 66 // this is a temporary function to help write debug logs until we have PORTER_VERBOSITY passed into the bundle properly 67 func (m *RuntimeManifest) debugf(log tracing.TraceLogger, msg string, args ...interface{}) { 68 if m.config.DebugMode { 69 log.Infof(msg, args...) 70 } 71 } 72 73 func (m *RuntimeManifest) Validate() error { 74 err := m.loadBundle() 75 if err != nil { 76 return err 77 } 78 79 err = m.loadDependencyDefinitions() 80 if err != nil { 81 return err 82 } 83 84 err = m.setStepsByAction() 85 if err != nil { 86 return err 87 } 88 89 err = m.steps.Validate(m.Manifest) 90 if err != nil { 91 return fmt.Errorf("invalid action configuration: %w", err) 92 } 93 94 return nil 95 } 96 97 func (m *RuntimeManifest) loadBundle() error { 98 // Load the CNAB representation of the bundle 99 b, err := cnab.LoadBundle(m.config.Context, "/cnab/bundle.json") 100 if err != nil { 101 return err 102 } 103 104 m.bundle = b 105 return nil 106 } 107 108 func (m *RuntimeManifest) GetInstallationNamespace() string { 109 return m.config.Getenv(config.EnvPorterInstallationNamespace) 110 } 111 112 func (m *RuntimeManifest) GetInstallationName() string { 113 return m.config.Getenv(config.EnvPorterInstallationName) 114 } 115 116 func (m *RuntimeManifest) loadDependencyDefinitions() error { 117 m.bundles = make(map[string]cnab.ExtendedBundle, len(m.Dependencies.Requires)) 118 for _, dep := range m.Dependencies.Requires { 119 bunD, err := GetDependencyDefinition(m.config.Context, dep.Name) 120 if err != nil { 121 return err 122 } 123 124 bun, err := bundle.Unmarshal(bunD) 125 if err != nil { 126 return fmt.Errorf("error unmarshaling bundle definition for dependency %s: %w", dep.Name, err) 127 } 128 129 m.bundles[dep.Name] = cnab.NewBundle(*bun) 130 } 131 132 return nil 133 } 134 135 // This wrapper type serve as a way of formatting a `map[string]interface{}` as 136 // JSON when the templating by mustache is done. It makes it possible to 137 // maintain the JSON string representation of the map while still allowing the 138 // map to be used as a context in the templating, allowing for accessing the 139 // map's keys in the template. 140 type FormattedObject map[string]interface{} 141 142 // Format the `FormattedObject` as a JSON string. 143 func (fo FormattedObject) Format(f fmt.State, c rune) { 144 jsonStr, _ := json.Marshal(fo) 145 fmt.Fprintf(f, "%s", string(jsonStr)) 146 } 147 148 func (m *RuntimeManifest) resolveParameter(pd manifest.ParameterDefinition) (interface{}, error) { 149 getValue := func(envVar string) (interface{}, error) { 150 value := m.config.Getenv(envVar) 151 if pd.Type == "object" { 152 var obj map[string]interface{} 153 if err := json.Unmarshal([]byte(value), &obj); err != nil { 154 return nil, err 155 } 156 return FormattedObject(obj), nil 157 } 158 return value, nil 159 } 160 161 if pd.Destination.EnvironmentVariable != "" { 162 return getValue(pd.Destination.EnvironmentVariable) 163 } 164 if pd.Destination.Path != "" { 165 return pd.Destination.Path, nil 166 } 167 envVar := manifest.ParamToEnvVar(pd.Name) 168 return getValue(envVar) 169 } 170 171 func (m *RuntimeManifest) resolveCredential(cd manifest.CredentialDefinition) (string, error) { 172 if cd.EnvironmentVariable != "" { 173 return m.config.Getenv(cd.EnvironmentVariable), nil 174 } else if cd.Path != "" { 175 return cd.Path, nil 176 } else { 177 return "", fmt.Errorf("credential: %s is malformed", cd.Name) 178 } 179 } 180 181 func (m *RuntimeManifest) resolveBundleOutput(outputName string) (string, error) { 182 // Get the output's value from the injected parameter source 183 ps := manifest.GetParameterSourceForOutput(outputName) 184 psParamEnv := manifest.ParamToEnvVar(ps) 185 outputValue, ok := m.config.LookupEnv(psParamEnv) 186 if !ok { 187 return "", fmt.Errorf("no parameter source was injected for output %s", outputName) 188 } 189 return outputValue, nil 190 } 191 192 func (m *RuntimeManifest) GetSensitiveValues() []string { 193 if m.sensitiveValues == nil { 194 return []string{} 195 } 196 return m.sensitiveValues 197 } 198 199 func (m *RuntimeManifest) setSensitiveValue(val string) { 200 exists := false 201 for _, item := range m.sensitiveValues { 202 if item == val { 203 exists = true 204 } 205 } 206 207 if !exists { 208 m.sensitiveValues = append(m.sensitiveValues, val) 209 } 210 } 211 212 func (m *RuntimeManifest) GetSteps() manifest.Steps { 213 return m.steps 214 } 215 216 func (m *RuntimeManifest) GetOutputs() map[string]string { 217 outputs := make(map[string]string, len(m.outputs)) 218 219 for k, v := range m.outputs { 220 outputs[k] = v 221 } 222 223 return outputs 224 } 225 226 func (m *RuntimeManifest) setStepsByAction() error { 227 switch m.Action { 228 case cnab.ActionInstall: 229 m.steps = m.Install 230 case cnab.ActionUninstall: 231 m.steps = m.Uninstall 232 case cnab.ActionUpgrade: 233 m.steps = m.Upgrade 234 default: 235 customAction, ok := m.CustomActions[m.Action] 236 if !ok { 237 actions := make([]string, 0, len(m.CustomActions)) 238 for a := range m.CustomActions { 239 actions = append(actions, a) 240 } 241 return fmt.Errorf("unsupported action %q, custom actions are defined for: %s", m.Action, strings.Join(actions, ", ")) 242 } 243 m.steps = customAction 244 } 245 246 return nil 247 } 248 249 func (m *RuntimeManifest) ApplyStepOutputs(assignments map[string]string) error { 250 if m.outputs == nil { 251 m.outputs = map[string]string{} 252 } 253 254 for outvar, outval := range assignments { 255 m.outputs[outvar] = outval 256 } 257 return nil 258 } 259 260 type StepOutput struct { 261 // The final value of the output returned by the mixin after executing 262 //lint:ignore U1000 ignore unused warning 263 value string 264 265 Name string `yaml:"name"` 266 Data map[string]interface{} `yaml:",inline"` 267 } 268 269 func (m *RuntimeManifest) buildSourceData() (map[string]interface{}, error) { 270 data := make(map[string]interface{}) 271 m.sensitiveValues = []string{} 272 273 inst := make(map[string]interface{}) 274 data["installation"] = inst 275 inst["namespace"] = m.GetInstallationNamespace() 276 inst["name"] = m.GetInstallationName() 277 278 bun := make(map[string]interface{}) 279 data["bundle"] = bun 280 281 // Enable interpolation of manifest/bundle name via bundle.name 282 bun["name"] = m.Name 283 bun["version"] = m.Version 284 bun["description"] = m.Description 285 bun["installerImage"] = m.Image 286 bun["custom"] = m.Custom 287 288 // Make environment variable accessible 289 env := m.config.EnvironMap() 290 data["env"] = env 291 292 params := make(map[string]interface{}) 293 bun["parameters"] = params 294 for _, param := range m.Parameters { 295 if !param.AppliesTo(m.Action) { 296 continue 297 } 298 299 pe := param.Name 300 val, err := m.resolveParameter(param) 301 if err != nil { 302 return nil, err 303 } 304 if param.Sensitive { 305 m.setSensitiveValue(fmt.Sprint(val)) 306 } 307 params[pe] = val 308 } 309 310 creds := make(map[string]interface{}) 311 bun["credentials"] = creds 312 for _, cred := range m.Credentials { 313 pe := cred.Name 314 val, err := m.resolveCredential(cred) 315 if err != nil { 316 return nil, err 317 } 318 m.setSensitiveValue(val) 319 creds[pe] = val 320 } 321 322 deps := make(map[string]interface{}) 323 bun["dependencies"] = deps 324 for alias, depB := range m.bundles { 325 // bundle.dependencies.ALIAS.outputs.NAME 326 depBun := make(map[string]interface{}) 327 deps[alias] = depBun 328 329 depBun["name"] = depB.Name 330 depBun["version"] = depB.Version 331 depBun["description"] = depB.Description 332 } 333 334 bun["outputs"] = m.outputs 335 336 // Iterate through the runtime manifest's step outputs and determine if we should mask 337 for name, val := range m.outputs { 338 // TODO: support configuring sensitivity for step outputs that aren't also bundle-level outputs 339 // See https://github.com/getporter/porter/issues/855 340 341 // If step output is also a bundle-level output, defer to bundle-level output sensitivity 342 if outputDef, ok := m.Outputs[name]; ok && !outputDef.Sensitive { 343 continue 344 } 345 m.setSensitiveValue(val) 346 } 347 348 // Externally injected outputs (bundle level outputs and dependency outputs) are 349 // injected through parameter sources. 350 bunExt, err := m.bundle.ProcessRequiredExtensions() 351 if err != nil { 352 return nil, err 353 } 354 355 paramSources, _, err := bunExt.GetParameterSources() 356 if err != nil { 357 return nil, err 358 } 359 360 templatedOutputs := m.GetTemplatedOutputs() 361 templatedDependencyOutputs := m.GetTemplatedDependencyOutputs() 362 for paramName, sources := range paramSources { 363 param := m.bundle.Parameters[paramName] 364 if !param.AppliesTo(m.Action) { 365 continue 366 } 367 368 for _, s := range sources.ListSourcesByPriority() { 369 switch ps := s.(type) { 370 case cnab.DependencyOutputParameterSource: 371 outRef := manifest.DependencyOutputReference{Dependency: ps.Dependency, Output: ps.OutputName} 372 373 // Ignore anything that isn't templated, because that's what we are building the source data for 374 if _, isTemplated := templatedDependencyOutputs[outRef.String()]; !isTemplated { 375 continue 376 } 377 378 depBun := deps[ps.Dependency].(map[string]interface{}) 379 var depOutputs map[string]interface{} 380 if depBun["outputs"] == nil { 381 depOutputs = make(map[string]interface{}) 382 depBun["outputs"] = depOutputs 383 } else { 384 depOutputs = depBun["outputs"].(map[string]interface{}) 385 } 386 387 value, err := m.ReadDependencyOutputValue(outRef) 388 if err != nil { 389 return nil, err 390 } 391 392 depOutputs[ps.OutputName] = value 393 394 // Determine if the dependency's output is defined as sensitive 395 depB := m.bundles[ps.Dependency] 396 if ok, _ := depB.IsOutputSensitive(ps.OutputName); ok { 397 m.setSensitiveValue(value) 398 } 399 400 case cnab.OutputParameterSource: 401 // Ignore anything that isn't templated, because that's what we are building the source data for 402 if _, isTemplated := templatedOutputs[ps.OutputName]; !isTemplated { 403 continue 404 } 405 406 // A bundle-level output may also be a step-level output 407 // If already set, do not override 408 if val, exists := m.outputs[ps.OutputName]; exists && val != "" { 409 continue 410 } 411 412 val, err := m.resolveBundleOutput(ps.OutputName) 413 if err != nil { 414 return nil, err 415 } 416 417 if m.outputs == nil { 418 m.outputs = map[string]string{} 419 } 420 m.outputs[ps.OutputName] = val 421 bun["outputs"] = m.outputs 422 423 outputDef := m.Manifest.Outputs[ps.OutputName] 424 if outputDef.Sensitive { 425 m.setSensitiveValue(val) 426 } 427 } 428 } 429 } 430 431 images := make(map[string]interface{}) 432 bun["images"] = images 433 for alias, image := range m.ImageMap { 434 // just assigning the struct here results in uppercase keys, which would give us 435 // strange things like {{ bundle.images.something.Repository }} 436 // So reflect and walk through the struct (this way we don't need to update this later) 437 val := reflect.ValueOf(image) 438 img := make(map[string]string) 439 typeOfT := val.Type() 440 for i := 0; i < val.NumField(); i++ { 441 f := val.Field(i) 442 name := toCamelCase(typeOfT.Field(i).Name) 443 img[name] = f.String() 444 } 445 images[alias] = img 446 } 447 return data, nil 448 } 449 450 func (m *RuntimeManifest) buildAndResolveMappedDependencyOutputs(sourceData map[string]interface{}) error { 451 for _, manifestDep := range m.Dependencies.Requires { 452 var depBun = sourceData["bundle"].(map[string]interface{})["dependencies"].(map[string]interface{})[manifestDep.Name].(map[string]interface{}) 453 var depOutputs map[string]interface{} 454 if depBun["outputs"] == nil { 455 depOutputs = make(map[string]interface{}) 456 depBun["outputs"] = depOutputs 457 } else { 458 depOutputs = depBun["outputs"].(map[string]interface{}) 459 } 460 461 sourceData["outputs"] = depOutputs 462 463 for outputName, mappedOutput := range manifestDep.Outputs { 464 mappedOutputTemplate := m.GetTemplatePrefix() + mappedOutput 465 renderedOutput, err := mustache.RenderRaw(mappedOutputTemplate, true, sourceData) 466 if err != nil { 467 return fmt.Errorf("unable to render dependency %s output template %s: %w", manifestDep.Name, mappedOutput, err) 468 } 469 depOutputs[outputName] = renderedOutput 470 } 471 } 472 473 delete(sourceData, "outputs") 474 475 return nil 476 } 477 478 // ResolveStep will walk through the Step's data and resolve any placeholder 479 // data using the definitions in the manifest, like parameters or credentials. 480 func (m *RuntimeManifest) ResolveStep(ctx context.Context, stepIndex int, step *manifest.Step) error { 481 log := tracing.LoggerFromContext(ctx) 482 483 // Refresh our template data 484 sourceData, err := m.buildSourceData() 485 if err != nil { 486 return log.Error(fmt.Errorf("unable to build step template data: %w", err)) 487 } 488 489 mustache.AllowMissingVariables = false 490 491 if m.config.IsFeatureEnabled(experimental.FlagDependenciesV2) { 492 err = m.buildAndResolveMappedDependencyOutputs(sourceData) 493 if err != nil { 494 return log.Errorf("unable to build and resolve mapped dependency outputs: %w", err) 495 } 496 } 497 498 // Get the original yaml for the current step 499 stepPath := fmt.Sprintf("%s[%d]", m.Action, stepIndex) 500 stepTemplate, err := m.getStepTemplate(stepPath) 501 if err != nil { 502 return log.Error(fmt.Errorf("unable to retrieve original yaml for step %s: %w", stepPath, err)) 503 } 504 505 // TODO: add back logging step data after we have a solid way to censor it in https://github.com/getporter/porter/issues/2256 506 //fmt.Fprintf(m.Err, "=== Step Data ===\n%v\n", sourceData) 507 m.debugf(log, "=== Step Template ===\n%v\n", stepTemplate) 508 509 rendered, err := mustache.RenderRaw(stepTemplate, true, sourceData) 510 if err != nil { 511 return log.Errorf("unable to render step template %s: %w", stepTemplate, err) 512 } 513 514 // TODO: add back logging step data after we have a solid way to censor it in https://github.com/getporter/porter/issues/2256 515 //fmt.Fprintf(m.Err, "=== Rendered Step ===\n%s\n", rendered) 516 517 // Update the step parameter with the result of rendering the template 518 err = yaml.Unmarshal([]byte(rendered), step) 519 if err != nil { 520 return log.Error(fmt.Errorf("invalid step yaml after rendering template\n%s: %w", stepTemplate, err)) 521 } 522 523 return nil 524 } 525 526 // Initialize prepares the runtime environment prior to step execution 527 func (m *RuntimeManifest) Initialize(ctx context.Context) error { 528 if err := m.createOutputsDir(); err != nil { 529 return err 530 } 531 532 // For parameters of type "file", we may need to decode files on the filesystem 533 // before execution of the step/action 534 for paramName, param := range m.bundle.Parameters { 535 if !param.AppliesTo(m.Action) { 536 continue 537 } 538 539 def, hasDef := m.bundle.Definitions[param.Definition] 540 if !hasDef { 541 continue 542 } 543 544 if m.bundle.IsFileType(def) { 545 if param.Destination.Path == "" { 546 return fmt.Errorf("destination path is not supplied for parameter %s", paramName) 547 } 548 549 // Porter by default places parameter value into file determined by Destination.Path 550 bytes, err := m.config.FileSystem.ReadFile(param.Destination.Path) 551 if err != nil { 552 if os.IsNotExist(err) { 553 continue 554 } 555 return fmt.Errorf("unable to acquire value for parameter %s: %w", paramName, err) 556 } 557 558 // TODO(carolynvs): hack around parameters ALWAYS being injected even when empty files mess things up 559 // I'm not sure yet why it's injecting as null instead of "" (as required by the spec) 560 // We want to get the null -> "" fixed, and also not write files into the bundle when unset. 561 // that's a cnab change somewhere probably 562 // the problem is in injectParameters in cnab-go 563 if string(bytes) == "null" { 564 err := m.config.FileSystem.Remove(param.Destination.Path) 565 if err != nil { 566 return fmt.Errorf("unable to remove destination path: %w", err) 567 } 568 continue 569 } 570 decoded, err := base64.StdEncoding.DecodeString(string(bytes)) 571 if err != nil { 572 return fmt.Errorf("unable to decode parameter %s: %w", paramName, err) 573 } 574 575 err = m.config.FileSystem.WriteFile(param.Destination.Path, decoded, pkg.FileModeWritable) 576 if err != nil { 577 return fmt.Errorf("unable to write decoded parameter %s: %w", paramName, err) 578 } 579 } 580 } 581 582 return m.unpackStateBag(ctx) 583 } 584 585 func (m *RuntimeManifest) createOutputsDir() error { 586 // Ensure outputs directory exists 587 if err := m.config.FileSystem.MkdirAll(config.BundleOutputsDir, pkg.FileModeDirectory); err != nil { 588 return fmt.Errorf("unable to ensure CNAB outputs directory exists: %w", err) 589 } 590 return nil 591 } 592 593 // Unpack each state variable from /porter/state.tgz and copy it to its 594 // declared location in the bundle. 595 func (m *RuntimeManifest) unpackStateBag(ctx context.Context) error { 596 log := tracing.LoggerFromContext(ctx) 597 _, err := m.config.FileSystem.Open(statePath) 598 if os.IsNotExist(err) || len(m.StateBag) == 0 { 599 m.debugf(log, "No existing bundle state to unpack") 600 return nil 601 } 602 bytes, err := m.config.FileSystem.ReadFile(statePath) 603 if err != nil { 604 m.debugf(log, "Unable to read bundle state file") 605 return err 606 } 607 // TODO(sgettys): hack around state.tgz ALWAYS being injected even when empty files mess things up 608 // I'm not sure yet why it's injecting as null instead of "" (as required by the spec) 609 // We want to get the null -> "" fixed, and also not write files into the bundle when unset. 610 // that's a cnab change somewhere probably 611 // the problem is in injectParameters in cnab-go 612 if string(bytes) == "null" { 613 m.debugf(log, "Bundle state file has null content") 614 err = m.config.FileSystem.Remove(statePath) 615 if err != nil { 616 _ = log.Error(err) 617 } 618 return nil 619 } 620 // Unpack the state file and copy its contents to where the bundle expects them 621 // state var name -> path in bundle 622 log.Debug("Unpacking bundle state...") 623 stateFiles := make(map[string]string, len(m.StateBag)) 624 for _, s := range m.StateBag { 625 stateFiles[s.Name] = s.Path 626 } 627 628 unpackStateFile := func(tr *tar.Reader, header *tar.Header) error { 629 name := strings.TrimPrefix(header.Name, "porter-state/") 630 dest := stateFiles[name] 631 m.debugf(log, " - %s -> %s", name, dest) 632 633 f, err := os.OpenFile(dest, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 634 if err != nil { 635 return log.Error(fmt.Errorf("error creating state file %s: %w", dest, err)) 636 } 637 defer f.Close() 638 639 _, err = io.Copy(f, tr) 640 if err != nil { 641 return log.Error(fmt.Errorf("error unpacking state file %s: %w", dest, err)) 642 } 643 644 return nil 645 } 646 647 stateArchive, err := m.config.FileSystem.Open(statePath) 648 if err != nil { 649 return log.Error(fmt.Errorf("could not open statefile at %s: %w", statePath, err)) 650 } 651 652 gzr, err := gzip.NewReader(stateArchive) 653 if err != nil { 654 if err == io.EOF { 655 log.Debug("statefile exists but is empty") 656 return nil 657 } 658 return log.Error(fmt.Errorf("could not create a new gzip reader for the statefile: %w", err)) 659 } 660 defer gzr.Close() 661 662 tr := tar.NewReader(gzr) 663 for { 664 header, err := tr.Next() 665 if err != nil { 666 if err == io.EOF { 667 break 668 } 669 } else if header.Typeflag != tar.TypeReg { 670 continue 671 } 672 673 if err = unpackStateFile(tr, header); err != nil { 674 return err 675 } 676 } 677 678 return nil 679 } 680 681 // Finalize cleans up the bundle before its completion. 682 func (m *RuntimeManifest) Finalize(ctx context.Context) error { 683 var bigErr *multierror.Error 684 685 if err := m.applyUnboundBundleOutputs(ctx); err != nil { 686 bigErr = multierror.Append(bigErr, err) 687 } 688 689 // Always try to persist state, even when errors occur 690 if err := m.packStateBag(ctx); err != nil { 691 bigErr = multierror.Append(bigErr, err) 692 } 693 694 return bigErr.ErrorOrNil() 695 } 696 697 // Pack each state variable into /porter/state/tgz. 698 func (m *RuntimeManifest) packStateBag(ctx context.Context) error { 699 log := tracing.LoggerFromContext(ctx) 700 701 m.debugf(log, "Packing bundle state...") 702 packStateFile := func(tw *tar.Writer, s manifest.StateVariable) error { 703 fi, err := m.config.FileSystem.Stat(s.Path) 704 if os.IsNotExist(err) { 705 return nil 706 } 707 708 m.debugf(log, " - %s", s.Path) 709 header, err := tar.FileInfoHeader(fi, fi.Name()) 710 if err != nil { 711 return log.Error(fmt.Errorf("error creating tar header for state variable %s from path %s: %w", s.Name, s.Path, err)) 712 } 713 header.Name = filepath.Join("porter-state", s.Name) 714 715 if err := tw.WriteHeader(header); err != nil { 716 return log.Error(fmt.Errorf("error writing tar header for state variable %s: %w", s.Name, err)) 717 } 718 719 f, err := os.Open(s.Path) 720 if err != nil { 721 return log.Error(fmt.Errorf("error reading state file %s for variable %s: %w", s.Path, s.Name, err)) 722 } 723 defer f.Close() 724 725 _, err = io.Copy(tw, f) 726 if err != nil { 727 return log.Error(fmt.Errorf("error archiving state file %s for variable %s: %w", s.Path, s.Name, err)) 728 } 729 730 return nil 731 } 732 733 // Save directly to the final output location since we've already collected outputs at this point 734 stateArchive, err := m.config.FileSystem.Create("/cnab/app/outputs/porter-state") 735 if err != nil { 736 return log.Error(fmt.Errorf("error creating porter statefile: %w", err)) 737 } 738 739 gzw := gzip.NewWriter(stateArchive) 740 defer gzw.Close() 741 742 tw := tar.NewWriter(gzw) 743 defer tw.Close() 744 745 // Persist as many of the state vars as possible, even if one fails 746 var bigErr *multierror.Error 747 for _, s := range m.StateBag { 748 err := packStateFile(tw, s) 749 if err != nil { 750 bigErr = multierror.Append(bigErr, err) 751 } 752 } 753 754 return log.Error(bigErr.ErrorOrNil()) 755 } 756 757 // applyUnboundBundleOutputs finds outputs that haven't been bound yet by a step, 758 // and if they can be bound, i.e. they grab a file from the bundle's filesystem, 759 // apply the output. 760 func (m *RuntimeManifest) applyUnboundBundleOutputs(ctx context.Context) error { 761 if len(m.bundle.Outputs) == 0 { 762 return nil 763 } 764 765 log := tracing.LoggerFromContext(ctx) 766 767 log.Debug("Collecting bundle outputs...") 768 var bigErr *multierror.Error 769 outputs := m.GetOutputs() 770 for name, outputDef := range m.bundle.Outputs { 771 outputSrcPath := m.Outputs[name].Path 772 773 // We can only deal with outputs that are based on a file right now 774 if outputSrcPath == "" { 775 continue 776 } 777 778 if !outputDef.AppliesTo(m.Action) { 779 continue 780 } 781 782 // Print the output that we've collected 783 m.debugf(log, " - %s", name) 784 if _, hasOutput := outputs[name]; !hasOutput { 785 // Use the path as originally defined in the manifest 786 // TODO(carolynvs): When we switch to driving everything completely 787 // from the bundle.json, we need to find a better way to get the original path value that the user specified. 788 // We don't force people to output files immediately to /cnab/app/outputs and so that original location should 789 // be persisted somewhere in the bundle.json (probably in custom) 790 srcPath := manifest.ResolvePath(outputSrcPath) 791 if _, err := m.config.FileSystem.Stat(srcPath); err != nil { 792 continue 793 } 794 dstPath := filepath.Join(config.BundleOutputsDir, name) 795 if dstExists, _ := m.config.FileSystem.Exists(dstPath); dstExists { 796 continue 797 } 798 799 err := m.config.CopyFile(srcPath, dstPath) 800 if err != nil { 801 bigErr = multierror.Append(bigErr, fmt.Errorf("unable to copy output file from %s to %s: %w", srcPath, dstPath, err)) 802 continue 803 } 804 } 805 } 806 807 return log.Error(bigErr.ErrorOrNil()) 808 } 809 810 // ResolveInvocationImage updates the RuntimeManifest to properly reflect the bundle image passed to the bundle via the 811 // mounted bundle.json and relocation mapping 812 func (m *RuntimeManifest) ResolveInvocationImage(bun cnab.ExtendedBundle, reloMap relocation.ImageRelocationMap) error { 813 for _, image := range bun.InvocationImages { 814 if image.Digest == "" || image.ImageType != "docker" { 815 continue 816 } 817 818 ref, err := cnab.ParseOCIReference(image.Image) 819 if err != nil { 820 return fmt.Errorf("unable to parse bundle image reference: %w", err) 821 } 822 refWithDigest, err := ref.WithDigest(digest.Digest(image.Digest)) 823 if err != nil { 824 return fmt.Errorf("unable to get bundle image reference with digest: %w", err) 825 } 826 827 m.Image = refWithDigest.String() 828 break 829 } 830 relocated, ok := reloMap[m.Image] 831 if ok { 832 m.Image = relocated 833 } 834 return nil 835 } 836 837 // ResolveImages updates the RuntimeManifest to properly reflect the image map passed to the bundle via the 838 // mounted bundle.json and relocation mapping 839 func (m *RuntimeManifest) ResolveImages(bun cnab.ExtendedBundle, reloMap relocation.ImageRelocationMap) error { 840 // It only makes sense to process this if the runtime manifest has images defined. If none are defined 841 // return early 842 if len(m.ImageMap) == 0 { 843 return nil 844 } 845 reverseLookup := make(map[string]string) 846 for alias, image := range bun.Images { 847 manifestImage, ok := m.ImageMap[alias] 848 if !ok { 849 return fmt.Errorf("unable to find image in porter manifest: %s", alias) 850 } 851 manifestImage.Digest = image.Digest 852 err := resolveImage(&manifestImage, image.Image) 853 if err != nil { 854 return fmt.Errorf("unable to update image map from bundle.json: %w", err) 855 } 856 m.ImageMap[alias] = manifestImage 857 reverseLookup[image.Image] = alias 858 } 859 for oldRef, reloRef := range reloMap { 860 alias := reverseLookup[oldRef] 861 if manifestImage, ok := m.ImageMap[alias]; ok { //note, there might be other images in the relocation mapping, like the bundle image 862 err := resolveImage(&manifestImage, reloRef) 863 if err != nil { 864 return fmt.Errorf("unable to update image map from relocation mapping: %w", err) 865 } 866 m.ImageMap[alias] = manifestImage 867 } 868 } 869 return nil 870 } 871 872 func (m *RuntimeManifest) getEditor() (*yaml.Editor, error) { 873 if m.editor != nil { 874 return m.editor, nil 875 } 876 877 // Get the original porter.yaml with additional yaml metadata so that we can look at just the current step's yaml 878 yq := yaml.NewEditor(m.config.FileSystem) 879 if err := yq.ReadFile(m.ManifestPath); err != nil { 880 return nil, fmt.Errorf("error loading yaml editor from %s", m.ManifestPath) 881 } 882 883 return yq, nil 884 } 885 886 func (m *RuntimeManifest) getStepTemplate(stepPath string) (string, error) { 887 yq, err := m.getEditor() 888 if err != nil { 889 return "", err 890 } 891 892 stepNode, err := yq.GetNode(stepPath) 893 if err != nil { 894 return "", fmt.Errorf("unable to retrieve original yaml for step %s: %w", stepPath, err) 895 } 896 897 var stepYAML bytes.Buffer 898 enc := yaml3.NewEncoder(&stepYAML) 899 defer enc.Close() 900 if err := enc.Encode(stepNode); err != nil { 901 return "", fmt.Errorf("error re-encoding porter.yaml for templating: %w", err) 902 } 903 904 stepTemplate := m.GetTemplatePrefix() + stepYAML.String() 905 return stepTemplate, nil 906 } 907 908 func resolveImage(image *manifest.MappedImage, refString string) error { 909 //figure out what type of Reference it is so we can extract useful things for our image map 910 ref, err := cnab.ParseOCIReference(refString) 911 if err != nil { 912 return err 913 } 914 915 image.Repository = ref.Repository() 916 if ref.HasDigest() { 917 image.Digest = ref.Digest().String() 918 } 919 920 if ref.HasTag() { 921 image.Tag = ref.Tag() 922 } 923 924 if ref.IsRepositoryOnly() { 925 image.Tag = "latest" // Populate this with latest so that the {{ can reference something }} 926 } 927 928 return nil 929 } 930 931 // toCamelCase returns a camel-cased variant of the provided string 932 func toCamelCase(str string) string { 933 var b strings.Builder 934 935 b.WriteString(strings.ToLower(string(str[0]))) 936 b.WriteString(str[1:]) 937 938 return b.String() 939 }