get.porter.sh/porter@v1.3.0/pkg/manifest/manifest.go (about) 1 package manifest 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "path" 10 "reflect" 11 "regexp" 12 "sort" 13 "strings" 14 15 "get.porter.sh/porter/pkg/cnab" 16 "get.porter.sh/porter/pkg/config" 17 "get.porter.sh/porter/pkg/experimental" 18 "get.porter.sh/porter/pkg/portercontext" 19 "get.porter.sh/porter/pkg/schema" 20 "get.porter.sh/porter/pkg/tracing" 21 "get.porter.sh/porter/pkg/yaml" 22 "github.com/Masterminds/semver/v3" 23 "github.com/cbroglie/mustache" 24 "github.com/cnabio/cnab-go/bundle" 25 "github.com/cnabio/cnab-go/bundle/definition" 26 "github.com/dustin/go-humanize" 27 "github.com/hashicorp/go-multierror" 28 "github.com/opencontainers/go-digest" 29 "go.opentelemetry.io/otel/attribute" 30 ) 31 32 const ( 33 invalidStepErrorFormat = "validation of action \"%s\" failed: %w" 34 35 // SchemaTypeBundle is the default schemaType value for Bundle resources 36 SchemaTypeBundle = "Bundle" 37 38 // TemplateDelimiterPrefix must be present at the beginning of any porter.yaml 39 // that wants to use ${} as the template delimiter instead of the mustache 40 // default of {{}}. 41 TemplateDelimiterPrefix = "{{=${ }=}}\n" 42 ) 43 44 var ( 45 // TODO(PEP003): Version 1.1.0 is behind the DependenciesV2 feature flag. We update the supported versions later 46 // when validating a bundle and only allow that version when it is enabled. 47 // The default version remains on the last stable version 1.0.1. 48 // When the schema version is stable and not behind a feature flag, we can update the supported versions and default version. 49 50 // SupportedSchemaVersions is the Porter manifest (porter.yaml) schema 51 // versions supported by this version of Porter, specified as a semver range. 52 // When the Manifest structure is changed, this field should be incremented. 53 SupportedSchemaVersions, _ = semver.NewConstraint("1.0.0-alpha.1 || 1.0.0 - 1.0.1") 54 55 // DefaultSchemaVersion is the most recently supported schema version. 56 // When the Manifest structure is changed, this field should be incremented. 57 DefaultSchemaVersion = semver.MustParse("1.0.1") 58 ) 59 60 type Manifest struct { 61 // ManifestPath is location to the original, user-supplied manifest, such as the path on the filesystem or a url 62 ManifestPath string `yaml:"-"` 63 64 // TemplateVariables are the variables used in the templating, e.g. bundle.parameters.NAME, or bundle.outputs.NAME 65 TemplateVariables []string `yaml:"-"` 66 67 // SchemaType indicates the type of resource contained in an imported file. 68 SchemaType string `yaml:"schemaType,omitempty"` 69 70 // SchemaVersion is a semver value that indicates which version of the porter.yaml schema is used in the file. 71 SchemaVersion string `yaml:"schemaVersion"` 72 Name string `yaml:"name,omitempty"` 73 Description string `yaml:"description,omitempty"` 74 Version string `yaml:"version,omitempty"` 75 76 Maintainers []MaintainerDefinition `yaml:"maintainers,omitempty"` 77 78 // Registry is the OCI registry and org/subdomain for the bundle 79 Registry string `yaml:"registry,omitempty"` 80 81 // Reference is the optional, full bundle reference 82 // in the format REGISTRY/NAME or REGISTRY/NAME:TAG 83 Reference string `yaml:"reference,omitempty"` 84 85 // DockerTag is the Docker tag portion of the published bundle 86 // image and bundle. It will only be set at time of publishing. 87 DockerTag string `yaml:"-"` 88 89 // Image is the name of the bundle image in the format REGISTRY/NAME:TAG 90 // It doesn't map to any field in the manifest as it has been deprecated 91 // and isn't meant to be user-specified 92 Image string `yaml:"-"` 93 94 // Dockerfile is the relative path to the Dockerfile template for the bundle image 95 Dockerfile string `yaml:"dockerfile,omitempty"` 96 97 Mixins []MixinDeclaration `yaml:"mixins,omitempty"` 98 99 Install Steps `yaml:"install"` 100 Uninstall Steps `yaml:"uninstall"` 101 Upgrade Steps `yaml:"upgrade"` 102 103 Custom CustomDefinitions `yaml:"custom,omitempty"` 104 CustomActions map[string]Steps `yaml:"-"` 105 CustomActionDefinitions map[string]CustomActionDefinition `yaml:"customActions,omitempty"` 106 107 StateBag StateBag `yaml:"state,omitempty"` 108 Parameters ParameterDefinitions `yaml:"parameters,omitempty"` 109 Credentials CredentialDefinitions `yaml:"credentials,omitempty"` 110 Dependencies Dependencies `yaml:"dependencies,omitempty"` 111 Outputs OutputDefinitions `yaml:"outputs,omitempty"` 112 113 // ImageMap is a map of images referenced in the bundle. If an image relocation mapping is later provided, that 114 // will be mounted at as a file at runtime to /cnab/app/relocation-mapping.json. 115 ImageMap map[string]MappedImage `yaml:"images,omitempty"` 116 117 Required []RequiredExtension `yaml:"required,omitempty"` 118 } 119 120 func (m *Manifest) Validate(ctx context.Context, cfg *config.Config) error { 121 ctx, span := tracing.StartSpan(ctx) 122 defer span.EndSpan() 123 124 var result error 125 126 err := m.validateMetadata(ctx, cfg) 127 if err != nil { 128 return err 129 } 130 131 err = m.SetDefaults() 132 if err != nil { 133 return span.Error(err) 134 } 135 136 if strings.ToLower(m.Dockerfile) == "dockerfile" { 137 return span.Error(errors.New("Dockerfile template cannot be named 'Dockerfile' because that is the filename generated during porter build")) 138 } 139 140 if len(m.Mixins) == 0 { 141 result = multierror.Append(result, errors.New("no mixins declared")) 142 } 143 144 if m.Install == nil { 145 result = multierror.Append(result, errors.New("no install action defined")) 146 } 147 err = m.Install.Validate(m) 148 if err != nil { 149 result = multierror.Append(result, fmt.Errorf(invalidStepErrorFormat, "install", err)) 150 } 151 152 if m.Uninstall == nil { 153 result = multierror.Append(result, errors.New("no uninstall action defined")) 154 } 155 err = m.Uninstall.Validate(m) 156 if err != nil { 157 result = multierror.Append(result, fmt.Errorf(invalidStepErrorFormat, "uninstall", err)) 158 } 159 160 if m.Upgrade != nil { 161 err = m.Upgrade.Validate(m) 162 if err != nil { 163 result = multierror.Append(result, fmt.Errorf(invalidStepErrorFormat, "upgrade", err)) 164 } 165 } 166 167 for actionName, steps := range m.CustomActions { 168 err := steps.Validate(m) 169 if err != nil { 170 result = multierror.Append(result, fmt.Errorf(invalidStepErrorFormat, actionName, err)) 171 } 172 } 173 174 for _, dep := range m.Dependencies.Requires { 175 err = dep.Validate(cfg.Context) 176 if err != nil { 177 result = multierror.Append(result, err) 178 } 179 } 180 181 for _, output := range m.Outputs { 182 err = output.Validate() 183 if err != nil { 184 result = multierror.Append(result, err) 185 } 186 } 187 188 for _, parameter := range m.Parameters { 189 err = parameter.Validate() 190 if err != nil { 191 result = multierror.Append(result, err) 192 } 193 } 194 195 for _, image := range m.ImageMap { 196 err = image.Validate() 197 if err != nil { 198 result = multierror.Append(result, err) 199 } 200 } 201 202 return span.Error(result) 203 } 204 205 func (m *Manifest) validateMetadata(ctx context.Context, cfg *config.Config) error { 206 ctx, span := tracing.StartSpan(ctx) 207 defer span.EndSpan() 208 209 strategy := cfg.GetSchemaCheckStrategy(ctx) 210 span.SetAttributes(attribute.String("schemaCheckStrategy", string(strategy))) 211 212 if m.SchemaType == "" { 213 m.SchemaType = SchemaTypeBundle 214 } else if !strings.EqualFold(m.SchemaType, SchemaTypeBundle) { 215 return span.Errorf("invalid schemaType %s, expected %s", m.SchemaType, SchemaTypeBundle) 216 } 217 218 // Check what the supported schema version is based on if depsv2 is enabled 219 supportedVersions := SupportedSchemaVersions 220 if cfg.IsFeatureEnabled(experimental.FlagDependenciesV2) { 221 supportedVersions, _ = semver.NewConstraint("1.0.0-alpha.1 || 1.0.0 - 1.0.1 || 1.1.0") 222 } 223 224 if warnOnly, err := schema.ValidateSchemaVersion(strategy, supportedVersions, m.SchemaVersion, DefaultSchemaVersion); err != nil { 225 if warnOnly { 226 span.Warn(err.Error()) 227 } else { 228 return span.Error(err) 229 } 230 } 231 232 if m.Name == "" { 233 return span.Error(errors.New("bundle name must be set")) 234 } 235 236 if m.Registry == "" && m.Reference == "" { 237 return span.Error(errors.New("a registry or reference value must be provided")) 238 } 239 240 if m.Reference != "" && m.Registry != "" { 241 span.Warnf("WARNING: both registry and reference were provided; "+ 242 "using the reference value of %s for the bundle reference\n", m.Reference) 243 } 244 245 // Allow for the user to have specified the version with a leading v prefix but save it as 246 // proper semver 247 if m.Version != "" { 248 v, err := semver.NewVersion(m.Version) 249 if err != nil { 250 return span.Errorf("version %q is not a valid semver value: %w", m.Version, err) 251 } 252 m.Version = v.String() 253 } 254 return nil 255 } 256 257 var templatedOutputRegex = regexp.MustCompile(`^bundle\.outputs\.(.+)$`) 258 259 // getTemplateOutputName returns the output name from the template variable. 260 func (m *Manifest) getTemplateOutputName(value string) (string, bool) { 261 matches := templatedOutputRegex.FindStringSubmatch(value) 262 if len(matches) < 2 { 263 return "", false 264 } 265 266 outputName := matches[1] 267 return outputName, true 268 } 269 270 var templatedDependencyOutputRegex = regexp.MustCompile(`^bundle\.dependencies\.(.+).outputs.(.+)$`) 271 272 // getTemplateDependencyOutputName returns the dependency and output name from the 273 // template variable. 274 func (m *Manifest) getTemplateDependencyOutputName(value string) (string, string, bool) { 275 matches := templatedDependencyOutputRegex.FindStringSubmatch(value) 276 if len(matches) < 3 { 277 return "", "", false 278 } 279 280 dependencyName := matches[1] 281 outputName := matches[2] 282 return dependencyName, outputName, true 283 } 284 285 var templatedDependencyShortOutputRegex = regexp.MustCompile(`^outputs.(.+)$`) 286 287 // getTemplateDependencyShortOutputName returns the dependency output name from the 288 // template variable. 289 func (m *Manifest) getTemplateDependencyShortOutputName(value string) (string, bool) { 290 matches := templatedDependencyShortOutputRegex.FindStringSubmatch(value) 291 if len(matches) < 2 { 292 return "", false 293 } 294 295 outputName := matches[1] 296 return outputName, true 297 } 298 299 var templatedParameterRegex = regexp.MustCompile(`^bundle\.parameters\.(.+)$`) 300 301 // GetTemplateParameterName returns the parameter name from the template variable. 302 func (m *Manifest) GetTemplateParameterName(value string) (string, bool) { 303 matches := templatedParameterRegex.FindStringSubmatch(value) 304 if len(matches) < 2 { 305 return "", false 306 } 307 308 parameterName := matches[1] 309 return parameterName, true 310 } 311 312 // GetTemplatedOutputs returns the output definitions for any bundle level outputs 313 // that have been templated, keyed by the output name. 314 func (m *Manifest) GetTemplatedOutputs() OutputDefinitions { 315 outputs := make(OutputDefinitions, len(m.TemplateVariables)) 316 for _, tmplVar := range m.TemplateVariables { 317 if name, ok := m.getTemplateOutputName(tmplVar); ok { 318 outputDef, ok := m.Outputs[name] 319 if !ok { 320 // Only return bundle level definitions 321 continue 322 } 323 outputs[name] = outputDef 324 } 325 } 326 return outputs 327 } 328 329 // GetTemplatedOutputs returns the output definitions for any bundle level outputs 330 // that have been templated, keyed by "DEPENDENCY.OUTPUT". 331 func (m *Manifest) GetTemplatedDependencyOutputs() DependencyOutputReferences { 332 outputs := make(DependencyOutputReferences, len(m.TemplateVariables)) 333 for _, tmplVar := range m.TemplateVariables { 334 if dep, output, ok := m.getTemplateDependencyOutputName(tmplVar); ok { 335 ref := DependencyOutputReference{ 336 Dependency: dep, 337 Output: output, 338 } 339 outputs[ref.String()] = ref 340 } 341 } 342 return outputs 343 } 344 345 // GetTemplatedParameters returns the output definitions for any bundle level outputs 346 // that have been templated, keyed by the output name. 347 func (m *Manifest) GetTemplatedParameters() ParameterDefinitions { 348 parameters := make(ParameterDefinitions, len(m.TemplateVariables)) 349 for _, tmplVar := range m.TemplateVariables { 350 if name, ok := m.GetTemplateParameterName(tmplVar); ok { 351 parameterDef, ok := m.Parameters[name] 352 if !ok { 353 // Only return bundle level definitions 354 continue 355 } 356 parameters[name] = parameterDef 357 } 358 } 359 return parameters 360 } 361 362 // DetermineDependenciesExtensionUsed looks for how dependencies are used 363 // by the bundle and which version of the dependency extension can be used. 364 func (m *Manifest) DetermineDependenciesExtensionUsed() string { 365 // Check if v2 deps are explicitly specified 366 for _, ext := range m.Required { 367 if ext.Name == cnab.DependenciesV2ExtensionShortHand || 368 ext.Name == cnab.DependenciesV2ExtensionKey { 369 return cnab.DependenciesV2ExtensionKey 370 } 371 } 372 373 // Check each dependency for use of v2 only features 374 for _, dep := range m.Dependencies.Requires { 375 if dep.UsesV2Features() { 376 return cnab.DependenciesV2ExtensionKey 377 } 378 } 379 380 // Check if the bundle declares that it can satisfy a v2 dependency 381 if m.Dependencies.Provides != nil { 382 return cnab.DependenciesV2ExtensionKey 383 } 384 385 if len(m.Dependencies.Requires) > 0 { 386 // Dependencies are declared but only use v1 features 387 return cnab.DependenciesV1ExtensionKey 388 } 389 390 // No dependencies are used at all 391 return "" 392 } 393 394 type CustomDefinitions map[string]interface{} 395 396 func (cd *CustomDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error { 397 raw, err := yaml.UnmarshalMap(unmarshal) 398 if err != nil { 399 return err 400 } 401 *cd = raw 402 return nil 403 } 404 405 type DependencyOutputReference struct { 406 Dependency string 407 Output string 408 } 409 410 func (r DependencyOutputReference) String() string { 411 return fmt.Sprintf("%s.%s", r.Dependency, r.Output) 412 } 413 414 type DependencyOutputReferences map[string]DependencyOutputReference 415 416 // DependencyProvider specifies how the current bundle can be used to satisfy a dependency. 417 type DependencyProvider struct { 418 // Interface declares the bundle interface that the current bundle provides. 419 Interface InterfaceDeclaration `yaml:"interface,omitempty"` 420 } 421 422 // InterfaceDeclaration declares that the current bundle supports the specified bundle interface 423 // Reserved for future use. Right now we only use an interface id, but could support other fields later. 424 type InterfaceDeclaration struct { 425 // ID is the URI of the interface that this bundle provides. Usually a well-known name defined by Porter or CNAB. 426 ID string `yaml:"id,omitempty"` 427 } 428 429 // ParameterDefinitions allows us to represent parameters as a list in the YAML 430 // and work with them as a map internally 431 type ParameterDefinitions map[string]ParameterDefinition 432 433 func (pd ParameterDefinitions) MarshalYAML() (interface{}, error) { 434 raw := make([]ParameterDefinition, 0, len(pd)) 435 436 for _, param := range pd { 437 raw = append(raw, param) 438 } 439 440 return raw, nil 441 } 442 443 func (pd *ParameterDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error { 444 var raw []ParameterDefinition 445 err := unmarshal(&raw) 446 if err != nil { 447 return err 448 } 449 450 if *pd == nil { 451 *pd = make(map[string]ParameterDefinition, len(raw)) 452 } 453 454 for _, item := range raw { 455 (*pd)[item.Name] = item 456 } 457 458 return nil 459 } 460 461 var _ bundle.Scoped = &ParameterDefinition{} 462 463 // ParameterDefinition defines a single parameter for a CNAB bundle 464 type ParameterDefinition struct { 465 Name string `yaml:"name"` 466 Sensitive bool `yaml:"sensitive"` 467 Source ParameterSource `yaml:"source,omitempty"` 468 469 // These fields represent a subset of bundle.Parameter as defined in cnabio/cnab-go, 470 // minus the 'Description' field (definition.Schema's will be used) and `Definition` field 471 ApplyTo []string `yaml:"applyTo,omitempty"` 472 Destination Location `yaml:",inline,omitempty"` 473 474 definition.Schema `yaml:",inline"` 475 476 // IsState identifies if the parameter was generated from a state variable 477 IsState bool `yaml:"-"` 478 } 479 480 func (pd *ParameterDefinition) GetApplyTo() []string { 481 return pd.ApplyTo 482 } 483 484 func (pd *ParameterDefinition) Validate() error { 485 var result *multierror.Error 486 487 if pd.Name == "" { 488 result = multierror.Append(result, errors.New("parameter name is required")) 489 } 490 491 // Porter supports declaring a parameter of type: "file", 492 // which we will convert to the appropriate bundle.Parameter type in adapter.go 493 // Here, we copy the ParameterDefinition and make the same modification before validation 494 pdCopy := pd.DeepCopy() 495 if pdCopy.Type == "file" { 496 if pd.Destination.Path == "" { 497 result = multierror.Append(result, fmt.Errorf("no destination path supplied for parameter %s", pd.Name)) 498 } 499 pdCopy.Type = "string" 500 pdCopy.ContentEncoding = "base64" 501 } 502 503 // Validate the Parameter Definition schema itself 504 if _, err := pdCopy.Schema.ValidateSchema(); err != nil { 505 return multierror.Append(result, fmt.Errorf("encountered an error while validating definition for parameter %q: %w", pdCopy.Name, err)) 506 } 507 508 if pdCopy.Default != nil { 509 schemaValidationErrs, err := pdCopy.Schema.Validate(pdCopy.Default) 510 if err != nil { 511 result = multierror.Append(result, fmt.Errorf("encountered error while validating parameter %s: %w", pdCopy.Name, err)) 512 } 513 for _, schemaValidationErr := range schemaValidationErrs { 514 result = multierror.Append(result, fmt.Errorf("encountered an error validating the default value %v for parameter %q: %s", pdCopy.Default, pdCopy.Name, schemaValidationErr.Error)) 515 } 516 } 517 518 return result.ErrorOrNil() 519 } 520 521 // DeepCopy copies a ParameterDefinition and returns the copy 522 func (pd *ParameterDefinition) DeepCopy() *ParameterDefinition { 523 p2 := *pd 524 p2.ApplyTo = make([]string, len(pd.ApplyTo)) 525 copy(p2.ApplyTo, pd.ApplyTo) 526 return &p2 527 } 528 529 // AppliesTo returns a boolean value specifying whether or not 530 // the Parameter applies to the provided action 531 func (pd *ParameterDefinition) AppliesTo(action string) bool { 532 return bundle.AppliesTo(pd, action) 533 } 534 535 // exemptFromInstall returns true if a parameter definition: 536 // - has an output source (which will not exist prior to install) 537 // - doesn't already have applyTo specified 538 // - doesn't have a default value 539 func (pd *ParameterDefinition) exemptFromInstall() bool { 540 return pd.Source.Output != "" && pd.ApplyTo == nil && pd.Default == nil 541 } 542 543 // UpdateApplyTo updates a parameter definition's applyTo section 544 // based on the provided manifest 545 func (pd *ParameterDefinition) UpdateApplyTo(m *Manifest) { 546 if pd.exemptFromInstall() { 547 applyTo := []string{cnab.ActionUninstall} 548 // The core action "Upgrade" is technically still optional 549 // so only add it if it is declared in the manifest 550 if m.Upgrade != nil { 551 applyTo = append(applyTo, cnab.ActionUpgrade) 552 } 553 // Add all custom actions 554 for action := range m.CustomActions { 555 applyTo = append(applyTo, action) 556 } 557 sort.Strings(applyTo) 558 pd.ApplyTo = applyTo 559 } 560 } 561 562 type ParameterSource struct { 563 Dependency string `yaml:"dependency,omitempty"` 564 Output string `yaml:"output"` 565 } 566 567 // CredentialDefinitions allows us to represent credentials as a list in the YAML 568 // and work with them as a map internally 569 type CredentialDefinitions map[string]CredentialDefinition 570 571 func (cd CredentialDefinitions) MarshalYAML() (interface{}, error) { 572 raw := make([]CredentialDefinition, 0, len(cd)) 573 574 for _, cred := range cd { 575 raw = append(raw, cred) 576 } 577 578 return raw, nil 579 } 580 581 func (cd *CredentialDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error { 582 var raw []CredentialDefinition 583 err := unmarshal(&raw) 584 if err != nil { 585 return err 586 } 587 588 if *cd == nil { 589 *cd = make(map[string]CredentialDefinition, len(raw)) 590 } 591 592 for _, item := range raw { 593 (*cd)[item.Name] = item 594 } 595 596 return nil 597 } 598 599 // CredentialDefinition represents the structure or fields of a credential parameter 600 type CredentialDefinition struct { 601 Name string `yaml:"name"` 602 Description string `yaml:"description,omitempty"` 603 604 // Required specifies if the credential must be specified for applicable actions. Defaults to true. 605 Required bool `yaml:"required,omitempty"` 606 607 // ApplyTo lists the actions to which the credential applies. When unset, defaults to all actions. 608 ApplyTo []string `yaml:"applyTo,omitempty"` 609 610 Location `yaml:",inline"` 611 } 612 613 func (cd *CredentialDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error { 614 type rawCreds CredentialDefinition 615 rawCred := rawCreds{ 616 Name: cd.Name, 617 Description: cd.Description, 618 Required: true, 619 Location: cd.Location, 620 } 621 622 if err := unmarshal(&rawCred); err != nil { 623 return err 624 } 625 626 *cd = CredentialDefinition(rawCred) 627 628 return nil 629 } 630 631 // Location represents a Parameter or Credential location in an InvocationImage 632 type Location struct { 633 Path string `yaml:"path,omitempty"` 634 EnvironmentVariable string `yaml:"env,omitempty"` 635 } 636 637 func (l Location) IsEmpty() bool { 638 var empty Location 639 return l == empty 640 } 641 642 type MixinDeclaration struct { 643 Name string 644 Version *semver.Constraints 645 Config interface{} 646 } 647 648 func extractVersionFromName(name string) (string, *semver.Constraints, error) { 649 parts := strings.Split(name, "@") 650 651 // if there isn't a version in the name, just stop! 652 if len(parts) == 1 { 653 return name, nil, nil 654 } 655 656 // if we somehow got more parts than expected! 657 if len(parts) != 2 { 658 return "", nil, fmt.Errorf("expected name@version, got: %s", name) 659 } 660 661 version, err := semver.NewConstraint(parts[1]) 662 if err != nil { 663 return "", nil, err 664 } 665 666 return parts[0], version, nil 667 } 668 669 // UnmarshalYAML allows mixin declarations to either be a normal list of strings 670 // mixins: 671 // - exec 672 // - helm3 673 // or allow some entries to have config data defined 674 // - az: 675 // extensions: 676 // - iot 677 // 678 // for each type, we can optionally support a version number in the name field 679 // mixins: 680 // - exec@2.1.1 681 // or 682 // - az@2.1.1 683 // extensions: 684 // - iot 685 func (m *MixinDeclaration) UnmarshalYAML(unmarshal func(interface{}) error) error { 686 // First try to just read the mixin name 687 var mixinNameOnly string 688 err := unmarshal(&mixinNameOnly) 689 if err == nil { 690 name, version, err := extractVersionFromName(mixinNameOnly) 691 if err != nil { 692 return fmt.Errorf("invalid mixin name/version: %w", err) 693 } 694 m.Name = name 695 m.Version = version 696 m.Config = nil 697 return nil 698 } 699 700 // Next try to read a mixin name with config defined 701 mixinWithConfig := map[string]interface{}{} 702 err = unmarshal(&mixinWithConfig) 703 if err != nil { 704 return fmt.Errorf("could not unmarshal raw yaml of mixin declarations: %w", err) 705 } 706 707 if len(mixinWithConfig) == 0 { 708 return errors.New("mixin declaration was empty") 709 } else if len(mixinWithConfig) > 1 { 710 return errors.New("mixin declaration contained more than one mixin") 711 } 712 713 for mixinName, config := range mixinWithConfig { 714 name, version, err := extractVersionFromName(mixinName) 715 if err != nil { 716 return fmt.Errorf("invalid mixin name/version: %w", err) 717 } 718 m.Name = name 719 m.Version = version 720 m.Config = config 721 break // There is only one mixin anyway but break for clarity 722 } 723 return nil 724 } 725 726 // MarshalYAML allows mixin declarations to either be a normal list of strings 727 // mixins: 728 // - exec 729 // - helm3 730 // or allow some entries to have config data defined 731 // - az: 732 // extensions: 733 // - iot 734 func (m MixinDeclaration) MarshalYAML() (interface{}, error) { 735 if m.Config == nil { 736 return m.Name, nil 737 } 738 739 raw := map[string]interface{}{ 740 m.Name: m.Config, 741 } 742 return raw, nil 743 } 744 745 type MappedImage struct { 746 Description string `yaml:"description"` 747 ImageType string `yaml:"imageType"` 748 Repository string `yaml:"repository"` 749 Digest string `yaml:"digest,omitempty"` 750 Size uint64 `yaml:"size,omitempty"` 751 MediaType string `yaml:"mediaType,omitempty"` 752 Labels map[string]string `yaml:"labels,omitempty"` 753 Tag string `yaml:"tag,omitempty"` 754 } 755 756 func (mi *MappedImage) Validate() error { 757 if mi.Digest != "" { 758 if _, err := digest.Parse(mi.Digest); err != nil { 759 return err 760 } 761 } 762 763 if _, err := cnab.ParseOCIReference(mi.Repository); err != nil { 764 return err 765 } 766 767 return nil 768 } 769 770 func (mi *MappedImage) ToOCIReference() (cnab.OCIReference, error) { 771 ref, err := cnab.ParseOCIReference(mi.Repository) 772 if err != nil { 773 return cnab.OCIReference{}, err 774 } 775 776 if mi.Digest != "" { 777 refWithDigest, err := ref.WithDigest(digest.Digest(mi.Digest)) 778 if err != nil { 779 return cnab.OCIReference{}, fmt.Errorf("failed to create a new reference with digest for repository %s: %w", mi.Repository, err) 780 } 781 782 return refWithDigest, nil 783 } 784 785 if mi.Tag != "" { 786 refWithTag, err := ref.WithTag(mi.Tag) 787 if err != nil { 788 return cnab.OCIReference{}, fmt.Errorf("failed to create a new reference with tag for repository %s: %w", mi.Repository, err) 789 } 790 791 return refWithTag, nil 792 } 793 794 return ref, nil 795 } 796 797 // Dependencies defies both v2 and v1 dependencies. 798 // Dependencies v1 is a subset of Dependencies v2. 799 type Dependencies struct { 800 // Requires specifies bundles required by the current bundle. 801 Requires []*Dependency `yaml:"requires,omitempty"` 802 803 // Provides specifies how the bundle can satisfy a dependency. 804 // This declares that the bundle can provide a dependency that another bundle requires. 805 Provides *DependencyProvider `yaml:"provides,omitempty"` 806 } 807 808 // Dependency defines a parent child relationship between this bundle (parent) and the specified bundle (child). 809 type Dependency struct { 810 // Name of the dependency, used to reference the dependency from other parts of 811 // the bundle such as the template syntax, bundle.dependencies.NAME 812 Name string `yaml:"name"` 813 814 // Bundle specifies criteria for selecting a bundle to satisfy the dependency. 815 Bundle BundleCriteria `yaml:"bundle"` 816 817 // Sharing is a set of rules for sharing a dependency with other bundles. 818 Sharing SharingCriteria `yaml:"sharing,omitempty"` 819 820 // Parameters is a map of values, keyed by the destination where the value is the 821 // source, to pass from the bundle to the dependency. May either be a hard-coded 822 // value, or a template value such as ${bundle.parameters.NAME}. The key is the 823 // dependency's parameter name, and the value is the data being passed to the 824 // dependency parameter. 825 Parameters map[string]string `yaml:"parameters,omitempty"` 826 827 // Credentials is a map of values, keyed by the destination where the value is 828 // the source, to pass from the bundle to the dependency. May either be a 829 // hard-coded value, or a template value such as ${bundle.credentials.NAME}. The 830 // key is the dependency's credential name, and the value is the data being 831 // passed to the dependency credential. 832 Credentials map[string]string `yaml:"credentials,omitempty"` 833 834 // Outputs is a map of values, keyed by the destination where the value is the 835 // source, to pass from the dependency and promote to a bundle-level outputs of 836 // the parent bundle. May either be the name of an output from the dependency, or 837 // a template value such as ${outputs.NAME} where the outputs variable holds the 838 // current dependency's outputs. The long form of the template syntax, 839 // ${bundle.dependencies.DEP.outputs.NAME}, is also supported. The key is the 840 // parent bundle's output name, and the value is the data being passed to the 841 // dependency parameter. 842 Outputs map[string]string `yaml:"outputs,omitempty"` 843 } 844 845 type DependencySource string 846 847 // BundleCriteria criteria for selecting a bundle to satisfy a dependency. 848 type BundleCriteria struct { 849 // Reference is an OCI reference to a bundle for use as the default implementation of the bundle. 850 // It should be in the format REGISTRY/NAME:TAG 851 Reference string `yaml:"reference"` 852 853 // "When constraint checking is used for checks or validation 854 // it will follow a different set of rules that are common for ranges with tools like npm/js and Rust/Cargo. 855 // This includes considering prereleases to be invalid if the ranges does not include one. 856 // If you want to have it include pre-releases a simple solution is to include -0 in your range." 857 // https://github.com/Masterminds/semver/blob/master/README.md#checking-version-constraints 858 Version string `yaml:"version,omitempty"` 859 860 // Interface specifies criteria for allowing a bundle to satisfy a dependency. 861 Interface *BundleInterface `yaml:"interface,omitempty"` 862 } 863 864 // BundleInterface specifies how a bundle can satisfy a dependency. 865 // Porter always infers a base interface based on how the dependency is used in porter.yaml 866 // but this allows the bundle author to extend it and add additional restrictions. 867 // Either bundle or reference may be specified but not both. 868 type BundleInterface struct { 869 // ID is the identifier or name of the bundle interface. It should be matched 870 // against the Dependencies.Provides.Interface.ID to determine if two interfaces 871 // are equivalent. 872 ID string `yaml:"id,omitempty"` 873 874 // Reference specifies an OCI reference to a bundle to use as the interface on top of how the bundle is used. 875 Reference string `yaml:"reference,omitempty"` 876 877 // Document specifies additional constraints that should be added to the bundle interface. 878 // By default, Porter only requires the name and the type to match, additional jsonschema values can be specified to restrict matching bundles even further. 879 // The value should be a jsonschema document containing relevant sub-documents from a bundle.json that should be applied to the base bundle interface. 880 Document *BundleInterfaceDocument `yaml:"document,omitempty"` 881 } 882 883 // BundleInterfaceDocument specifies the interface that a bundle must support in 884 // order to satisfy a dependency. 885 type BundleInterfaceDocument struct { 886 // Parameters that are defined on the interface. 887 Parameters ParameterDefinitions `yaml:"parameters,omitempty"` 888 889 // Credentials that are defined on the interface. 890 Credentials CredentialDefinitions `yaml:"credentials,omitempty"` 891 892 // Outputs that are defined on the interface. 893 Outputs OutputDefinitions `yaml:"outputs,omitempty"` 894 } 895 896 // SharingCriteria is a set of rules for sharing a dependency with other bundles. 897 type SharingCriteria struct { 898 // Mode defines how a dependency can be shared. 899 // - false: The dependency cannot be shared, even within the same dependency graph. 900 // - true: The dependency is shared with other bundles who defined the dependency 901 // with the same sharing group. This is the default mode. 902 Mode bool `yaml:"mode"` 903 904 // Group defines matching criteria for determining if two dependencies are in the same sharing group. 905 Group SharingGroup `yaml:"group,omitempty"` 906 } 907 908 // GetEffectiveMode returns the mode, taking into account the default value when 909 // no mode is specified. 910 func (s SharingCriteria) GetEffectiveMode() bool { 911 return s.Mode 912 } 913 914 // SharingGroup defines a set of characteristics for sharing a dependency with 915 // other bundles. 916 // Reserved for future use: We can add more characteristics later to expands how we share if needed 917 type SharingGroup struct { 918 // Name of the sharing group. The name of the group must match for two bundles to share the same dependency. 919 Name string `yaml:"name"` 920 } 921 922 func (d *Dependency) Validate(cxt *portercontext.Context) error { 923 if d.Name == "" { 924 return errors.New("dependency name is required") 925 } 926 927 if d.Bundle.Reference == "" { 928 return fmt.Errorf("reference is required for dependency %q", d.Name) 929 } 930 931 ref, err := cnab.ParseOCIReference(d.Bundle.Reference) 932 if err != nil { 933 return fmt.Errorf("invalid reference %s for dependency %s: %w", d.Bundle.Reference, d.Name, err) 934 } 935 936 if ref.IsRepositoryOnly() && d.Bundle.Version == "" { 937 return fmt.Errorf("reference for dependency %q can specify only a repository, without a digest or tag, when a version constraint is specified", d.Name) 938 } 939 940 return nil 941 } 942 943 // UsesV2Features returns true if the dependency uses features from v2 of 944 // Porter's implementation of dependencies, and returns false if the v1 945 // implementation of dependencies would suffice. 946 func (d *Dependency) UsesV2Features() bool { 947 // Sharing was added in v2 948 if d.Sharing != (SharingCriteria{}) { 949 return true 950 } 951 952 // Credentials and output mapping was added in v2 953 if len(d.Credentials) > 0 || len(d.Outputs) > 0 { 954 return true 955 } 956 957 // Bundle interfaces was added in v2 958 if d.Bundle.Interface != nil { 959 return true 960 } 961 962 // Anything else can be handled with v1 963 return false 964 } 965 966 type CustomActionDefinition struct { 967 Description string `yaml:"description,omitempty"` 968 ModifiesResources bool `yaml:"modifies,omitempty"` 969 Stateless bool `yaml:"stateless,omitempty"` 970 } 971 972 // OutputDefinitions allows us to represent parameters as a list in the YAML 973 // and work with them as a map internally 974 type OutputDefinitions map[string]OutputDefinition 975 976 func (od OutputDefinitions) MarshalYAML() (interface{}, error) { 977 raw := make([]OutputDefinition, 0, len(od)) 978 979 for _, output := range od { 980 raw = append(raw, output) 981 } 982 983 return raw, nil 984 } 985 986 func (od *OutputDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error { 987 var raw []OutputDefinition 988 err := unmarshal(&raw) 989 if err != nil { 990 return err 991 } 992 993 if *od == nil { 994 *od = make(map[string]OutputDefinition, len(raw)) 995 } 996 997 for _, item := range raw { 998 (*od)[item.Name] = item 999 } 1000 1001 return nil 1002 } 1003 1004 // OutputDefinition defines a single output for a CNAB 1005 type OutputDefinition struct { 1006 Name string `yaml:"name"` 1007 ApplyTo []string `yaml:"applyTo,omitempty"` 1008 Sensitive bool `yaml:"sensitive"` 1009 1010 // This is not in the CNAB spec, but it allows a mixin to create a file 1011 // and porter will take care of making it a proper output. 1012 Path string `yaml:"path,omitempty"` 1013 1014 definition.Schema `yaml:",inline"` 1015 1016 // IsState identifies if the output was generated from a state variable 1017 IsState bool `yaml:"-"` 1018 } 1019 1020 // DeepCopy copies a ParameterDefinition and returns the copy 1021 func (od *OutputDefinition) DeepCopy() *OutputDefinition { 1022 o2 := *od 1023 o2.ApplyTo = make([]string, len(od.ApplyTo)) 1024 copy(o2.ApplyTo, od.ApplyTo) 1025 return &o2 1026 } 1027 1028 func (od *OutputDefinition) Validate() error { 1029 var result *multierror.Error 1030 1031 if od.Name == "" { 1032 return errors.New("output name is required") 1033 } 1034 1035 // Porter supports declaring an output of type: "file", 1036 // which we will convert to the appropriate type in adapter.go 1037 // Here, we copy the definition and make the same modification before validation 1038 odCopy := od.DeepCopy() 1039 if odCopy.Type == "file" { 1040 if od.Path == "" { 1041 result = multierror.Append(result, fmt.Errorf("no path supplied for output %s", od.Name)) 1042 } 1043 odCopy.Type = "string" 1044 odCopy.ContentEncoding = "base64" 1045 } 1046 1047 // Validate the Output Definition schema itself 1048 if _, err := odCopy.Schema.ValidateSchema(); err != nil { 1049 return multierror.Append(result, fmt.Errorf("encountered an error while validating definition for output %q: %w", odCopy.Name, err)) 1050 } 1051 1052 if odCopy.Default != nil { 1053 schemaValidationErrs, err := odCopy.Schema.Validate(odCopy.Default) 1054 if err != nil { 1055 result = multierror.Append(result, fmt.Errorf("encountered error while validating output %s: %w", odCopy.Name, err)) 1056 } 1057 for _, schemaValidationErr := range schemaValidationErrs { 1058 result = multierror.Append(result, fmt.Errorf("encountered an error validating the default value %v for output %q: %s", odCopy.Default, odCopy.Name, schemaValidationErr.Error)) 1059 } 1060 } 1061 1062 return result.ErrorOrNil() 1063 } 1064 1065 type BundleOutput struct { 1066 Name string `yaml:"name"` 1067 Path string `yaml:"path"` 1068 EnvironmentVariable string `yaml:"env"` 1069 } 1070 1071 type Steps []*Step 1072 1073 func (s Steps) Validate(m *Manifest) error { 1074 for i, step := range s { 1075 err := step.Validate(m) 1076 if err != nil { 1077 return fmt.Errorf("failed to validate %s step: %s", humanize.Ordinal(i+1), err) 1078 } 1079 } 1080 return nil 1081 } 1082 1083 type Step struct { 1084 Data map[string]interface{} `yaml:",inline"` 1085 } 1086 1087 func (s *Step) Validate(m *Manifest) error { 1088 if s == nil { 1089 return errors.New("found an empty step") 1090 } 1091 if len(s.Data) == 0 { 1092 return errors.New("no mixin specified") 1093 } 1094 if len(s.Data) > 1 { 1095 return errors.New("malformed step, possibly incorrect indentation") 1096 } 1097 1098 mixinDeclared := false 1099 mixinType := s.GetMixinName() 1100 for _, mixin := range m.Mixins { 1101 if mixin.Name == mixinType { 1102 mixinDeclared = true 1103 break 1104 } 1105 } 1106 if !mixinDeclared { 1107 return fmt.Errorf("mixin (%s) was not declared", mixinType) 1108 } 1109 1110 if _, err := s.GetDescription(); err != nil { 1111 return err 1112 } 1113 1114 return nil 1115 } 1116 1117 // GetDescription returns a description of the step. 1118 // Every step must have this property. 1119 func (s *Step) GetDescription() (string, error) { 1120 if s.Data == nil { 1121 return "", errors.New("empty step data") 1122 } 1123 1124 mixinName := s.GetMixinName() 1125 children := s.Data[mixinName] 1126 m, ok := children.(map[string]interface{}) 1127 if !ok { 1128 return "", fmt.Errorf("invalid mixin type (%T) for mixin step (%s)", children, mixinName) 1129 } 1130 d := m["description"] 1131 if d == nil { 1132 return "", nil 1133 } 1134 desc, ok := d.(string) 1135 if !ok { 1136 return "", fmt.Errorf("invalid description type (%T) for mixin step (%s)", desc, mixinName) 1137 } 1138 1139 return desc, nil 1140 } 1141 1142 func (s *Step) GetMixinName() string { 1143 var mixinName string 1144 for k := range s.Data { 1145 mixinName = k 1146 } 1147 return mixinName 1148 } 1149 1150 func UnmarshalManifest(cxt *portercontext.Context, manifestData []byte) (*Manifest, error) { 1151 // Unmarshal the manifest into the normal struct 1152 manifest := &Manifest{} 1153 err := yaml.Unmarshal(manifestData, &manifest) 1154 if err != nil { 1155 return nil, fmt.Errorf("error unmarshaling the typed manifest: %w", err) 1156 } 1157 1158 // Do a second pass to identify custom actions, which don't have yaml tags since they are dynamic 1159 // 1. Marshal the manifest a second time into a plain map 1160 // 2. Remove keys for fields that are already mapped with yaml tags 1161 // 3. Anything left is a custom action 1162 1163 // Marshal the manifest into an untyped map 1164 unmappedData := make(map[string]interface{}) 1165 err = yaml.Unmarshal(manifestData, &unmappedData) 1166 if err != nil { 1167 return nil, fmt.Errorf("error unmarshaling the untyped manifest: %w", err) 1168 } 1169 1170 // Use reflection to figure out which fields are on the manifest and have yaml tags 1171 objValue := reflect.ValueOf(manifest).Elem() 1172 knownFields := map[string]reflect.Value{} 1173 for i := 0; i != objValue.NumField(); i++ { 1174 tagName := strings.Split(objValue.Type().Field(i).Tag.Get("yaml"), ",")[0] 1175 knownFields[tagName] = objValue.Field(i) 1176 } 1177 1178 // Remove any fields that have yaml tags 1179 for key := range unmappedData { 1180 if _, found := knownFields[key]; found { 1181 delete(unmappedData, key) 1182 } 1183 // Delete known deprecated fields with no yaml tags 1184 if key == "invocationImage" || key == "tag" { 1185 fmt.Fprintf(cxt.Out, "WARNING: The %q field has been deprecated and can no longer be user-specified; ignoring.\n", key) 1186 delete(unmappedData, key) 1187 } 1188 } 1189 1190 // Marshal the remaining keys in the unmappedData as custom actions and append them to the typed manifest 1191 manifest.CustomActions = make(map[string]Steps, len(unmappedData)) 1192 for key, chunk := range unmappedData { 1193 chunkData, err := yaml.Marshal(chunk) 1194 if err != nil { 1195 return nil, fmt.Errorf("error remarshaling custom action %s: %w", key, err) 1196 } 1197 1198 steps := Steps{} 1199 err = yaml.Unmarshal(chunkData, &steps) 1200 if err != nil { 1201 return nil, fmt.Errorf("error unmarshaling custom action %s: %w", key, err) 1202 } 1203 1204 manifest.CustomActions[key] = steps 1205 } 1206 1207 return manifest, nil 1208 } 1209 1210 // SetDefaults updates the manifest with default values where not populated 1211 func (m *Manifest) SetDefaults() error { 1212 return m.SetBundleImageAndReference("") 1213 } 1214 1215 // SetBundleImageAndReference sets the bundle image name and the 1216 // bundle reference on the manifest per the provided reference or via the 1217 // registry or name values on the manifest. 1218 func (m *Manifest) SetBundleImageAndReference(ref string) error { 1219 if ref != "" { 1220 m.Reference = ref 1221 } 1222 1223 if m.Reference == "" && m.Registry != "" { 1224 repo, err := cnab.ParseOCIReference(path.Join(m.Registry, m.Name)) 1225 if err != nil { 1226 return fmt.Errorf("invalid bundle reference %s: %w", path.Join(m.Registry, m.Name), err) 1227 } 1228 m.Reference = repo.Repository() 1229 } 1230 1231 bundleRef, err := cnab.ParseOCIReference(m.Reference) 1232 if err != nil { 1233 return fmt.Errorf("invalid bundle reference %s: %w", m.Reference, err) 1234 } 1235 1236 dockerTag, err := m.getDockerTagFromBundleRef(bundleRef) 1237 if err != nil { 1238 return fmt.Errorf("unable to derive docker tag from bundle reference %q: %w", m.Reference, err) 1239 } 1240 1241 // If the docker tag is initially missing from bundleTag, update with 1242 // returned dockerTag 1243 if !bundleRef.HasTag() { 1244 bundleRef, err = bundleRef.WithTag(dockerTag) 1245 if err != nil { 1246 return fmt.Errorf("could not set bundle tag to %q: %w", dockerTag, err) 1247 } 1248 m.Reference = bundleRef.String() 1249 } 1250 1251 installerImage, err := cnab.CalculateTemporaryImageTag(bundleRef) 1252 if err != nil { 1253 return err 1254 } 1255 1256 m.Image = installerImage.String() 1257 return nil 1258 } 1259 1260 // getDockerTagFromBundleRef returns the Docker tag portion of the bundle tag, 1261 // using the bundle version as a fallback 1262 func (m *Manifest) getDockerTagFromBundleRef(bundleRef cnab.OCIReference) (string, error) { 1263 // If the manifest has a DockerTag override already set (e.g. on publish), use this 1264 if m.DockerTag != "" { 1265 return m.DockerTag, nil 1266 } 1267 1268 if bundleRef.HasTag() { 1269 return bundleRef.Tag(), nil 1270 } 1271 1272 if bundleRef.HasDigest() { 1273 return "", errors.New("invalid bundle tag format, must be an OCI image tag") 1274 } 1275 1276 // Docker tag is missing from the provided bundle tag, so default it 1277 // to use the manifest version prefixed with v 1278 // Example: bundle version is 1.0.0, so the bundle tag is v1.0.0 1279 newRef, err := bundleRef.WithVersion(m.Version) 1280 if err != nil { 1281 return "", err 1282 } 1283 return newRef.Tag(), nil 1284 } 1285 1286 // ResolvePath resolves a path specified in the Porter manifest into 1287 // an absolute path, assuming the current directory is /cnab/app. 1288 // Returns an empty string when the specified value is empty. 1289 func ResolvePath(value string) string { 1290 if value == "" { 1291 return "" 1292 } 1293 1294 if path.IsAbs(value) { 1295 return value 1296 } 1297 1298 return path.Join("/cnab/app", value) 1299 } 1300 1301 func readFromFile(cxt *portercontext.Context, path string) ([]byte, error) { 1302 if exists, _ := cxt.FileSystem.Exists(path); !exists { 1303 return nil, fmt.Errorf("the specified porter configuration file %s does not exist", path) 1304 } 1305 1306 data, err := cxt.FileSystem.ReadFile(path) 1307 if err != nil { 1308 return nil, fmt.Errorf("could not read manifest at %q: %w", path, err) 1309 } 1310 return data, nil 1311 } 1312 1313 func readFromURL(path string) ([]byte, error) { 1314 resp, err := http.Get(path) 1315 if err != nil { 1316 return nil, fmt.Errorf("could not reach url %s: %w", path, err) 1317 } 1318 1319 defer resp.Body.Close() 1320 data, err := io.ReadAll(resp.Body) 1321 if err != nil { 1322 return nil, fmt.Errorf("could not read from url %s: %w", path, err) 1323 } 1324 return data, nil 1325 } 1326 1327 func ReadManifestData(cxt *portercontext.Context, path string) ([]byte, error) { 1328 if strings.HasPrefix(path, "http") { 1329 return readFromURL(path) 1330 } else { 1331 return readFromFile(cxt, path) 1332 } 1333 } 1334 1335 // ReadManifest determines if specified path is a URL or a filepath. 1336 // After reading the data in the path it returns a Manifest and any errors 1337 func ReadManifest(cxt *portercontext.Context, path string, config *config.Config) (*Manifest, error) { 1338 data, err := ReadManifestData(cxt, path) 1339 if err != nil { 1340 return nil, err 1341 } 1342 1343 m, err := UnmarshalManifest(cxt, data) 1344 if err != nil { 1345 return nil, fmt.Errorf("unsupported property set or a custom action is defined incorrectly: %w", err) 1346 } 1347 1348 tmplResult, err := m.ScanManifestTemplating(data, config) 1349 if err != nil { 1350 return nil, err 1351 } 1352 1353 m.ManifestPath = path 1354 m.TemplateVariables = tmplResult.Variables 1355 1356 return m, nil 1357 } 1358 1359 // templateScanResult is the result of parsing the mustache templating used in the manifest. 1360 type templateScanResult struct { 1361 // Variables used in the template, e.g. {{ bundle.parameters.NAME }} 1362 Variables []string 1363 } 1364 1365 func (m *Manifest) GetTemplatePrefix() string { 1366 if m.SchemaVersion == "" { 1367 // Super-old bundles use the mustache default 1368 return "" 1369 } 1370 1371 // In 1.0.0-alpha.2+, the prefix is ${}. Beforehand it was {{}} 1372 v, err := semver.NewVersion(m.SchemaVersion) 1373 if err == nil { 1374 if v.GreaterThan(semver.MustParse("v1.0.0-alpha.1")) { 1375 // Change the delimiter 1376 return TemplateDelimiterPrefix 1377 } 1378 } 1379 1380 // Fallback to the mustache default if we can't determine the schema version 1381 return "" 1382 } 1383 1384 func (m *Manifest) ScanManifestTemplating(data []byte, config *config.Config) (templateScanResult, error) { 1385 // Handle outputs variable 1386 shortOutputVars, err := m.mapShortDependencyOutputVariables(config) 1387 if err != nil { 1388 return templateScanResult{}, fmt.Errorf("error parsing the templating used in the manifest: %w", err) 1389 } 1390 1391 vars, err := m.getTemplateVariables(string(data)) 1392 if err != nil { 1393 return templateScanResult{}, fmt.Errorf("error parsing the templating used in the manifest: %w", err) 1394 } 1395 1396 if config.IsFeatureEnabled(experimental.FlagDependenciesV2) { 1397 m.deduplicateAndFilterShortOutputVariables(shortOutputVars, vars) 1398 } 1399 1400 result := templateScanResult{ 1401 Variables: make([]string, 0, len(vars)), 1402 } 1403 for v := range vars { 1404 result.Variables = append(result.Variables, v) 1405 } 1406 1407 sort.Strings(result.Variables) 1408 return result, nil 1409 } 1410 1411 func (m *Manifest) mapShortDependencyOutputVariables(config *config.Config) ([]string, error) { 1412 shortOutputVars := []string{} 1413 if config.IsFeatureEnabled(experimental.FlagDependenciesV2) { 1414 for _, dep := range m.Dependencies.Requires { 1415 for outputName, output := range dep.Outputs { 1416 vars, err := m.getTemplateVariables(output) 1417 if err != nil { 1418 return nil, fmt.Errorf("error parsing the templating used for dependency %s output %s: %w", dep.Name, outputName, err) 1419 } 1420 1421 for tmplVar := range vars { 1422 outputTemplateName, ok := m.getTemplateDependencyShortOutputName(tmplVar) 1423 if ok { 1424 shortOutputVars = append(shortOutputVars, fmt.Sprintf("bundle.dependencies.%s.outputs.%s", dep.Name, outputTemplateName)) 1425 } 1426 } 1427 } 1428 } 1429 } 1430 1431 return shortOutputVars, nil 1432 } 1433 1434 func (m *Manifest) deduplicateAndFilterShortOutputVariables(shortOutputVars []string, vars map[string]struct{}) { 1435 for tmplVar := range vars { 1436 if strings.HasPrefix(tmplVar, "outputs.") { 1437 delete(vars, tmplVar) 1438 } 1439 } 1440 1441 for _, shortHandVar := range shortOutputVars { 1442 vars[shortHandVar] = struct{}{} 1443 } 1444 } 1445 1446 func (m *Manifest) getTemplateVariables(data string) (map[string]struct{}, error) { 1447 const disableHtmlEscaping = true 1448 templateSrc := m.GetTemplatePrefix() + string(data) 1449 tmpl, err := mustache.ParseStringRaw(templateSrc, disableHtmlEscaping) 1450 if err != nil { 1451 return nil, fmt.Errorf("error parsing the templating used in the manifest: %w", err) 1452 } 1453 1454 tags := tmpl.Tags() 1455 vars := map[string]struct{}{} // Keep track of unique variable names 1456 for _, tag := range tags { 1457 if tag.Type() != mustache.Variable { 1458 continue 1459 } 1460 1461 vars[tag.Name()] = struct{}{} 1462 } 1463 1464 return vars, nil 1465 } 1466 1467 // LoadManifestFrom reads and validates the manifest at the specified location, 1468 // and returns a populated Manifest structure. 1469 func LoadManifestFrom(ctx context.Context, config *config.Config, file string) (*Manifest, error) { 1470 ctx, log := tracing.StartSpan(ctx) 1471 defer log.EndSpan() 1472 1473 m, err := ReadManifest(config.Context, file, config) 1474 if err != nil { 1475 return nil, err 1476 } 1477 1478 if err = m.Validate(ctx, config); err != nil { 1479 return nil, err 1480 } 1481 1482 return m, nil 1483 } 1484 1485 // RequiredExtension represents a custom extension that is required 1486 // in order for a bundle to work correctly 1487 type RequiredExtension struct { 1488 Name string 1489 Config map[string]interface{} 1490 } 1491 1492 // UnmarshalYAML allows required extensions to either be a normal list of strings 1493 // required: 1494 // - docker 1495 // or allow some entries to have config data defined 1496 // - vpn: 1497 // name: mytrustednetwork 1498 func (r *RequiredExtension) UnmarshalYAML(unmarshal func(interface{}) error) error { 1499 // First try to just read the mixin name 1500 var extNameOnly string 1501 err := unmarshal(&extNameOnly) 1502 if err == nil { 1503 r.Name = extNameOnly 1504 r.Config = nil 1505 return nil 1506 } 1507 1508 // Next try to read a required extension with config defined 1509 extWithConfig := map[string]map[string]interface{}{} 1510 err = unmarshal(&extWithConfig) 1511 if err != nil { 1512 return fmt.Errorf("could not unmarshal raw yaml of required extensions: %w", err) 1513 } 1514 1515 if len(extWithConfig) == 0 { 1516 return errors.New("required extension was empty") 1517 } else if len(extWithConfig) > 1 { 1518 return errors.New("required extension contained more than one extension") 1519 } 1520 1521 for extName, config := range extWithConfig { 1522 r.Name = extName 1523 r.Config = config 1524 break // There is only one extension anyway but break for clarity 1525 } 1526 return nil 1527 } 1528 1529 // Convert a parameter name to an environment variable. 1530 // Anything more complicated should define the variable explicitly. 1531 func ParamToEnvVar(name string) string { 1532 name = strings.ToUpper(name) 1533 fixer := strings.NewReplacer("-", "_", ".", "_") 1534 return fixer.Replace(name) 1535 } 1536 1537 // GetParameterSourceForOutput builds the parameter source name used by Porter 1538 // internally for wiring up an output to a parameter. 1539 func GetParameterSourceForOutput(outputName string) string { 1540 return fmt.Sprintf("porter-%s-output", outputName) 1541 } 1542 1543 // GetParameterSourceForDependency builds the parameter source name used by Porter 1544 // internally for wiring up an dependency's output to a parameter. 1545 func GetParameterSourceForDependency(ref DependencyOutputReference) string { 1546 return fmt.Sprintf("porter-%s-%s-dep-output", ref.Dependency, ref.Output) 1547 } 1548 1549 type MaintainerDefinition struct { 1550 Name string `yaml:"name,omitempty"` 1551 Email string `yaml:"email,omitempty"` 1552 Url string `yaml:"url,omitempty"` 1553 } 1554 1555 // StateBag is the set of state files and variables that Porter should 1556 // track between bundle executions. 1557 type StateBag []StateVariable 1558 1559 type StateVariable struct { 1560 // Name of the state variable 1561 Name string `yaml:"name"` 1562 1563 // Description of the state variable and how it's used by the bundle 1564 Description string `yaml:"description,omitempty"` 1565 1566 // Mixin is the name of the mixin that manages the state variable. 1567 Mixin string `yaml:"mixin,omitempty"` 1568 1569 // Location defines where the state variable is located in the bundle. 1570 Location `yaml:",inline"` 1571 }