diff --git a/docs/content/docs/development/authoring-a-bundle/using-templates.md b/docs/content/docs/development/authoring-a-bundle/using-templates.md index 62e9a3b72..ccbfe0f9c 100644 --- a/docs/content/docs/development/authoring-a-bundle/using-templates.md +++ b/docs/content/docs/development/authoring-a-bundle/using-templates.md @@ -192,6 +192,41 @@ install: database-password: ${ bundle.dependencies.mysql.outputs.mysql-password } ``` +##### Dependecies v2 (experimental) + +The second version of dependencies, called DependenciesV2, is available under the +[**experimental** flag](https://porter.sh/docs/configuration/configuration/#experimental-feature-flags). + +You can enable the experimental flag by setting an environment variable as follows: + +``` +PORTER_EXPERIMENTAL=dependencies-v2 +``` + +When DependenciesV2 is enabled, it is possible to use templating inside the dependency output section, like this: + +```yaml +dependencies: + requires: + - name: mysql + bundle: + reference: getporter/mysql:v0.1.3 + outputs: + connection-string: "Server=mysql;Database=myDb;Uid=${bundle.parameters.uid};Pwd=${bundles.dependencies.mysql.mysql-password};" +``` + +If referencing an output from the same dependency, the format `outputs.OUTPUT_NAME` can also be used. + +```yaml +dependencies: + requires: + - name: mysql + bundle: + reference: getporter/mysql:v0.1.3 + outputs: + connection-string: "Server=mysql;Database=myDb;Uid=${bundle.parameters.uid};Pwd=${outputs.mysql-password};" +``` + #### images The bundle.images variable contains metadata about [referenced images] in the bundle. diff --git a/pkg/cnab/config-adapter/helpers.go b/pkg/cnab/config-adapter/helpers.go index 750df0909..5e9a6ae2f 100644 --- a/pkg/cnab/config-adapter/helpers.go +++ b/pkg/cnab/config-adapter/helpers.go @@ -13,7 +13,7 @@ import ( func LoadTestBundle(t *testing.T, config *config.Config, path string) cnab.ExtendedBundle { ctx := context.Background() - m, err := manifest.ReadManifest(config.Context, path) + m, err := manifest.ReadManifest(config.Context, path, config) require.NoError(t, err) b, err := ConvertToTestBundle(ctx, config, m) require.NoError(t, err) diff --git a/pkg/cnab/config-adapter/testdata/myenv-depsv2.bundle.json b/pkg/cnab/config-adapter/testdata/myenv-depsv2.bundle.json index 9d914bf3d..11d4d4b16 100644 --- a/pkg/cnab/config-adapter/testdata/myenv-depsv2.bundle.json +++ b/pkg/cnab/config-adapter/testdata/myenv-depsv2.bundle.json @@ -16,6 +16,13 @@ "env": "LOGLEVEL" } }, + "porter-app-port-dep-output": { + "definition": "porter-app-port-dep-output", + "description": "Wires up the app dependency port output for use as a parameter. Porter internal parameter that should not be set manually.", + "destination": { + "env": "PORTER_APP_PORT_DEP_OUTPUT" + } + }, "porter-debug": { "definition": "porter-debug-parameter", "description": "Print debug information from Porter when executing the bundle", @@ -69,6 +76,10 @@ "default": "info", "type": "string" }, + "porter-app-port-dep-output": { + "$comment": "porter-internal", + "$id": "https://porter.sh/generated-bundle/#porter-parameter-source-definition" + }, "porter-debug-parameter": { "$comment": "porter-internal", "$id": "https://porter.sh/generated-bundle/#porter-debug", @@ -99,6 +110,17 @@ ], "custom": { "io.cnab.parameter-sources": { + "porter-app-port-dep-output": { + "priority": [ + "dependencies.output" + ], + "sources": { + "dependencies.output": { + "dependency": "app", + "name": "port" + } + } + }, "porter-infra-ip-dep-output": { "priority": [ "dependencies.output" diff --git a/pkg/linter/linter.go b/pkg/linter/linter.go index 3a763c771..be1f03415 100644 --- a/pkg/linter/linter.go +++ b/pkg/linter/linter.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "get.porter.sh/porter/pkg/config" "get.porter.sh/porter/pkg/manifest" "get.porter.sh/porter/pkg/mixin/query" "get.porter.sh/porter/pkg/pkgmgmt" @@ -160,7 +161,7 @@ type action struct { steps manifest.Steps } -func (l *Linter) Lint(ctx context.Context, m *manifest.Manifest) (Results, error) { +func (l *Linter) Lint(ctx context.Context, m *manifest.Manifest, config *config.Config) (Results, error) { // Check for reserved porter prefix on parameter names reservedPrefixes := []string{"porter-", "porter_"} params := m.Parameters @@ -205,7 +206,7 @@ func (l *Linter) Lint(ctx context.Context, m *manifest.Manifest) (Results, error actions = append(actions, action{actionName, steps}) } for _, action := range actions { - res, err := validateParamsAppliesToAction(m, action.steps, tmplParams, action.name) + res, err := validateParamsAppliesToAction(m, action.steps, tmplParams, action.name, config) if err != nil { return nil, span.Error(fmt.Errorf("error validating action: %s", action.name)) } @@ -266,7 +267,7 @@ func (l *Linter) Lint(ctx context.Context, m *manifest.Manifest) (Results, error return results, nil } -func validateParamsAppliesToAction(m *manifest.Manifest, steps manifest.Steps, tmplParams manifest.ParameterDefinitions, actionName string) (Results, error) { +func validateParamsAppliesToAction(m *manifest.Manifest, steps manifest.Steps, tmplParams manifest.ParameterDefinitions, actionName string, config *config.Config) (Results, error) { var results Results for stepNumber, step := range steps { data, err := yaml.Marshal(step.Data) @@ -274,7 +275,7 @@ func validateParamsAppliesToAction(m *manifest.Manifest, steps manifest.Steps, t return nil, fmt.Errorf("error during marshalling: %w", err) } - tmplResult, err := m.ScanManifestTemplating(data) + tmplResult, err := m.ScanManifestTemplating(data, config) if err != nil { return nil, fmt.Errorf("error parsing templating: %w", err) } diff --git a/pkg/linter/linter_test.go b/pkg/linter/linter_test.go index b26239636..895a94afa 100644 --- a/pkg/linter/linter_test.go +++ b/pkg/linter/linter_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "get.porter.sh/porter/pkg/config" "get.porter.sh/porter/pkg/manifest" "get.porter.sh/porter/pkg/mixin" "get.porter.sh/porter/pkg/portercontext" @@ -13,6 +14,8 @@ import ( func TestLinter_Lint(t *testing.T) { ctx := context.Background() + testConfig := config.NewTestConfig(t).Config + t.Run("no results", func(t *testing.T) { cxt := portercontext.NewTestContext(t) mixins := mixin.NewTestMixinProvider() @@ -26,7 +29,7 @@ func TestLinter_Lint(t *testing.T) { } mixins.LintResults = nil - results, err := l.Lint(ctx, m) + results, err := l.Lint(ctx, m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 0, "linter should have returned 0 results") }) @@ -50,7 +53,7 @@ func TestLinter_Lint(t *testing.T) { }, } - results, err := l.Lint(ctx, m) + results, err := l.Lint(ctx, m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 1, "linter should have returned 1 result") require.Equal(t, mixins.LintResults, results, "unexpected lint results") @@ -68,7 +71,7 @@ func TestLinter_Lint(t *testing.T) { }, } - results, err := l.Lint(ctx, m) + results, err := l.Lint(ctx, m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 0, "linter should ignore mixins that doesn't support the lint command") }) @@ -121,7 +124,7 @@ func TestLinter_Lint(t *testing.T) { }, } - results, err := l.Lint(ctx, m) + results, err := l.Lint(ctx, m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 1, "linter should have returned 1 result") require.Equal(t, mixins.LintResults, results, "unexpected lint results") @@ -149,7 +152,7 @@ func TestLinter_Lint(t *testing.T) { }, } - results, err := l.Lint(ctx, m) + results, err := l.Lint(ctx, m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 0, "linter should have returned 1 result") }) @@ -168,7 +171,7 @@ func TestLinter_Lint(t *testing.T) { Parameters: param, } - results, err := l.Lint(ctx, m) + results, err := l.Lint(ctx, m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 1, "linter should have returned 1 result") require.NotContains(t, results[0].String(), ": 0th step in the mixin ()") @@ -189,6 +192,7 @@ func TestLinter_Lint_ParameterDoesNotApplyTo(t *testing.T) { m.CustomActions["customAction"] = steps }}, } + testConfig := config.NewTestConfig(t).Config for _, tc := range testCases { t.Run(tc.action, func(t *testing.T) { @@ -236,7 +240,7 @@ func TestLinter_Lint_ParameterDoesNotApplyTo(t *testing.T) { URL: "https://porter.sh/docs/references/linter/#porter-101", }, } - results, err := l.Lint(ctx, m) + results, err := l.Lint(ctx, m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 1, "linter should have returned 1 result") require.Equal(t, lintResults, results, "unexpected lint results") @@ -258,6 +262,7 @@ func TestLinter_Lint_ParameterAppliesTo(t *testing.T) { m.CustomActions["customAction"] = steps }}, } + testConfig := config.NewTestConfig(t).Config for _, tc := range testCases { t.Run(tc.action, func(t *testing.T) { @@ -290,7 +295,7 @@ func TestLinter_Lint_ParameterAppliesTo(t *testing.T) { } tc.setSteps(m, steps) - results, err := l.Lint(ctx, m) + results, err := l.Lint(ctx, m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 0, "linter should have returned 1 result") }) @@ -298,6 +303,8 @@ func TestLinter_Lint_ParameterAppliesTo(t *testing.T) { } func TestLinter_DependencyMultipleTimes(t *testing.T) { + testConfig := config.NewTestConfig(t).Config + t.Run("dependency defined multiple times", func(t *testing.T) { cxt := portercontext.NewTestContext(t) mixins := mixin.NewTestMixinProvider() @@ -321,7 +328,7 @@ func TestLinter_DependencyMultipleTimes(t *testing.T) { }, } - results, err := l.Lint(context.Background(), m) + results, err := l.Lint(context.Background(), m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 1, "linter should have returned 1 result") require.Equal(t, expectedResult, results, "unexpected lint results") @@ -340,7 +347,7 @@ func TestLinter_DependencyMultipleTimes(t *testing.T) { }, } - results, err := l.Lint(context.Background(), m) + results, err := l.Lint(context.Background(), m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 0, "linter should have returned 0 result") }) @@ -351,7 +358,7 @@ func TestLinter_DependencyMultipleTimes(t *testing.T) { m := &manifest.Manifest{} - results, err := l.Lint(context.Background(), m) + results, err := l.Lint(context.Background(), m, testConfig) require.NoError(t, err, "Lint failed") require.Len(t, results, 0, "linter should have returned 0 result") }) diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go index 690565a7b..be1192f60 100644 --- a/pkg/manifest/manifest.go +++ b/pkg/manifest/manifest.go @@ -282,6 +282,20 @@ func (m *Manifest) getTemplateDependencyOutputName(value string) (string, string return dependencyName, outputName, true } +var templatedDependencyShortOutputRegex = regexp.MustCompile(`^outputs.(.+)$`) + +// getTemplateDependencyShortOutputName returns the dependency output name from the +// template variable. +func (m *Manifest) getTemplateDependencyShortOutputName(value string) (string, bool) { + matches := templatedDependencyShortOutputRegex.FindStringSubmatch(value) + if len(matches) < 2 { + return "", false + } + + outputName := matches[1] + return outputName, true +} + var templatedParameterRegex = regexp.MustCompile(`^bundle\.parameters\.(.+)$`) // GetTemplateParameterName returns the parameter name from the template variable. @@ -1277,7 +1291,7 @@ func ReadManifestData(cxt *portercontext.Context, path string) ([]byte, error) { // ReadManifest determines if specified path is a URL or a filepath. // After reading the data in the path it returns a Manifest and any errors -func ReadManifest(cxt *portercontext.Context, path string) (*Manifest, error) { +func ReadManifest(cxt *portercontext.Context, path string, config *config.Config) (*Manifest, error) { data, err := ReadManifestData(cxt, path) if err != nil { return nil, err @@ -1288,7 +1302,7 @@ func ReadManifest(cxt *portercontext.Context, path string) (*Manifest, error) { return nil, fmt.Errorf("unsupported property set or a custom action is defined incorrectly: %w", err) } - tmplResult, err := m.ScanManifestTemplating(data) + tmplResult, err := m.ScanManifestTemplating(data, config) if err != nil { return nil, err } @@ -1324,22 +1338,20 @@ func (m *Manifest) GetTemplatePrefix() string { return "" } -func (m *Manifest) ScanManifestTemplating(data []byte) (templateScanResult, error) { - const disableHtmlEscaping = true - templateSrc := m.GetTemplatePrefix() + string(data) - tmpl, err := mustache.ParseStringRaw(templateSrc, disableHtmlEscaping) +func (m *Manifest) ScanManifestTemplating(data []byte, config *config.Config) (templateScanResult, error) { + // Handle outputs variable + shortOutputVars, err := m.mapShortDependencyOutputVariables(config) if err != nil { return templateScanResult{}, fmt.Errorf("error parsing the templating used in the manifest: %w", err) } - tags := tmpl.Tags() - vars := map[string]struct{}{} // Keep track of unique variable names - for _, tag := range tags { - if tag.Type() != mustache.Variable { - continue - } + vars, err := m.getTemplateVariables(string(data)) + if err != nil { + return templateScanResult{}, fmt.Errorf("error parsing the templating used in the manifest: %w", err) + } - vars[tag.Name()] = struct{}{} + if config.IsFeatureEnabled(experimental.FlagDependenciesV2) { + m.deduplicateAndFilterShortOutputVariables(shortOutputVars, vars) } result := templateScanResult{ @@ -1353,13 +1365,69 @@ func (m *Manifest) ScanManifestTemplating(data []byte) (templateScanResult, erro return result, nil } +func (m *Manifest) mapShortDependencyOutputVariables(config *config.Config) ([]string, error) { + shortOutputVars := []string{} + if config.IsFeatureEnabled(experimental.FlagDependenciesV2) { + for _, dep := range m.Dependencies.Requires { + for outputName, output := range dep.Outputs { + vars, err := m.getTemplateVariables(output) + if err != nil { + return nil, fmt.Errorf("error parsing the templating used for dependency %s output %s: %w", dep.Name, outputName, err) + } + + for tmplVar := range vars { + outputTemplateName, ok := m.getTemplateDependencyShortOutputName(tmplVar) + if ok { + shortOutputVars = append(shortOutputVars, fmt.Sprintf("bundle.dependencies.%s.outputs.%s", dep.Name, outputTemplateName)) + } + } + } + } + } + + return shortOutputVars, nil +} + +func (m *Manifest) deduplicateAndFilterShortOutputVariables(shortOutputVars []string, vars map[string]struct{}) { + for tmplVar := range vars { + if strings.HasPrefix(tmplVar, "outputs.") { + delete(vars, tmplVar) + } + } + + for _, shortHandVar := range shortOutputVars { + vars[shortHandVar] = struct{}{} + } +} + +func (m *Manifest) getTemplateVariables(data string) (map[string]struct{}, error) { + const disableHtmlEscaping = true + templateSrc := m.GetTemplatePrefix() + string(data) + tmpl, err := mustache.ParseStringRaw(templateSrc, disableHtmlEscaping) + if err != nil { + return nil, fmt.Errorf("error parsing the templating used in the manifest: %w", err) + } + + tags := tmpl.Tags() + vars := map[string]struct{}{} // Keep track of unique variable names + for _, tag := range tags { + if tag.Type() != mustache.Variable { + continue + } + + vars[tag.Name()] = struct{}{} + } + + return vars, nil +} + // LoadManifestFrom reads and validates the manifest at the specified location, // and returns a populated Manifest structure. func LoadManifestFrom(ctx context.Context, config *config.Config, file string) (*Manifest, error) { ctx, log := tracing.StartSpan(ctx) defer log.EndSpan() - m, err := ReadManifest(config.Context, file) + m, err := ReadManifest(config.Context, file, config) if err != nil { return nil, err } diff --git a/pkg/manifest/manifest_test.go b/pkg/manifest/manifest_test.go index b30bf6f4b..79285d476 100644 --- a/pkg/manifest/manifest_test.go +++ b/pkg/manifest/manifest_test.go @@ -263,7 +263,7 @@ func TestManifest_Validate_SchemaVersion(t *testing.T) { cfg.TestContext.UseFilesystem() cfg.Data.SchemaCheck = string(schema.CheckStrategyExact) - m, err := ReadManifest(cfg.Context, "testdata/porter.yaml") + m, err := ReadManifest(cfg.Context, "testdata/porter.yaml", cfg.Config) require.NoError(t, err) err = m.Validate(ctx, cfg.Config) @@ -281,7 +281,7 @@ func TestManifest_Validate_SchemaVersion(t *testing.T) { cfg.TestContext.EditYaml("porter.yaml", func(yq *yaml.Editor) error { return yq.SetValue("schemaVersion", "1.1.0") }) - m, err := ReadManifest(cfg.Context, "porter.yaml") + m, err := ReadManifest(cfg.Context, "porter.yaml", cfg.Config) require.NoError(t, err) err = m.Validate(ctx, cfg.Config) @@ -300,7 +300,7 @@ func TestManifest_Validate_SchemaVersion(t *testing.T) { defer span.EndSpan() cfg.Data.SchemaCheck = string(schema.CheckStrategyNone) - m, err := ReadManifest(cfg.Context, "testdata/porter.yaml") + m, err := ReadManifest(cfg.Context, "testdata/porter.yaml", cfg.Config) require.NoError(t, err) m.SchemaVersion = "" @@ -415,7 +415,7 @@ func TestManifest_Validate_WrongSchema(t *testing.T) { func TestReadManifest_URL(t *testing.T) { cxt := portercontext.NewTestContext(t) url := "https://raw.githubusercontent.com/getporter/porter/v0.27.1/pkg/manifest/testdata/simple.porter.yaml" - m, err := ReadManifest(cxt.Context, url) + m, err := ReadManifest(cxt.Context, url, config.NewTestConfig(t).Config) require.NoError(t, err) assert.Equal(t, "hello", m.Name) @@ -423,7 +423,7 @@ func TestReadManifest_URL(t *testing.T) { func TestReadManifest_Validate_InvalidURL(t *testing.T) { cxt := portercontext.NewTestContext(t) - _, err := ReadManifest(cxt.Context, "http://fake-example-porter") + _, err := ReadManifest(cxt.Context, "http://fake-example-porter", config.NewTestConfig(t).Config) assert.Error(t, err) assert.Regexp(t, "could not reach url http://fake-example-porter", err) @@ -432,7 +432,7 @@ func TestReadManifest_Validate_InvalidURL(t *testing.T) { func TestReadManifest_File(t *testing.T) { cxt := portercontext.NewTestContext(t) cxt.AddTestFile("testdata/simple.porter.yaml", config.Name) - m, err := ReadManifest(cxt.Context, config.Name) + m, err := ReadManifest(cxt.Context, config.Name, config.NewTestConfig(t).Config) require.NoError(t, err) assert.Equal(t, "hello", m.Name) @@ -562,7 +562,7 @@ func TestSetDefaults(t *testing.T) { func TestReadManifest_Validate_MissingFile(t *testing.T) { cxt := portercontext.NewTestContext(t) - _, err := ReadManifest(cxt.Context, "fake-porter.yaml") + _, err := ReadManifest(cxt.Context, "fake-porter.yaml", config.NewTestConfig(t).Config) assert.EqualError(t, err, "the specified porter configuration file fake-porter.yaml does not exist") } @@ -570,7 +570,7 @@ func TestReadManifest_Validate_MissingFile(t *testing.T) { func TestMixinDeclaration_UnmarshalYAML(t *testing.T) { cxt := portercontext.NewTestContext(t) cxt.AddTestFile("testdata/mixin-with-config.yaml", config.Name) - m, err := ReadManifest(cxt.Context, config.Name) + m, err := ReadManifest(cxt.Context, config.Name, config.NewTestConfig(t).Config) require.NoError(t, err) assert.Len(t, m.Mixins, 3, "expected 3 mixins") @@ -583,7 +583,7 @@ func TestMixinDeclaration_UnmarshalYAML(t *testing.T) { func TestMixinDeclaration_UnmarshalYAML_Invalid(t *testing.T) { cxt := portercontext.NewTestContext(t) cxt.AddTestFile("testdata/mixin-with-bad-config.yaml", config.Name) - _, err := ReadManifest(cxt.Context, config.Name) + _, err := ReadManifest(cxt.Context, config.Name, config.NewTestConfig(t).Config) require.Error(t, err) assert.Contains(t, err.Error(), "mixin declaration contained more than one mixin") @@ -598,7 +598,7 @@ func TestCredentialsDefinition_UnmarshalYAML(t *testing.T) { t.Run("all credentials in the generated manifest file are required", func(t *testing.T) { cxt := portercontext.NewTestContext(t) cxt.AddTestFile("testdata/with-credentials.yaml", config.Name) - m, err := ReadManifest(cxt.Context, config.Name) + m, err := ReadManifest(cxt.Context, config.Name, config.NewTestConfig(t).Config) require.NoError(t, err) assertAllCredentialsRequired(t, m.Credentials) @@ -878,7 +878,7 @@ func TestLoadManifestWithRequiredExtensions(t *testing.T) { func TestReadManifest_WithTemplateVariables(t *testing.T) { cxt := portercontext.NewTestContext(t) cxt.AddTestFile("testdata/porter-with-templating.yaml", config.Name) - m, err := ReadManifest(cxt.Context, config.Name) + m, err := ReadManifest(cxt.Context, config.Name, config.NewTestConfig(t).Config) require.NoError(t, err, "ReadManifest failed") wantVars := []string{"bundle.dependencies.mysql.outputs.mysql-password", "bundle.outputs.msg", "bundle.outputs.name"} assert.Equal(t, wantVars, m.TemplateVariables) @@ -887,7 +887,7 @@ func TestReadManifest_WithTemplateVariables(t *testing.T) { func TestManifest_GetTemplatedOutputs(t *testing.T) { cxt := portercontext.NewTestContext(t) cxt.AddTestFile("testdata/porter-with-templating.yaml", config.Name) - m, err := ReadManifest(cxt.Context, config.Name) + m, err := ReadManifest(cxt.Context, config.Name, config.NewTestConfig(t).Config) require.NoError(t, err, "ReadManifest failed") outputs := m.GetTemplatedOutputs() @@ -899,7 +899,7 @@ func TestManifest_GetTemplatedOutputs(t *testing.T) { func TestManifest_GetTemplatedDependencyOutputs(t *testing.T) { cxt := portercontext.NewTestContext(t) cxt.AddTestFile("testdata/porter-with-templating.yaml", config.Name) - m, err := ReadManifest(cxt.Context, config.Name) + m, err := ReadManifest(cxt.Context, config.Name, config.NewTestConfig(t).Config) require.NoError(t, err, "ReadManifest failed") outputs := m.GetTemplatedDependencyOutputs() diff --git a/pkg/porter/lint.go b/pkg/porter/lint.go index 154839347..99ae97754 100644 --- a/pkg/porter/lint.go +++ b/pkg/porter/lint.go @@ -61,7 +61,7 @@ func (p *Porter) Lint(ctx context.Context, opts LintOptions) (linter.Results, er } l := linter.New(p.Context, p.Mixins) - return l.Lint(ctx, manifest) + return l.Lint(ctx, manifest, p.Config) } // PrintLintResults lints the manifest and prints the results to the attached output. diff --git a/pkg/runtime/runtime_manifest.go b/pkg/runtime/runtime_manifest.go index aa8b7dc2b..cc21f9421 100644 --- a/pkg/runtime/runtime_manifest.go +++ b/pkg/runtime/runtime_manifest.go @@ -429,6 +429,8 @@ func (m *RuntimeManifest) buildAndResolveMappedDependencyOutputs(sourceData map[ depOutputs = depBun["outputs"].(map[string]interface{}) } + sourceData["outputs"] = depOutputs + for outputName, mappedOutput := range manifestDep.Outputs { mappedOutputTemplate := m.GetTemplatePrefix() + mappedOutput renderedOutput, err := mustache.RenderRaw(mappedOutputTemplate, true, sourceData) @@ -439,6 +441,8 @@ func (m *RuntimeManifest) buildAndResolveMappedDependencyOutputs(sourceData map[ } } + delete(sourceData, "outputs") + return nil } diff --git a/pkg/runtime/runtime_manifest_test.go b/pkg/runtime/runtime_manifest_test.go index 8de9c49f4..5c9943890 100644 --- a/pkg/runtime/runtime_manifest_test.go +++ b/pkg/runtime/runtime_manifest_test.go @@ -24,7 +24,7 @@ import ( func runtimeManifestFromStepYaml(t *testing.T, testConfig *config.TestConfig, stepYaml string) *RuntimeManifest { mContent := []byte(stepYaml) require.NoError(t, testConfig.FileSystem.WriteFile("/cnab/app/porter.yaml", mContent, pkg.FileModeWritable)) - m, err := manifest.ReadManifest(testConfig.Context, "/cnab/app/porter.yaml") + m, err := manifest.ReadManifest(testConfig.Context, "/cnab/app/porter.yaml", testConfig.Config) require.NoError(t, err, "ReadManifest failed") cfg := NewConfigFor(testConfig.Config) return NewRuntimeManifest(cfg, cnab.ActionInstall, m) @@ -501,6 +501,61 @@ install: assert.Equal(t, []interface{}{"password"}, args, "Incorrect template args passed to the mixin step") } +func TestResolveStep_DependencyTemplatedMappedOutput_OutputVariable(t *testing.T) { + ctx := context.Background() + testConfig := config.NewTestConfig(t) + testConfig.SetExperimentalFlags(experimental.FlagDependenciesV2) + testConfig.Setenv("PORTER_MYSQL_PASSWORD_DEP_OUTPUT", "password") + + mContent := `schemaVersion: 1.0.0 +dependencies: + requires: + - name: mysql + bundle: + reference: "getporter/porter-mysql" + outputs: + mappedOutput: combined-${ outputs.password } + +install: +- mymixin: + Arguments: + - ${ bundle.dependencies.mysql.outputs.mappedOutput } +` + rm := runtimeManifestFromStepYaml(t, testConfig, mContent) + ps := cnab.ParameterSources{} + ps.SetParameterFromDependencyOutput("porter-mysql-password", "mysql", "password") + rm.bundle = cnab.NewBundle(bundle.Bundle{ + Custom: map[string]interface{}{ + cnab.ParameterSourcesExtensionKey: ps, + }, + RequiredExtensions: []string{cnab.ParameterSourcesExtensionKey}, + }) + + rm.bundles = map[string]cnab.ExtendedBundle{ + "mysql": cnab.NewBundle(bundle.Bundle{ + Outputs: map[string]bundle.Output{ + "password": { + Definition: "password", + }, + }, + Definitions: map[string]*definition.Schema{ + "password": {WriteOnly: makeBoolPtr(true)}, + }, + }), + } + + s := rm.Install[0] + err := rm.ResolveStep(ctx, 0, s) + require.NoError(t, err) + + require.IsType(t, map[string]interface{}{}, s.Data["mymixin"], "Data.mymixin has incorrect type") + mixin := s.Data["mymixin"].(map[string]interface{}) + require.IsType(t, mixin["Arguments"], []interface{}{}, "Data.mymixin.Arguments has incorrect type") + args := mixin["Arguments"].([]interface{}) + + assert.Equal(t, []interface{}{"combined-password"}, args, "Incorrect template args passed to the mixin step") +} + func TestResolveInMainDict(t *testing.T) { ctx := context.Background() c := config.NewTestConfig(t) diff --git a/tests/integration/schema_test.go b/tests/integration/schema_test.go index 92cfb1c07..5116a50e0 100644 --- a/tests/integration/schema_test.go +++ b/tests/integration/schema_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "get.porter.sh/porter/pkg/config" "get.porter.sh/porter/pkg/manifest" testhelper "get.porter.sh/porter/pkg/test" "get.porter.sh/porter/pkg/yaml" @@ -56,7 +57,7 @@ mixins.2.testmixin: Additional property missingproperty is not allowed`}, t.Run(tm.name, func(t *testing.T) { // Load the manifest as a go dump testManifestPath := tm.path - mani, err := manifest.ReadManifest(test.TestContext.Context, testManifestPath) + mani, err := manifest.ReadManifest(test.TestContext.Context, testManifestPath, config.NewTestConfig(t).Config) maniYaml, err := yaml.Marshal(mani) require.NoError(t, err, "error marshaling manifest to yaml")