From 242ca7eaa3a58c6d642ce070a8a668ef65de815a Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Wed, 31 Aug 2022 18:50:38 -0500 Subject: [PATCH] Convert a depsv2 porter.yaml into bundle.json This adds support for the dependencies-v2 experimental flag when building a bundle. * Add dependency v2 fields to the porter.yaml manifest * Represent v2 deps in the bundle.json * Determine which dependency extension is used by a bundle * Have separate packages for the dependencies v2 cnab extension, and its implementation * Consolidate extensions definition into a single file * Match file names to contained structs in pkg/cnab Signed-off-by: Carolyn Van Slyck --- magefile.go | 4 +- pkg/cnab/{bundle.go => bundle_reference.go} | 0 ...undle_test.go => bundle_reference_test.go} | 0 pkg/cnab/config-adapter/adapter.go | 99 ++++++++- pkg/cnab/config-adapter/adapter_test.go | 79 ++++--- pkg/cnab/config-adapter/doc.go | 1 + pkg/cnab/config-adapter/helpers.go | 12 + .../testdata/mybuns-depsv2.bundle.json | 207 ++++++++++++++++++ .../testdata/mybuns.bundle.json | 2 +- .../testdata/porter-depsv2.yaml | 56 +++++ pkg/cnab/config-adapter/testdata/porter.yaml | 10 + pkg/cnab/dependencies/v1/doc.go | 2 + pkg/cnab/{ => dependencies/v1}/solver.go | 20 +- pkg/cnab/{ => dependencies/v1}/solver_test.go | 27 +-- pkg/cnab/dependencies_v1.go | 12 +- pkg/cnab/dependencies_v2.go | 82 +++++++ pkg/cnab/dependencies_v2_test.go | 79 +++++++ pkg/cnab/extensions.go | 65 ++++++ pkg/cnab/extensions/dependencies/v1/doc.go | 2 + .../{ => extensions}/dependencies/v1/types.go | 0 .../dependencies/v1/types_test.go | 0 pkg/cnab/extensions/dependencies/v2/doc.go | 3 + pkg/cnab/extensions/dependencies/v2/types.go | 150 +++++++++++++ .../extensions/dependencies/v2/types_test.go | 91 ++++++++ pkg/cnab/extensions_test.go | 96 ++++++++ pkg/cnab/{reference.go => oci_reference.go} | 0 ...eference_test.go => oci_reference_test.go} | 0 pkg/cnab/required.go | 65 ------ pkg/cnab/required_test.go | 102 --------- pkg/cnab/testdata/bundle-depsv2.json | 140 ++++++++++++ pkg/manifest/manifest.go | 65 +++++- pkg/manifest/manifest_test.go | 124 ++++++++++- pkg/manifest/testdata/porter-depsv2.yaml | 37 ++++ pkg/porter/dependencies.go | 4 +- pkg/porter/explain.go | 4 +- pkg/porter/explain_test.go | 6 +- pkg/porter/parameters.go | 3 +- pkg/porter/testdata/schema.json | 33 +++ pkg/runtime/runtime_manifest_test.go | 11 +- pkg/schema/manifest.schema.json | 35 ++- tests/integration/testdata/schema/schema.json | 11 + tests/testdata/mybuns/porter.yaml | 4 + tests/testdata/mydb/porter.yaml | 10 +- 43 files changed, 1490 insertions(+), 263 deletions(-) rename pkg/cnab/{bundle.go => bundle_reference.go} (100%) rename pkg/cnab/{bundle_test.go => bundle_reference_test.go} (100%) create mode 100644 pkg/cnab/config-adapter/testdata/mybuns-depsv2.bundle.json create mode 100644 pkg/cnab/config-adapter/testdata/porter-depsv2.yaml create mode 100644 pkg/cnab/dependencies/v1/doc.go rename pkg/cnab/{ => dependencies/v1}/solver.go (77%) rename pkg/cnab/{ => dependencies/v1}/solver_test.go (65%) create mode 100644 pkg/cnab/dependencies_v2.go create mode 100644 pkg/cnab/dependencies_v2_test.go create mode 100644 pkg/cnab/extensions/dependencies/v1/doc.go rename pkg/cnab/{ => extensions}/dependencies/v1/types.go (100%) rename pkg/cnab/{ => extensions}/dependencies/v1/types_test.go (100%) create mode 100644 pkg/cnab/extensions/dependencies/v2/doc.go create mode 100644 pkg/cnab/extensions/dependencies/v2/types.go create mode 100644 pkg/cnab/extensions/dependencies/v2/types_test.go rename pkg/cnab/{reference.go => oci_reference.go} (100%) rename pkg/cnab/{reference_test.go => oci_reference_test.go} (100%) delete mode 100644 pkg/cnab/required.go delete mode 100644 pkg/cnab/required_test.go create mode 100644 pkg/cnab/testdata/bundle-depsv2.json create mode 100644 pkg/manifest/testdata/porter-depsv2.yaml diff --git a/magefile.go b/magefile.go index b5afe389b1..0fa13fe7f2 100644 --- a/magefile.go +++ b/magefile.go @@ -216,8 +216,8 @@ func TestUnit() { must.Command("go", "test", v, "./...").CollapseArgs().RunV() - // Verify integration tests compile since we don't run them automatically on pull requests - must.Run("go", "test", "-run=non", "-tags=integration", "./...") + // Verify integration/smoke tests compile since we don't run them automatically on pull requests + must.Run("go", "test", "-run=non", `-tags="integration smoke"`, "./...") } // Run smoke tests to quickly check if Porter is broken diff --git a/pkg/cnab/bundle.go b/pkg/cnab/bundle_reference.go similarity index 100% rename from pkg/cnab/bundle.go rename to pkg/cnab/bundle_reference.go diff --git a/pkg/cnab/bundle_test.go b/pkg/cnab/bundle_reference_test.go similarity index 100% rename from pkg/cnab/bundle_test.go rename to pkg/cnab/bundle_reference_test.go diff --git a/pkg/cnab/config-adapter/adapter.go b/pkg/cnab/config-adapter/adapter.go index a77999f63e..30f64187b0 100644 --- a/pkg/cnab/config-adapter/adapter.go +++ b/pkg/cnab/config-adapter/adapter.go @@ -2,12 +2,14 @@ package configadapter import ( "context" + "encoding/json" "fmt" "path" "strings" "get.porter.sh/porter/pkg/cnab" - depsv1 "get.porter.sh/porter/pkg/cnab/dependencies/v1" + depsv1ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v1" + depsv2ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v2" "get.porter.sh/porter/pkg/config" "get.porter.sh/porter/pkg/experimental" "get.porter.sh/porter/pkg/manifest" @@ -414,33 +416,33 @@ func (c *ManifestConverter) generateDependencies() (interface{}, string, error) // Check if they are using v1 of the dependencies spec or v2 if c.config.IsFeatureEnabled(experimental.FlagDependenciesV2) { - panic("the dependencies-v2 experimental flag was specified but is not yet implemented") + // Ok we are using v2! + deps, err := c.generateDependenciesV2() + return deps, cnab.DependenciesV2ExtensionKey, err } - deps, err := c.generateDependenciesV1() - if err != nil { - return nil, "", err - } + // Default to using v1 of deps + deps := c.generateDependenciesV1() return deps, cnab.DependenciesV1ExtensionKey, nil } -func (c *ManifestConverter) generateDependenciesV1() (*depsv1.Dependencies, error) { +func (c *ManifestConverter) generateDependenciesV1() *depsv1ext.Dependencies { if len(c.Manifest.Dependencies.Requires) == 0 { - return nil, nil + return nil } - deps := &depsv1.Dependencies{ + deps := &depsv1ext.Dependencies{ Sequence: make([]string, 0, len(c.Manifest.Dependencies.Requires)), - Requires: make(map[string]depsv1.Dependency, len(c.Manifest.Dependencies.Requires)), + Requires: make(map[string]depsv1ext.Dependency, len(c.Manifest.Dependencies.Requires)), } for _, dep := range c.Manifest.Dependencies.Requires { - dependencyRef := depsv1.Dependency{ + dependencyRef := depsv1ext.Dependency{ Name: dep.Name, Bundle: dep.Bundle.Reference, } if len(dep.Bundle.Version) > 0 { - dependencyRef.Version = &depsv1.DependencyVersion{ + dependencyRef.Version = &depsv1ext.DependencyVersion{ Ranges: []string{dep.Bundle.Version}, } @@ -454,6 +456,77 @@ func (c *ManifestConverter) generateDependenciesV1() (*depsv1.Dependencies, erro deps.Requires[dep.Name] = dependencyRef } + return deps +} + +func (c *ManifestConverter) generateDependenciesV2() (*depsv2ext.Dependencies, error) { + deps := &depsv2ext.Dependencies{ + Requires: make(map[string]depsv2ext.Dependency, len(c.Manifest.Dependencies.Requires)), + } + + for _, dep := range c.Manifest.Dependencies.Requires { + dependencyRef := depsv2ext.Dependency{ + Name: dep.Name, + Bundle: dep.Bundle.Reference, + Version: dep.Bundle.Version, + } + + if dep.Bundle.Interface != nil { + if dep.Bundle.Interface.Reference != "" { + dependencyRef.Interface.Reference = dep.Bundle.Interface.Reference + } + if dep.Bundle.Interface.Document != nil { + bundleData, err := json.Marshal(dep.Bundle.Interface.Document) + if err != nil { + return nil, fmt.Errorf("invalid bundle interface document for dependency %s: %w", dep.Name, err) + } + rawMessage := &json.RawMessage{} + err = rawMessage.UnmarshalJSON(bundleData) + if err != nil { + return nil, fmt.Errorf("could not convert bundle interface document to a raw json message for dependency %s: %w", dep.Name, err) + } + dependencyRef.Interface.Document = rawMessage + } + } + + if dep.Installation != nil { + dependencyRef.Installation = &depsv2ext.DependencyInstallation{ + Labels: dep.Installation.Labels, + } + if dep.Installation.Criteria != nil { + dependencyRef.Installation.Criteria = &depsv2ext.InstallationCriteria{ + MatchInterface: dep.Installation.Criteria.MatchInterface, + MatchNamespace: dep.Installation.Criteria.MatchNamespace, + IgnoreLabels: dep.Installation.Criteria.IgnoreLabels, + } + } + } + + if len(dep.Parameters) > 0 { + dependencyRef.Parameters = make(map[string]depsv2ext.DependencySource, len(dep.Parameters)) + for param, source := range dep.Parameters { + ds, err := depsv2ext.ParseDependencySource(source) + if err != nil { + return nil, fmt.Errorf("invalid parameter wiring specified for dependency %s: %w", dep.Name, err) + } + dependencyRef.Parameters[param] = ds + } + } + + if len(dep.Credentials) > 0 { + dependencyRef.Credentials = make(map[string]depsv2ext.DependencySource, len(dep.Credentials)) + for cred, source := range dep.Credentials { + ds, err := depsv2ext.ParseDependencySource(source) + if err != nil { + return nil, fmt.Errorf("invalid credential wiring specified for dependency %s: %w", dep.Name, err) + } + dependencyRef.Credentials[cred] = ds + } + } + + deps.Requires[dep.Name] = dependencyRef + } + return deps, nil } @@ -643,6 +716,8 @@ func (c *ManifestConverter) generateRequiredExtensions(b cnab.ExtendedBundle) [] // Add the appropriate dependencies key if applicable if b.HasDependenciesV1() { requiredExtensions = append(requiredExtensions, cnab.DependenciesV1ExtensionKey) + } else if b.HasDependenciesV2() { + requiredExtensions = append(requiredExtensions, cnab.DependenciesV2ExtensionKey) } // Add the appropriate parameter sources key if applicable diff --git a/pkg/cnab/config-adapter/adapter_test.go b/pkg/cnab/config-adapter/adapter_test.go index 60f71eea14..c298c256ff 100644 --- a/pkg/cnab/config-adapter/adapter_test.go +++ b/pkg/cnab/config-adapter/adapter_test.go @@ -8,8 +8,9 @@ import ( "testing" "get.porter.sh/porter/pkg/cnab" - depsv1 "get.porter.sh/porter/pkg/cnab/dependencies/v1" + depsv1ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v1" "get.porter.sh/porter/pkg/config" + "get.porter.sh/porter/pkg/experimental" "get.porter.sh/porter/pkg/manifest" "get.porter.sh/porter/pkg/mixin" "get.porter.sh/porter/pkg/pkgmgmt" @@ -22,27 +23,53 @@ import ( func TestManifestConverter(t *testing.T) { t.Parallel() - c := config.NewTestConfig(t) - c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name) + testcases := []struct { + name string + configHandler func(c *config.Config) + manifestPath string + goldenFile string + }{ + {name: "depsv1", + configHandler: func(c *config.Config) {}, + manifestPath: "tests/testdata/mybuns/porter.yaml", + goldenFile: "testdata/mybuns.bundle.json"}, + {name: "depsv2ext", + configHandler: func(c *config.Config) { + c.SetExperimentalFlags(experimental.FlagDependenciesV2) + }, + manifestPath: "tests/testdata/mybuns/porter.yaml", + goldenFile: "testdata/mybuns-depsv2.bundle.json"}, + } - ctx := context.Background() - m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name) - require.NoError(t, err, "could not load manifest") + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - installedMixins := []mixin.Metadata{ - {Name: "exec", VersionInfo: pkgmgmt.VersionInfo{Version: "v1.2.3"}}, - } + c := config.NewTestConfig(t) + tc.configHandler(c.Config) + c.TestContext.AddTestFileFromRoot(tc.manifestPath, config.Name) - a := NewManifestConverter(c.Config, m, nil, installedMixins) + ctx := context.Background() + m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name) + require.NoError(t, err, "could not load manifest") - bun, err := a.ToBundle(ctx) - require.NoError(t, err, "ToBundle failed") + installedMixins := []mixin.Metadata{ + {Name: "exec", VersionInfo: pkgmgmt.VersionInfo{Version: "v1.2.3"}}, + } + + a := NewManifestConverter(c.Config, m, nil, installedMixins) + + bun, err := a.ToBundle(ctx) + require.NoError(t, err, "ToBundle failed") - // Compare the regular json, not the canonical, because that's hard to diff - prepBundleForDiff(&bun.Bundle) - bunD, err := json.MarshalIndent(bun, "", " ") - require.NoError(t, err) - c.TestContext.CompareGoldenFile("testdata/mybuns.bundle.json", string(bunD)) + // Compare the regular json, not the canonical, because that's hard to diff + prepBundleForDiff(&bun.Bundle) + bunD, err := json.MarshalIndent(bun, "", " ") + require.NoError(t, err) + c.TestContext.CompareGoldenFile(tc.goldenFile, string(bunD)) + }) + } } func prepBundleForDiff(b *bundle.Bundle) { @@ -538,24 +565,24 @@ func TestManifestConverter_generateDependencies(t *testing.T) { testcases := []struct { name string - wantDep depsv1.Dependency + wantDep depsv1ext.Dependency }{ - {"no-version", depsv1.Dependency{ + {"no-version", depsv1ext.Dependency{ Name: "mysql", Bundle: "getporter/azure-mysql:5.7", }}, - {"no-ranges, uses prerelease", depsv1.Dependency{ + {"no-ranges, uses prerelease", depsv1ext.Dependency{ Name: "ad", Bundle: "getporter/azure-active-directory", - Version: &depsv1.DependencyVersion{ + Version: &depsv1ext.DependencyVersion{ AllowPrereleases: true, Ranges: []string{"1.0.0-0"}, }, }}, - {"with-ranges", depsv1.Dependency{ + {"with-ranges", depsv1ext.Dependency{ Name: "storage", Bundle: "getporter/azure-blob-storage", - Version: &depsv1.DependencyVersion{ + Version: &depsv1ext.DependencyVersion{ Ranges: []string{ "1.x - 2,2.1 - 3.x", }, @@ -581,12 +608,12 @@ func TestManifestConverter_generateDependencies(t *testing.T) { depsExt, depsExtKey, err := a.generateDependencies() require.NoError(t, err) require.Equal(t, cnab.DependenciesV1ExtensionKey, depsExtKey, "expected the v1 dependencies extension key") - require.IsType(t, &depsv1.Dependencies{}, depsExt, "expected a v1 dependencies extension section") - deps := depsExt.(*depsv1.Dependencies) + require.IsType(t, &depsv1ext.Dependencies{}, depsExt, "expected a v1 dependencies extension section") + deps := depsExt.(*depsv1ext.Dependencies) require.Len(t, deps.Requires, 3, "incorrect number of dependencies were generated") require.Equal(t, []string{"mysql", "ad", "storage"}, deps.Sequence, "incorrect sequence was generated") - var dep *depsv1.Dependency + var dep *depsv1ext.Dependency for _, d := range deps.Requires { if d.Bundle == tc.wantDep.Bundle { dep = &d diff --git a/pkg/cnab/config-adapter/doc.go b/pkg/cnab/config-adapter/doc.go index fa52e028c6..7da1f640af 100644 --- a/pkg/cnab/config-adapter/doc.go +++ b/pkg/cnab/config-adapter/doc.go @@ -1 +1,2 @@ +// Package configadapter converts a Porter manifest (porter.yaml) to a CNAB bundle.json package configadapter diff --git a/pkg/cnab/config-adapter/helpers.go b/pkg/cnab/config-adapter/helpers.go index 318257f964..750df09096 100644 --- a/pkg/cnab/config-adapter/helpers.go +++ b/pkg/cnab/config-adapter/helpers.go @@ -2,12 +2,24 @@ package configadapter import ( "context" + "testing" + + "github.com/stretchr/testify/require" "get.porter.sh/porter/pkg/cnab" "get.porter.sh/porter/pkg/config" "get.porter.sh/porter/pkg/manifest" ) +func LoadTestBundle(t *testing.T, config *config.Config, path string) cnab.ExtendedBundle { + ctx := context.Background() + m, err := manifest.ReadManifest(config.Context, path) + require.NoError(t, err) + b, err := ConvertToTestBundle(ctx, config, m) + require.NoError(t, err) + return b +} + // ConvertToTestBundle is suitable for taking a test manifest (porter.yaml) // and making a bundle.json for it. Does not make an accurate representation // of the bundle, but is suitable for testing. diff --git a/pkg/cnab/config-adapter/testdata/mybuns-depsv2.bundle.json b/pkg/cnab/config-adapter/testdata/mybuns-depsv2.bundle.json new file mode 100644 index 0000000000..24b592d539 --- /dev/null +++ b/pkg/cnab/config-adapter/testdata/mybuns-depsv2.bundle.json @@ -0,0 +1,207 @@ +{ + "schemaVersion": "1.2.0", + "name": "mybuns", + "version": "0.1.2", + "description": "A very thorough test bundle", + "invocationImages": [ + { + "imageType": "docker", + "image": "localhost:5000/mybuns:porter-332dd75c541511a27fc332bdcd049d5b" + } + ], + "images": { + "whalesayd": { + "imageType": "docker", + "image": "carolynvs/whalesayd:latest", + "description": "Whalesay as a service" + } + }, + "actions": { + "boom": { + "modifies": true, + "description": "boom" + }, + "dry-run": { + "stateless": true, + "description": "Make sure it will work before you run it" + }, + "status": { + "description": "Print the installation status" + } + }, + "parameters": { + "cfg": { + "definition": "cfg-parameter", + "description": "A json config file", + "destination": { + "path": "/cnab/app/buncfg.json" + } + }, + "chaos_monkey": { + "definition": "chaos_monkey-parameter", + "description": "Set to true to make the bundle fail", + "destination": { + "env": "CHAOS_MONKEY" + } + }, + "log_level": { + "definition": "log_level-parameter", + "description": "How unhelpful would you like the logs to be?", + "destination": { + "env": "LOG_LEVEL" + } + }, + "password": { + "definition": "password-parameter", + "description": "The super secret data", + "destination": { + "env": "PASSWORD" + } + }, + "porter-debug": { + "definition": "porter-debug-parameter", + "description": "Print debug information from Porter when executing the bundle", + "destination": { + "env": "PORTER_DEBUG" + } + }, + "porter-state": { + "definition": "porter-state", + "description": "Supports persisting state for bundles. Porter internal parameter that should not be set manually.", + "destination": { + "path": "/porter/state.tgz" + } + } + }, + "credentials": { + "username": { + "env": "USERNAME", + "description": "The name you want on the audit log", + "required": true + } + }, + "outputs": { + "mylogs": { + "definition": "mylogs-output", + "applyTo": [ + "install", + "upgrade" + ], + "path": "/cnab/app/outputs/mylogs" + }, + "porter-state": { + "definition": "porter-state", + "description": "Supports persisting state for bundles. Porter internal parameter that should not be set manually.", + "path": "/cnab/app/outputs/porter-state" + }, + "result": { + "definition": "result-output", + "applyTo": [ + "install", + "upgrade" + ], + "path": "/cnab/app/outputs/result" + } + }, + "definitions": { + "cfg-parameter": { + "contentEncoding": "base64", + "default": "", + "description": "A json config file", + "type": "string" + }, + "chaos_monkey-parameter": { + "default": false, + "description": "Set to true to make the bundle fail", + "type": "boolean" + }, + "log_level-parameter": { + "default": 5, + "description": "How unhelpful would you like the logs to be?", + "maximum": 11, + "minimum": 1, + "type": "integer" + }, + "mylogs-output": { + "type": "string" + }, + "password-parameter": { + "default": "defautl-secret", + "description": "The super secret data", + "type": "string", + "writeOnly": true + }, + "porter-debug-parameter": { + "$comment": "porter-internal", + "$id": "https://getporter.org/generated-bundle/#porter-debug", + "default": false, + "description": "Print debug information from Porter when executing the bundle", + "type": "boolean" + }, + "porter-state": { + "$comment": "porter-internal", + "$id": "https://getporter.org/generated-bundle/#porter-state", + "contentEncoding": "base64", + "description": "Supports persisting state for bundles. Porter internal parameter that should not be set manually.", + "type": "string" + }, + "result-output": { + "type": "string", + "writeOnly": true + } + }, + "requiredExtensions": [ + "sh.porter.file-parameters", + "sh.porter.dependencies.v2", + "io.cnab.parameter-sources", + "io.cnab.docker" + ], + "custom": { + "io.cnab.docker": null, + "io.cnab.parameter-sources": { + "porter-state": { + "priority": [ + "output" + ], + "sources": { + "output": { + "name": "porter-state" + } + } + } + }, + "sh.porter": { + "manifestDigest": "", + "mixins": { + "exec": { + "version": "v1.2.3" + } + }, + "manifest": "IyBUaGlzIGlzIGEgdGVzdCBidW5kbGUgdGhhdCBtYWtlcyBubyBsb2dpY2FsIHNlbnNlLCBidXQgaXQgZG9lcyBleGVyY2lzZSBsb3RzIG9mIGRpZmZlcmVudCBidW5kbGUgZmVhdHVyZXMKCnNjaGVtYVZlcnNpb246IDEuMC4wCm5hbWU6IG15YnVucwp2ZXJzaW9uOiAwLjEuMgpkZXNjcmlwdGlvbjogIkEgdmVyeSB0aG9yb3VnaCB0ZXN0IGJ1bmRsZSIKcmVnaXN0cnk6IGxvY2FsaG9zdDo1MDAwCmRvY2tlcmZpbGU6IERvY2tlcmZpbGUudG1wbAoKcmVxdWlyZWQ6CiAgLSBkb2NrZXIKCmNyZWRlbnRpYWxzOgogIC0gbmFtZTogdXNlcm5hbWUKICAgIGRlc2NyaXB0aW9uOiAiVGhlIG5hbWUgeW91IHdhbnQgb24gdGhlIGF1ZGl0IGxvZyIKICAgIGVudjogVVNFUk5BTUUKCnBhcmFtZXRlcnM6CiAgLSBuYW1lOiBsb2dfbGV2ZWwKICAgIGRlc2NyaXB0aW9uOiAiSG93IHVuaGVscGZ1bCB3b3VsZCB5b3UgbGlrZSB0aGUgbG9ncyB0byBiZT8iCiAgICB0eXBlOiBpbnRlZ2VyCiAgICBtaW5pbXVtOiAxCiAgICBtYXhpbXVtOiAxMQogICAgZGVmYXVsdDogNQogIC0gbmFtZTogcGFzc3dvcmQKICAgIGRlc2NyaXB0aW9uOiAiVGhlIHN1cGVyIHNlY3JldCBkYXRhIgogICAgdHlwZTogc3RyaW5nCiAgICBkZWZhdWx0OiAiZGVmYXV0bC1zZWNyZXQiCiAgICBzZW5zaXRpdmU6IHRydWUKICAtIG5hbWU6IGNoYW9zX21vbmtleQogICAgZGVzY3JpcHRpb246ICJTZXQgdG8gdHJ1ZSB0byBtYWtlIHRoZSBidW5kbGUgZmFpbCIKICAgIHR5cGU6IGJvb2xlYW4KICAgIGRlZmF1bHQ6IGZhbHNlCiAgLSBuYW1lOiBjZmcKICAgIGRlc2NyaXB0aW9uOiAiQSBqc29uIGNvbmZpZyBmaWxlIgogICAgdHlwZTogZmlsZQogICAgZGVmYXVsdDogJycKICAgIHBhdGg6IGJ1bmNmZy5qc29uCgpvdXRwdXRzOgogIC0gbmFtZTogbXlsb2dzCiAgICBhcHBseVRvOgogICAgICAtIGluc3RhbGwKICAgICAgLSB1cGdyYWRlCiAgLSBuYW1lOiByZXN1bHQKICAgIGFwcGx5VG86CiAgICAgIC0gaW5zdGFsbAogICAgICAtIHVwZ3JhZGUKICAgIHNlbnNpdGl2ZTogdHJ1ZQoKc3RhdGU6CiAgLSBuYW1lOiBtYWdpY19maWxlCiAgICBwYXRoOiBtYWdpYy50eHQKCmRlcGVuZGVuY2llczoKICByZXF1aXJlczoKICAgIC0gbmFtZTogZGIKICAgICAgYnVuZGxlOgogICAgICAgIHJlZmVyZW5jZTogImxvY2FsaG9zdDo1MDAwL215ZGI6djAuMS4wIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAgIGRhdGFiYXNlOiBiaWdkYgogICAgICAgIGNvbGxhdGlvbjogYnVuZGxlLnBhcmFtZXRlcnMuZGItY29sbGF0aW9uCiAgICAgIGNyZWRlbnRpYWxzOgogICAgICAgICMgVE9ETyhQRVAwMDMpOiBVcGRhdGUgdGhlIGJ1bmRsZSB0byBoYXZlIDIgZGVwZW5kZW5jaWVzLiBQYXNzIGFuIG91dHB1dCBmcm9tIG9uZSBkZXAgdG8gYW5vdGhlci4KICAgICAgICB1c2VybmFtZTogYnVuZGxlLmNyZWRlbnRpYWxzLnVzZXJuYW1lCgppbWFnZXM6CiAgd2hhbGVzYXlkOgogICAgZGVzY3JpcHRpb246ICJXaGFsZXNheSBhcyBhIHNlcnZpY2UiCiAgICBpbWFnZVR5cGU6ICJkb2NrZXIiCiAgICByZXBvc2l0b3J5OiBjYXJvbHludnMvd2hhbGVzYXlkCiAgICB0YWc6ICJsYXRlc3QiCgptaXhpbnM6CiAgLSBleGVjCiAgLSB0ZXN0bWl4aW46CiAgICAgIGNsaWVudFZlcnNpb246IDEuMi4zCgpjdXN0b21BY3Rpb25zOgogIGRyeS1ydW46CiAgICBkZXNjcmlwdGlvbjogIk1ha2Ugc3VyZSBpdCB3aWxsIHdvcmsgYmVmb3JlIHlvdSBydW4gaXQiCiAgICBzdGF0ZWxlc3M6IHRydWUKICAgIG1vZGlmaWVzOiBmYWxzZQogIHN0YXR1czoKICAgIGRlc2NyaXB0aW9uOiAiUHJpbnQgdGhlIGluc3RhbGxhdGlvbiBzdGF0dXMiCiAgICBzdGF0ZWxlc3M6IGZhbHNlCiAgICBtb2RpZmllczogZmFsc2UKCmluc3RhbGw6CiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIkNoZWNrIHRoZSBkb2NrZXIgc29ja2V0IgogICAgICBjb21tYW5kOiBzdGF0CiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIC92YXIvcnVuL2RvY2tlci5zb2NrCiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIkxldCdzIG1ha2Ugc29tZSBtYWdpYyIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIG1ha2VNYWdpYwogICAgICAgIC0gIiR7IGJ1bmRsZS5jcmVkZW50aWFscy51c2VybmFtZSB9IGlzIGEgdW5pY29ybiB3aXRoICR7IGJ1bmRsZS5wYXJhbWV0ZXJzLnBhc3N3b3JkIH0gc2VjcmV0LiIKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAiaW5zdGFsbCIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIGluc3RhbGwKICAgICAgb3V0cHV0czoKICAgICAgICAtIG5hbWU6IG15bG9ncwogICAgICAgICAgcmVnZXg6ICIoLiopIgogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJyb2xsIHRoZSBkaWNlIHdpdGggeW91ciBjaGFvcyBtb25rZXkiCiAgICAgIGNvbW1hbmQ6IC4vaGVscGVycy5zaAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSBjaGFvc19tb25rZXkKICAgICAgICAtICR7IGJ1bmRsZS5wYXJhbWV0ZXJzLmNoYW9zX21vbmtleSB9CiAgICAgIG91dHB1dHM6CiAgICAgICAgLSBuYW1lOiByZXN1bHQKICAgICAgICAgIHJlZ2V4OiAiKC4qKSIKCmRyeS1ydW46CiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIkNoZWNrIHNvbWUgdGhpbmdzIgogICAgICBjb21tYW5kOiBlY2hvCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtICJBbGwgY2xlYXIhIgoKc3RhdHVzOgogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJQcmludCBjb25maWciCiAgICAgIGNvbW1hbmQ6IGNhdAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSAkeyBidW5kbGUucGFyYW1ldGVycy5jZmcgfQogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJQcmludCBtYWdpYyIKICAgICAgY29tbWFuZDogY2F0CiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIG1hZ2ljLnR4dAoKYm9vbToKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAibW9kaWZ5IHRoZSBidW5kbGUgaW4gdW5rbm93YWJsZSB3YXlzIgogICAgICBjb21tYW5kOiBlY2hvCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtICJZT0xPIgoKdXBncmFkZToKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAiRW5zdXJlIG1hZ2ljIgogICAgICBjb21tYW5kOiAuL2hlbHBlcnMuc2gKICAgICAgYXJndW1lbnRzOgogICAgICAgIC0gZW5zdXJlTWFnaWMKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAidXBncmFkZSIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIHVwZ3JhZGUKICAgICAgb3V0cHV0czoKICAgICAgICAtIG5hbWU6IG15bG9ncwogICAgICAgICAgcmVnZXg6ICIoLiopIgogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJyb2xsIHRoZSBkaWNlIHdpdGggeW91ciBjaGFvcyBtb25rZXkiCiAgICAgIGNvbW1hbmQ6IC4vaGVscGVycy5zaAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSBjaGFvc19tb25rZXkKICAgICAgICAtICR7IGJ1bmRsZS5wYXJhbWV0ZXJzLmNoYW9zX21vbmtleSB9CiAgICAgIG91dHB1dHM6CiAgICAgICAgLSBuYW1lOiByZXN1bHQKICAgICAgICAgIHJlZ2V4OiAiKC4qKSIKCnVuaW5zdGFsbDoKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAiRW5zdXJlIE1hZ2ljIgogICAgICBjb21tYW5kOiAuL2hlbHBlcnMuc2gKICAgICAgYXJndW1lbnRzOgogICAgICAgIC0gZW5zdXJlTWFnaWMKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAidW5pbnN0YWxsIgogICAgICBjb21tYW5kOiAuL2hlbHBlcnMuc2gKICAgICAgYXJndW1lbnRzOgogICAgICAgIC0gdW5pbnN0YWxsCiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogInJvbGwgdGhlIGRpY2Ugd2l0aCB5b3VyIGNoYW9zIG1vbmtleSIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIGNoYW9zX21vbmtleQogICAgICAgIC0gJHsgYnVuZGxlLnBhcmFtZXRlcnMuY2hhb3NfbW9ua2V5IH0K", + "version": "", + "commit": "" + }, + "sh.porter.dependencies.v2": { + "requires": { + "db": { + "name": "db", + "bundle": "localhost:5000/mydb:v0.1.0", + "parameters": { + "collation": { + "parameter": "db-collation" + }, + "database": { + "value": "bigdb" + } + }, + "credentials": { + "username": { + "credential": "username" + } + } + } + } + }, + "sh.porter.file-parameters": {} + } +} \ No newline at end of file diff --git a/pkg/cnab/config-adapter/testdata/mybuns.bundle.json b/pkg/cnab/config-adapter/testdata/mybuns.bundle.json index d3ce2839ec..23ece92d87 100644 --- a/pkg/cnab/config-adapter/testdata/mybuns.bundle.json +++ b/pkg/cnab/config-adapter/testdata/mybuns.bundle.json @@ -188,7 +188,7 @@ "version": "v1.2.3" } }, - "manifest": "IyBUaGlzIGlzIGEgdGVzdCBidW5kbGUgdGhhdCBtYWtlcyBubyBsb2dpY2FsIHNlbnNlLCBidXQgaXQgZG9lcyBleGVyY2lzZSBsb3RzIG9mIGRpZmZlcmVudCBidW5kbGUgZmVhdHVyZXMKCnNjaGVtYVZlcnNpb246IDEuMC4wCm5hbWU6IG15YnVucwp2ZXJzaW9uOiAwLjEuMgpkZXNjcmlwdGlvbjogIkEgdmVyeSB0aG9yb3VnaCB0ZXN0IGJ1bmRsZSIKcmVnaXN0cnk6IGxvY2FsaG9zdDo1MDAwCmRvY2tlcmZpbGU6IERvY2tlcmZpbGUudG1wbAoKcmVxdWlyZWQ6CiAgLSBkb2NrZXIKCmNyZWRlbnRpYWxzOgogIC0gbmFtZTogdXNlcm5hbWUKICAgIGRlc2NyaXB0aW9uOiAiVGhlIG5hbWUgeW91IHdhbnQgb24gdGhlIGF1ZGl0IGxvZyIKICAgIGVudjogVVNFUk5BTUUKCnBhcmFtZXRlcnM6CiAgLSBuYW1lOiBsb2dfbGV2ZWwKICAgIGRlc2NyaXB0aW9uOiAiSG93IHVuaGVscGZ1bCB3b3VsZCB5b3UgbGlrZSB0aGUgbG9ncyB0byBiZT8iCiAgICB0eXBlOiBpbnRlZ2VyCiAgICBtaW5pbXVtOiAxCiAgICBtYXhpbXVtOiAxMQogICAgZGVmYXVsdDogNQogIC0gbmFtZTogcGFzc3dvcmQKICAgIGRlc2NyaXB0aW9uOiAiVGhlIHN1cGVyIHNlY3JldCBkYXRhIgogICAgdHlwZTogc3RyaW5nCiAgICBkZWZhdWx0OiAiZGVmYXV0bC1zZWNyZXQiCiAgICBzZW5zaXRpdmU6IHRydWUKICAtIG5hbWU6IGNoYW9zX21vbmtleQogICAgZGVzY3JpcHRpb246ICJTZXQgdG8gdHJ1ZSB0byBtYWtlIHRoZSBidW5kbGUgZmFpbCIKICAgIHR5cGU6IGJvb2xlYW4KICAgIGRlZmF1bHQ6IGZhbHNlCiAgLSBuYW1lOiBjZmcKICAgIGRlc2NyaXB0aW9uOiAiQSBqc29uIGNvbmZpZyBmaWxlIgogICAgdHlwZTogZmlsZQogICAgZGVmYXVsdDogJycKICAgIHBhdGg6IGJ1bmNmZy5qc29uCgpvdXRwdXRzOgogIC0gbmFtZTogbXlsb2dzCiAgICBhcHBseVRvOgogICAgICAtIGluc3RhbGwKICAgICAgLSB1cGdyYWRlCiAgLSBuYW1lOiByZXN1bHQKICAgIGFwcGx5VG86CiAgICAgIC0gaW5zdGFsbAogICAgICAtIHVwZ3JhZGUKICAgIHNlbnNpdGl2ZTogdHJ1ZQoKc3RhdGU6CiAgLSBuYW1lOiBtYWdpY19maWxlCiAgICBwYXRoOiBtYWdpYy50eHQKCmRlcGVuZGVuY2llczoKICByZXF1aXJlczoKICAgIC0gbmFtZTogZGIKICAgICAgYnVuZGxlOgogICAgICAgIHJlZmVyZW5jZTogImxvY2FsaG9zdDo1MDAwL215ZGI6djAuMS4wIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAgIGRhdGFiYXNlOiBiaWdkYgoKaW1hZ2VzOgogIHdoYWxlc2F5ZDoKICAgIGRlc2NyaXB0aW9uOiAiV2hhbGVzYXkgYXMgYSBzZXJ2aWNlIgogICAgaW1hZ2VUeXBlOiAiZG9ja2VyIgogICAgcmVwb3NpdG9yeTogY2Fyb2x5bnZzL3doYWxlc2F5ZAogICAgdGFnOiAibGF0ZXN0IgoKbWl4aW5zOgogIC0gZXhlYwogIC0gdGVzdG1peGluOgogICAgICBjbGllbnRWZXJzaW9uOiAxLjIuMwoKY3VzdG9tQWN0aW9uczoKICBkcnktcnVuOgogICAgZGVzY3JpcHRpb246ICJNYWtlIHN1cmUgaXQgd2lsbCB3b3JrIGJlZm9yZSB5b3UgcnVuIGl0IgogICAgc3RhdGVsZXNzOiB0cnVlCiAgICBtb2RpZmllczogZmFsc2UKICBzdGF0dXM6CiAgICBkZXNjcmlwdGlvbjogIlByaW50IHRoZSBpbnN0YWxsYXRpb24gc3RhdHVzIgogICAgc3RhdGVsZXNzOiBmYWxzZQogICAgbW9kaWZpZXM6IGZhbHNlCgppbnN0YWxsOgogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJDaGVjayB0aGUgZG9ja2VyIHNvY2tldCIKICAgICAgY29tbWFuZDogc3RhdAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSAvdmFyL3J1bi9kb2NrZXIuc29jawogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJMZXQncyBtYWtlIHNvbWUgbWFnaWMiCiAgICAgIGNvbW1hbmQ6IC4vaGVscGVycy5zaAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSBtYWtlTWFnaWMKICAgICAgICAtICIkeyBidW5kbGUuY3JlZGVudGlhbHMudXNlcm5hbWUgfSBpcyBhIHVuaWNvcm4gd2l0aCAkeyBidW5kbGUucGFyYW1ldGVycy5wYXNzd29yZCB9IHNlY3JldC4iCiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogImluc3RhbGwiCiAgICAgIGNvbW1hbmQ6IC4vaGVscGVycy5zaAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSBpbnN0YWxsCiAgICAgIG91dHB1dHM6CiAgICAgICAgLSBuYW1lOiBteWxvZ3MKICAgICAgICAgIHJlZ2V4OiAiKC4qKSIKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAicm9sbCB0aGUgZGljZSB3aXRoIHlvdXIgY2hhb3MgbW9ua2V5IgogICAgICBjb21tYW5kOiAuL2hlbHBlcnMuc2gKICAgICAgYXJndW1lbnRzOgogICAgICAgIC0gY2hhb3NfbW9ua2V5CiAgICAgICAgLSAkeyBidW5kbGUucGFyYW1ldGVycy5jaGFvc19tb25rZXkgfQogICAgICBvdXRwdXRzOgogICAgICAgIC0gbmFtZTogcmVzdWx0CiAgICAgICAgICByZWdleDogIiguKikiCgpkcnktcnVuOgogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJDaGVjayBzb21lIHRoaW5ncyIKICAgICAgY29tbWFuZDogZWNobwogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSAiQWxsIGNsZWFyISIKCnN0YXR1czoKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAiUHJpbnQgY29uZmlnIgogICAgICBjb21tYW5kOiBjYXQKICAgICAgYXJndW1lbnRzOgogICAgICAgIC0gJHsgYnVuZGxlLnBhcmFtZXRlcnMuY2ZnIH0KICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAiUHJpbnQgbWFnaWMiCiAgICAgIGNvbW1hbmQ6IGNhdAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSBtYWdpYy50eHQKCmJvb206CiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIm1vZGlmeSB0aGUgYnVuZGxlIGluIHVua25vd2FibGUgd2F5cyIKICAgICAgY29tbWFuZDogZWNobwogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSAiWU9MTyIKCnVwZ3JhZGU6CiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIkVuc3VyZSBtYWdpYyIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIGVuc3VyZU1hZ2ljCiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogInVwZ3JhZGUiCiAgICAgIGNvbW1hbmQ6IC4vaGVscGVycy5zaAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSB1cGdyYWRlCiAgICAgIG91dHB1dHM6CiAgICAgICAgLSBuYW1lOiBteWxvZ3MKICAgICAgICAgIHJlZ2V4OiAiKC4qKSIKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAicm9sbCB0aGUgZGljZSB3aXRoIHlvdXIgY2hhb3MgbW9ua2V5IgogICAgICBjb21tYW5kOiAuL2hlbHBlcnMuc2gKICAgICAgYXJndW1lbnRzOgogICAgICAgIC0gY2hhb3NfbW9ua2V5CiAgICAgICAgLSAkeyBidW5kbGUucGFyYW1ldGVycy5jaGFvc19tb25rZXkgfQogICAgICBvdXRwdXRzOgogICAgICAgIC0gbmFtZTogcmVzdWx0CiAgICAgICAgICByZWdleDogIiguKikiCgp1bmluc3RhbGw6CiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIkVuc3VyZSBNYWdpYyIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIGVuc3VyZU1hZ2ljCiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogInVuaW5zdGFsbCIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIHVuaW5zdGFsbAogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJyb2xsIHRoZSBkaWNlIHdpdGggeW91ciBjaGFvcyBtb25rZXkiCiAgICAgIGNvbW1hbmQ6IC4vaGVscGVycy5zaAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSBjaGFvc19tb25rZXkKICAgICAgICAtICR7IGJ1bmRsZS5wYXJhbWV0ZXJzLmNoYW9zX21vbmtleSB9Cg==", + "manifest": "IyBUaGlzIGlzIGEgdGVzdCBidW5kbGUgdGhhdCBtYWtlcyBubyBsb2dpY2FsIHNlbnNlLCBidXQgaXQgZG9lcyBleGVyY2lzZSBsb3RzIG9mIGRpZmZlcmVudCBidW5kbGUgZmVhdHVyZXMKCnNjaGVtYVZlcnNpb246IDEuMC4wCm5hbWU6IG15YnVucwp2ZXJzaW9uOiAwLjEuMgpkZXNjcmlwdGlvbjogIkEgdmVyeSB0aG9yb3VnaCB0ZXN0IGJ1bmRsZSIKcmVnaXN0cnk6IGxvY2FsaG9zdDo1MDAwCmRvY2tlcmZpbGU6IERvY2tlcmZpbGUudG1wbAoKcmVxdWlyZWQ6CiAgLSBkb2NrZXIKCmNyZWRlbnRpYWxzOgogIC0gbmFtZTogdXNlcm5hbWUKICAgIGRlc2NyaXB0aW9uOiAiVGhlIG5hbWUgeW91IHdhbnQgb24gdGhlIGF1ZGl0IGxvZyIKICAgIGVudjogVVNFUk5BTUUKCnBhcmFtZXRlcnM6CiAgLSBuYW1lOiBsb2dfbGV2ZWwKICAgIGRlc2NyaXB0aW9uOiAiSG93IHVuaGVscGZ1bCB3b3VsZCB5b3UgbGlrZSB0aGUgbG9ncyB0byBiZT8iCiAgICB0eXBlOiBpbnRlZ2VyCiAgICBtaW5pbXVtOiAxCiAgICBtYXhpbXVtOiAxMQogICAgZGVmYXVsdDogNQogIC0gbmFtZTogcGFzc3dvcmQKICAgIGRlc2NyaXB0aW9uOiAiVGhlIHN1cGVyIHNlY3JldCBkYXRhIgogICAgdHlwZTogc3RyaW5nCiAgICBkZWZhdWx0OiAiZGVmYXV0bC1zZWNyZXQiCiAgICBzZW5zaXRpdmU6IHRydWUKICAtIG5hbWU6IGNoYW9zX21vbmtleQogICAgZGVzY3JpcHRpb246ICJTZXQgdG8gdHJ1ZSB0byBtYWtlIHRoZSBidW5kbGUgZmFpbCIKICAgIHR5cGU6IGJvb2xlYW4KICAgIGRlZmF1bHQ6IGZhbHNlCiAgLSBuYW1lOiBjZmcKICAgIGRlc2NyaXB0aW9uOiAiQSBqc29uIGNvbmZpZyBmaWxlIgogICAgdHlwZTogZmlsZQogICAgZGVmYXVsdDogJycKICAgIHBhdGg6IGJ1bmNmZy5qc29uCgpvdXRwdXRzOgogIC0gbmFtZTogbXlsb2dzCiAgICBhcHBseVRvOgogICAgICAtIGluc3RhbGwKICAgICAgLSB1cGdyYWRlCiAgLSBuYW1lOiByZXN1bHQKICAgIGFwcGx5VG86CiAgICAgIC0gaW5zdGFsbAogICAgICAtIHVwZ3JhZGUKICAgIHNlbnNpdGl2ZTogdHJ1ZQoKc3RhdGU6CiAgLSBuYW1lOiBtYWdpY19maWxlCiAgICBwYXRoOiBtYWdpYy50eHQKCmRlcGVuZGVuY2llczoKICByZXF1aXJlczoKICAgIC0gbmFtZTogZGIKICAgICAgYnVuZGxlOgogICAgICAgIHJlZmVyZW5jZTogImxvY2FsaG9zdDo1MDAwL215ZGI6djAuMS4wIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAgIGRhdGFiYXNlOiBiaWdkYgogICAgICAgIGNvbGxhdGlvbjogYnVuZGxlLnBhcmFtZXRlcnMuZGItY29sbGF0aW9uCiAgICAgIGNyZWRlbnRpYWxzOgogICAgICAgICMgVE9ETyhQRVAwMDMpOiBVcGRhdGUgdGhlIGJ1bmRsZSB0byBoYXZlIDIgZGVwZW5kZW5jaWVzLiBQYXNzIGFuIG91dHB1dCBmcm9tIG9uZSBkZXAgdG8gYW5vdGhlci4KICAgICAgICB1c2VybmFtZTogYnVuZGxlLmNyZWRlbnRpYWxzLnVzZXJuYW1lCgppbWFnZXM6CiAgd2hhbGVzYXlkOgogICAgZGVzY3JpcHRpb246ICJXaGFsZXNheSBhcyBhIHNlcnZpY2UiCiAgICBpbWFnZVR5cGU6ICJkb2NrZXIiCiAgICByZXBvc2l0b3J5OiBjYXJvbHludnMvd2hhbGVzYXlkCiAgICB0YWc6ICJsYXRlc3QiCgptaXhpbnM6CiAgLSBleGVjCiAgLSB0ZXN0bWl4aW46CiAgICAgIGNsaWVudFZlcnNpb246IDEuMi4zCgpjdXN0b21BY3Rpb25zOgogIGRyeS1ydW46CiAgICBkZXNjcmlwdGlvbjogIk1ha2Ugc3VyZSBpdCB3aWxsIHdvcmsgYmVmb3JlIHlvdSBydW4gaXQiCiAgICBzdGF0ZWxlc3M6IHRydWUKICAgIG1vZGlmaWVzOiBmYWxzZQogIHN0YXR1czoKICAgIGRlc2NyaXB0aW9uOiAiUHJpbnQgdGhlIGluc3RhbGxhdGlvbiBzdGF0dXMiCiAgICBzdGF0ZWxlc3M6IGZhbHNlCiAgICBtb2RpZmllczogZmFsc2UKCmluc3RhbGw6CiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIkNoZWNrIHRoZSBkb2NrZXIgc29ja2V0IgogICAgICBjb21tYW5kOiBzdGF0CiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIC92YXIvcnVuL2RvY2tlci5zb2NrCiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIkxldCdzIG1ha2Ugc29tZSBtYWdpYyIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIG1ha2VNYWdpYwogICAgICAgIC0gIiR7IGJ1bmRsZS5jcmVkZW50aWFscy51c2VybmFtZSB9IGlzIGEgdW5pY29ybiB3aXRoICR7IGJ1bmRsZS5wYXJhbWV0ZXJzLnBhc3N3b3JkIH0gc2VjcmV0LiIKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAiaW5zdGFsbCIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIGluc3RhbGwKICAgICAgb3V0cHV0czoKICAgICAgICAtIG5hbWU6IG15bG9ncwogICAgICAgICAgcmVnZXg6ICIoLiopIgogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJyb2xsIHRoZSBkaWNlIHdpdGggeW91ciBjaGFvcyBtb25rZXkiCiAgICAgIGNvbW1hbmQ6IC4vaGVscGVycy5zaAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSBjaGFvc19tb25rZXkKICAgICAgICAtICR7IGJ1bmRsZS5wYXJhbWV0ZXJzLmNoYW9zX21vbmtleSB9CiAgICAgIG91dHB1dHM6CiAgICAgICAgLSBuYW1lOiByZXN1bHQKICAgICAgICAgIHJlZ2V4OiAiKC4qKSIKCmRyeS1ydW46CiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIkNoZWNrIHNvbWUgdGhpbmdzIgogICAgICBjb21tYW5kOiBlY2hvCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtICJBbGwgY2xlYXIhIgoKc3RhdHVzOgogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJQcmludCBjb25maWciCiAgICAgIGNvbW1hbmQ6IGNhdAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSAkeyBidW5kbGUucGFyYW1ldGVycy5jZmcgfQogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJQcmludCBtYWdpYyIKICAgICAgY29tbWFuZDogY2F0CiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIG1hZ2ljLnR4dAoKYm9vbToKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAibW9kaWZ5IHRoZSBidW5kbGUgaW4gdW5rbm93YWJsZSB3YXlzIgogICAgICBjb21tYW5kOiBlY2hvCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtICJZT0xPIgoKdXBncmFkZToKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAiRW5zdXJlIG1hZ2ljIgogICAgICBjb21tYW5kOiAuL2hlbHBlcnMuc2gKICAgICAgYXJndW1lbnRzOgogICAgICAgIC0gZW5zdXJlTWFnaWMKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAidXBncmFkZSIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIHVwZ3JhZGUKICAgICAgb3V0cHV0czoKICAgICAgICAtIG5hbWU6IG15bG9ncwogICAgICAgICAgcmVnZXg6ICIoLiopIgogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJyb2xsIHRoZSBkaWNlIHdpdGggeW91ciBjaGFvcyBtb25rZXkiCiAgICAgIGNvbW1hbmQ6IC4vaGVscGVycy5zaAogICAgICBhcmd1bWVudHM6CiAgICAgICAgLSBjaGFvc19tb25rZXkKICAgICAgICAtICR7IGJ1bmRsZS5wYXJhbWV0ZXJzLmNoYW9zX21vbmtleSB9CiAgICAgIG91dHB1dHM6CiAgICAgICAgLSBuYW1lOiByZXN1bHQKICAgICAgICAgIHJlZ2V4OiAiKC4qKSIKCnVuaW5zdGFsbDoKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAiRW5zdXJlIE1hZ2ljIgogICAgICBjb21tYW5kOiAuL2hlbHBlcnMuc2gKICAgICAgYXJndW1lbnRzOgogICAgICAgIC0gZW5zdXJlTWFnaWMKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAidW5pbnN0YWxsIgogICAgICBjb21tYW5kOiAuL2hlbHBlcnMuc2gKICAgICAgYXJndW1lbnRzOgogICAgICAgIC0gdW5pbnN0YWxsCiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogInJvbGwgdGhlIGRpY2Ugd2l0aCB5b3VyIGNoYW9zIG1vbmtleSIKICAgICAgY29tbWFuZDogLi9oZWxwZXJzLnNoCiAgICAgIGFyZ3VtZW50czoKICAgICAgICAtIGNoYW9zX21vbmtleQogICAgICAgIC0gJHsgYnVuZGxlLnBhcmFtZXRlcnMuY2hhb3NfbW9ua2V5IH0K", "version": "", "commit": "" }, diff --git a/pkg/cnab/config-adapter/testdata/porter-depsv2.yaml b/pkg/cnab/config-adapter/testdata/porter-depsv2.yaml new file mode 100644 index 0000000000..8e6976763a --- /dev/null +++ b/pkg/cnab/config-adapter/testdata/porter-depsv2.yaml @@ -0,0 +1,56 @@ +# TODO(PEP003): Get rid of this file, and use porter.yaml with depsv2 enabled instead of having extra bundles +schemaVersion: 1.0.0-alpha.1 +name: porter-hello +description: "An example Porter configuration" +version: 0.1.0 +registry: "localhost:5000" + +credentials: + - name: username + description: Name of the database user + required: false + env: ROOT_USERNAME + - name: password + path: /tmp/password + applyTo: + - uninstall + +parameters: + - name: db_collation + type: string + default: magic + +dependencies: + requires: + - name: mysql + bundle: + reference: "getporter/azure-mysql:5.7" + parameters: + database: wordpress + collation: magic + credentials: + user: bundle.credentials.username + +mixins: +- exec + +install: +- exec: + description: "Say Hello" + command: bash + flags: + c: echo Hello World + +status: +- exec: + description: "Get World Status" + command: bash + flags: + c: echo The world is on fire + +uninstall: +- exec: + description: "Say Goodbye" + command: bash + flags: + c: echo Goodbye World diff --git a/pkg/cnab/config-adapter/testdata/porter.yaml b/pkg/cnab/config-adapter/testdata/porter.yaml index f9a5dce638..cecee0ab22 100644 --- a/pkg/cnab/config-adapter/testdata/porter.yaml +++ b/pkg/cnab/config-adapter/testdata/porter.yaml @@ -14,11 +14,21 @@ credentials: applyTo: - uninstall +parameters: + - name: db_collation + type: string + default: Latin1_General_CI_AI + dependencies: requires: - name: mysql bundle: reference: "getporter/azure-mysql:5.7" + parameters: + database: wordpress + collation: ${bundle.parameters.db_collation} + credentials: + user: ${bundle.credentials.username} mixins: - exec diff --git a/pkg/cnab/dependencies/v1/doc.go b/pkg/cnab/dependencies/v1/doc.go new file mode 100644 index 0000000000..b3fc9a00e1 --- /dev/null +++ b/pkg/cnab/dependencies/v1/doc.go @@ -0,0 +1,2 @@ +// Package v1 contains the implementation for v1 of the CNAB Dependencies specification. +package v1 diff --git a/pkg/cnab/solver.go b/pkg/cnab/dependencies/v1/solver.go similarity index 77% rename from pkg/cnab/solver.go rename to pkg/cnab/dependencies/v1/solver.go index d8bbc5eb82..814498c77c 100644 --- a/pkg/cnab/solver.go +++ b/pkg/cnab/dependencies/v1/solver.go @@ -1,10 +1,12 @@ -package cnab +package v1 import ( "fmt" "sort" - depsv1 "get.porter.sh/porter/pkg/cnab/dependencies/v1" + "get.porter.sh/porter/pkg/cnab" + + depsv1ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v1" "github.com/Masterminds/semver/v3" "github.com/google/go-containerregistry/pkg/crane" ) @@ -18,7 +20,7 @@ type DependencyLock struct { type DependencySolver struct { } -func (s *DependencySolver) ResolveDependencies(bun ExtendedBundle) ([]DependencyLock, error) { +func (s *DependencySolver) ResolveDependencies(bun cnab.ExtendedBundle) ([]DependencyLock, error) { if !bun.HasDependenciesV1() { return nil, nil } @@ -49,10 +51,10 @@ func (s *DependencySolver) ResolveDependencies(bun ExtendedBundle) ([]Dependency } // ResolveVersion returns the bundle name, its version and any error. -func (s *DependencySolver) ResolveVersion(name string, dep depsv1.Dependency) (OCIReference, error) { - ref, err := ParseOCIReference(dep.Bundle) +func (s *DependencySolver) ResolveVersion(name string, dep depsv1ext.Dependency) (cnab.OCIReference, error) { + ref, err := cnab.ParseOCIReference(dep.Bundle) if err != nil { - return OCIReference{}, fmt.Errorf("error parsing dependency (%s) bundle %q as OCI reference: %w", name, dep.Bundle, err) + return cnab.OCIReference{}, fmt.Errorf("error parsing dependency (%s) bundle %q as OCI reference: %w", name, dep.Bundle, err) } // Here is where we could split out this logic into multiple strategy funcs / structs if necessary @@ -64,16 +66,16 @@ func (s *DependencySolver) ResolveVersion(name string, dep depsv1.Dependency) (O tag, err := s.determineDefaultTag(dep) if err != nil { - return OCIReference{}, err + return cnab.OCIReference{}, err } return ref.WithTag(tag) } - return OCIReference{}, fmt.Errorf("not implemented: dependency version range specified for %s: %w", name, err) + return cnab.OCIReference{}, fmt.Errorf("not implemented: dependency version range specified for %s: %w", name, err) } -func (s *DependencySolver) determineDefaultTag(dep depsv1.Dependency) (string, error) { +func (s *DependencySolver) determineDefaultTag(dep depsv1ext.Dependency) (string, error) { tags, err := crane.ListTags(dep.Bundle) if err != nil { return "", fmt.Errorf("error listing tags for %s: %w", dep.Bundle, err) diff --git a/pkg/cnab/solver_test.go b/pkg/cnab/dependencies/v1/solver_test.go similarity index 65% rename from pkg/cnab/solver_test.go rename to pkg/cnab/dependencies/v1/solver_test.go index 416d6555c2..d0e4b93c96 100644 --- a/pkg/cnab/solver_test.go +++ b/pkg/cnab/dependencies/v1/solver_test.go @@ -1,9 +1,10 @@ -package cnab +package v1 import ( "testing" - depsv1 "get.porter.sh/porter/pkg/cnab/dependencies/v1" + "get.porter.sh/porter/pkg/cnab" + depsv1ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v1" "github.com/cnabio/cnab-go/bundle" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -12,10 +13,10 @@ import ( func TestDependencySolver_ResolveDependencies(t *testing.T) { t.Parallel() - bun := NewBundle(bundle.Bundle{ + bun := cnab.NewBundle(bundle.Bundle{ Custom: map[string]interface{}{ - DependenciesV1ExtensionKey: depsv1.Dependencies{ - Requires: map[string]depsv1.Dependency{ + cnab.DependenciesV1ExtensionKey: depsv1ext.Dependencies{ + Requires: map[string]depsv1ext.Dependency{ "mysql": { Bundle: "getporter/mysql:5.7", }, @@ -52,30 +53,30 @@ func TestDependencySolver_ResolveVersion(t *testing.T) { testcases := []struct { name string - dep depsv1.Dependency + dep depsv1ext.Dependency wantVersion string wantError string }{ {name: "pinned version", - dep: depsv1.Dependency{Bundle: "mysql:5.7"}, + dep: depsv1ext.Dependency{Bundle: "mysql:5.7"}, wantVersion: "5.7"}, {name: "unimplemented range", - dep: depsv1.Dependency{Bundle: "mysql", Version: &depsv1.DependencyVersion{Ranges: []string{"1 - 1.5"}}}, + dep: depsv1ext.Dependency{Bundle: "mysql", Version: &depsv1ext.DependencyVersion{Ranges: []string{"1 - 1.5"}}}, wantError: "not implemented"}, {name: "default tag to latest", - dep: depsv1.Dependency{Bundle: "getporterci/porter-test-only-latest"}, + dep: depsv1ext.Dependency{Bundle: "getporterci/porter-test-only-latest"}, wantVersion: "latest"}, {name: "no default tag", - dep: depsv1.Dependency{Bundle: "getporterci/porter-test-no-default-tag"}, + dep: depsv1ext.Dependency{Bundle: "getporterci/porter-test-no-default-tag"}, wantError: "no tag was specified"}, {name: "default tag to highest semver", - dep: depsv1.Dependency{Bundle: "getporterci/porter-test-with-versions", Version: &depsv1.DependencyVersion{Ranges: nil, AllowPrereleases: true}}, + dep: depsv1ext.Dependency{Bundle: "getporterci/porter-test-with-versions", Version: &depsv1ext.DependencyVersion{Ranges: nil, AllowPrereleases: true}}, wantVersion: "v1.3-beta1"}, {name: "default tag to highest semver, explicitly excluding prereleases", - dep: depsv1.Dependency{Bundle: "getporterci/porter-test-with-versions", Version: &depsv1.DependencyVersion{Ranges: nil, AllowPrereleases: false}}, + dep: depsv1ext.Dependency{Bundle: "getporterci/porter-test-with-versions", Version: &depsv1ext.DependencyVersion{Ranges: nil, AllowPrereleases: false}}, wantVersion: "v1.2"}, {name: "default tag to highest semver, excluding prereleases by default", - dep: depsv1.Dependency{Bundle: "getporterci/porter-test-with-versions"}, + dep: depsv1ext.Dependency{Bundle: "getporterci/porter-test-with-versions"}, wantVersion: "v1.2"}, } diff --git a/pkg/cnab/dependencies_v1.go b/pkg/cnab/dependencies_v1.go index 2169edc355..acb567f8e3 100644 --- a/pkg/cnab/dependencies_v1.go +++ b/pkg/cnab/dependencies_v1.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - depsv1 "get.porter.sh/porter/pkg/cnab/dependencies/v1" + depsv1ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v1" ) const ( @@ -32,15 +32,15 @@ var DependenciesV1Extension = RequiredExtension{ // ReadDependenciesV1 is a convenience method for returning a bonafide // Dependencies reference after reading from the applicable section from // the provided bundle -func (b ExtendedBundle) ReadDependenciesV1() (depsv1.Dependencies, error) { +func (b ExtendedBundle) ReadDependenciesV1() (depsv1ext.Dependencies, error) { raw, err := b.DependencyV1Reader() if err != nil { - return depsv1.Dependencies{}, err + return depsv1ext.Dependencies{}, err } - deps, ok := raw.(depsv1.Dependencies) + deps, ok := raw.(depsv1ext.Dependencies) if !ok { - return depsv1.Dependencies{}, errors.New("unable to read dependencies extension data") + return depsv1ext.Dependencies{}, errors.New("unable to read dependencies extension data") } // Return the dependencies @@ -61,7 +61,7 @@ func (b ExtendedBundle) DependencyV1Reader() (interface{}, error) { return nil, fmt.Errorf("could not marshal the untyped dependencies extension data %q: %w", string(dataB), err) } - deps := depsv1.Dependencies{} + deps := depsv1ext.Dependencies{} err = json.Unmarshal(dataB, &deps) if err != nil { return nil, fmt.Errorf("could not unmarshal the dependencies extension %q: %w", string(dataB), err) diff --git a/pkg/cnab/dependencies_v2.go b/pkg/cnab/dependencies_v2.go new file mode 100644 index 0000000000..1cf02cc5ec --- /dev/null +++ b/pkg/cnab/dependencies_v2.go @@ -0,0 +1,82 @@ +package cnab + +import ( + "encoding/json" + "errors" + "fmt" + + v2 "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v2" +) + +const ( + // DependenciesV2ExtensionShortHand is the short suffix of the DependenciesV2ExtensionKey + DependenciesV2ExtensionShortHand = "dependencies.v2" + + // DependenciesV2ExtensionKey represents the full key for the DependenciesV2Extension. + DependenciesV2ExtensionKey = PorterExtension + "." + DependenciesV2ExtensionShortHand + + // DependenciesV2Schema represents the schema for the DependenciesV2 Extension + DependenciesV2Schema = "https://porter.sh/extensions/dependencies/v2/schema.json" +) + +// DependenciesV2Extension represents the required extension to enable dependencies +var DependenciesV2Extension = RequiredExtension{ + Shorthand: DependenciesV2ExtensionShortHand, + Key: DependenciesV2ExtensionKey, + Schema: DependenciesV2Schema, + Reader: func(b ExtendedBundle) (interface{}, error) { + return b.DependencyV2Reader() + }, +} + +// ReadDependenciesV2 is a convenience method for returning a bonafide +// DependenciesV2 reference after reading from the applicable section from +// the provided bundle +func (b ExtendedBundle) ReadDependenciesV2() (v2.Dependencies, error) { + raw, err := b.DependencyV2Reader() + if err != nil { + return v2.Dependencies{}, err + } + + deps, ok := raw.(v2.Dependencies) + if !ok { + return v2.Dependencies{}, errors.New("unable to read dependencies v2 extension data") + } + + // Return the dependencies + return deps, nil +} + +// DependencyV2Reader is a Reader for the DependenciesV2Extension, which reads +// from the applicable section in the provided bundle and returns the raw +// data in the form of an interface +func (b ExtendedBundle) DependencyV2Reader() (interface{}, error) { + data, ok := b.Custom[DependenciesV2ExtensionKey] + if !ok { + return nil, errors.New("attempted to read dependencies from bundle but none are defined") + } + + dataB, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("could not marshal the untyped dependencies extension data %q: %w", string(dataB), err) + } + + deps := v2.Dependencies{} + err = json.Unmarshal(dataB, &deps) + if err != nil { + return nil, fmt.Errorf("could not unmarshal the dependencies extension %q: %w", string(dataB), err) + } + + return deps, nil +} + +// SupportsDependenciesV2 checks if the bundle supports dependencies +func (b ExtendedBundle) SupportsDependenciesV2() bool { + return b.SupportsExtension(DependenciesV2ExtensionKey) +} + +// HasDependenciesV2 returns whether or not the bundle has parameter sources defined. +func (b ExtendedBundle) HasDependenciesV2() bool { + _, ok := b.Custom[DependenciesV2ExtensionKey] + return ok +} diff --git a/pkg/cnab/dependencies_v2_test.go b/pkg/cnab/dependencies_v2_test.go new file mode 100644 index 0000000000..ad0a648f71 --- /dev/null +++ b/pkg/cnab/dependencies_v2_test.go @@ -0,0 +1,79 @@ +package cnab + +import ( + "os" + "testing" + + "github.com/cnabio/cnab-go/bundle" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadDependencyV2Properties(t *testing.T) { + t.Parallel() + + data, err := os.ReadFile("testdata/bundle-depsv2.json") + require.NoError(t, err, "cannot read bundle file") + + b, err := bundle.Unmarshal(data) + require.NoError(t, err, "could not unmarshal the bundle") + + bun := ExtendedBundle{*b} + assert.True(t, bun.HasDependenciesV2()) + + deps, err := bun.ReadDependenciesV2() + require.NoError(t, err) + + assert.NotNil(t, deps, "DependenciesV2 was not populated") + assert.Len(t, deps.Requires, 2, "DependenciesV2.Requires is the wrong length") + + dep := deps.Requires["storage"] + assert.NotNil(t, dep, "expected DependenciesV2.Requires to have an entry for 'storage'") + assert.Equal(t, "somecloud/blob-storage", dep.Bundle, "DependencyV2.Bundle is incorrect") + assert.Empty(t, dep.Version, "DependencyV2.Version should be nil") + + dep = deps.Requires["mysql"] + assert.NotNil(t, dep, "expected DependenciesV2.Requires to have an entry for 'mysql'") + assert.Equal(t, "somecloud/mysql", dep.Bundle, "DependencyV2.Bundle is incorrect") + assert.Equal(t, "5.7.x", dep.Version, "DependencyV2.Bundle.Version is incorrect") + +} + +func TestSupportsDependenciesV2(t *testing.T) { + t.Parallel() + + t.Run("supported", func(t *testing.T) { + b := ExtendedBundle{bundle.Bundle{ + RequiredExtensions: []string{DependenciesV2ExtensionKey}, + }} + + assert.True(t, b.SupportsDependenciesV2()) + }) + t.Run("unsupported", func(t *testing.T) { + b := ExtendedBundle{} + + assert.False(t, b.SupportsDependenciesV2()) + }) +} + +func TestHasDependenciesV2(t *testing.T) { + t.Parallel() + + t.Run("has dependencies", func(t *testing.T) { + b := ExtendedBundle{bundle.Bundle{ + RequiredExtensions: []string{DependenciesV2ExtensionKey}, + Custom: map[string]interface{}{ + DependenciesV2ExtensionKey: struct{}{}, + }, + }} + + assert.True(t, b.HasDependenciesV2()) + }) + t.Run("no dependencies", func(t *testing.T) { + b := ExtendedBundle{bundle.Bundle{ + RequiredExtensions: []string{DependenciesV2ExtensionKey}, + }} + + assert.False(t, b.HasDependenciesV2()) + }) +} diff --git a/pkg/cnab/extensions.go b/pkg/cnab/extensions.go index b1698ff9d6..60c5357aa9 100644 --- a/pkg/cnab/extensions.go +++ b/pkg/cnab/extensions.go @@ -1,5 +1,7 @@ package cnab +import "fmt" + const ( // PorterExtension is the key for all Porter configuration stored the the custom section of bundles. PorterExtension = "sh.porter" @@ -15,6 +17,69 @@ const ( PorterInternal = "porter-internal" ) +// RequiredExtension represents a required extension that is known and supported by Porter +type RequiredExtension struct { + Shorthand string + Key string + Schema string + Reader func(b ExtendedBundle) (interface{}, error) +} + +// SupportedExtensions represent a listing of the current required extensions +// that Porter supports +var SupportedExtensions = []RequiredExtension{ + DependenciesV1Extension, + DependenciesV2Extension, + DockerExtension, + FileParameterExtension, + ParameterSourcesExtension, +} + +// ProcessedExtensions represents a map of the extension name to the +// processed extension configuration +type ProcessedExtensions map[string]interface{} + +// ProcessRequiredExtensions checks all required extensions in the provided +// bundle and makes sure Porter supports them. +// +// If an unsupported required extension is found, an error is returned. +// +// For each supported required extension, the configuration for that extension +// is read and returned in the form of a map of the extension name to +// the extension configuration +func (b ExtendedBundle) ProcessRequiredExtensions() (ProcessedExtensions, error) { + processed := ProcessedExtensions{} + for _, reqExt := range b.RequiredExtensions { + supportedExtension, err := GetSupportedExtension(reqExt) + if err != nil { + return processed, err + } + + raw, err := supportedExtension.Reader(b) + if err != nil { + return processed, fmt.Errorf("unable to process extension: %s: %w", reqExt, err) + } + + processed[supportedExtension.Key] = raw + } + + return processed, nil +} + +// GetSupportedExtension returns a supported extension according to the +// provided name, or an error +func GetSupportedExtension(e string) (*RequiredExtension, error) { + for _, ext := range SupportedExtensions { + // TODO(v1) we should only check for the key in v1.0.0 + // We are checking for both because of a bug in the cnab dependencies spec + // https://github.com/cnabio/cnab-spec/issues/403 + if e == ext.Key || e == ext.Shorthand { + return &ext, nil + } + } + return nil, fmt.Errorf("unsupported required extension: %s", e) +} + // SupportsExtension checks if the bundle supports the specified CNAB extension. func (b ExtendedBundle) SupportsExtension(key string) bool { for _, ext := range b.RequiredExtensions { diff --git a/pkg/cnab/extensions/dependencies/v1/doc.go b/pkg/cnab/extensions/dependencies/v1/doc.go new file mode 100644 index 0000000000..b739e8b5c8 --- /dev/null +++ b/pkg/cnab/extensions/dependencies/v1/doc.go @@ -0,0 +1,2 @@ +// Package v1 defines the v1 CNAB Dependency specification, io.cnab.dependencies. +package v1 diff --git a/pkg/cnab/dependencies/v1/types.go b/pkg/cnab/extensions/dependencies/v1/types.go similarity index 100% rename from pkg/cnab/dependencies/v1/types.go rename to pkg/cnab/extensions/dependencies/v1/types.go diff --git a/pkg/cnab/dependencies/v1/types_test.go b/pkg/cnab/extensions/dependencies/v1/types_test.go similarity index 100% rename from pkg/cnab/dependencies/v1/types_test.go rename to pkg/cnab/extensions/dependencies/v1/types_test.go diff --git a/pkg/cnab/extensions/dependencies/v2/doc.go b/pkg/cnab/extensions/dependencies/v2/doc.go new file mode 100644 index 0000000000..b387c3acac --- /dev/null +++ b/pkg/cnab/extensions/dependencies/v2/doc.go @@ -0,0 +1,3 @@ +// Package v2 defines the v2 Porter Dependency specification, +// sh.porter.dependencies.v2. +package v2 diff --git a/pkg/cnab/extensions/dependencies/v2/types.go b/pkg/cnab/extensions/dependencies/v2/types.go new file mode 100644 index 0000000000..7dee2d5e7d --- /dev/null +++ b/pkg/cnab/extensions/dependencies/v2/types.go @@ -0,0 +1,150 @@ +package v2 + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" +) + +// Dependencies describes the set of custom extension metadata associated with the dependencies spec +// https://github.com/cnabio/cnab-spec/blob/master/500-CNAB-dependencies.md +type Dependencies struct { + // Requires is a list of bundles required by this bundle + Requires map[string]Dependency `json:"requires,omitempty" mapstructure:"requires"` +} + +/* +dependencies: + requires: # dependencies are always created in the current namespace, never global though they can match globally? + mysql: + bundle: + reference: getporter/mysql:v1.0.2 + version: 1.x + interface: # Porter defaults the interface based on usage + reference: getporter/generic-mysql-interface:v1.0.0 # point to an interface bundle to be more specific + bundle: # add extra interface requirements + outputs: + - $id: "mysql-5.7-connection-string" # match on something other than name, so that outputs with different names can be reused + installation: + labels: # labels applied to the installation if created + app: myapp + installation: {{ installation.name }} # exclusive resource + criteria: # criteria for reusing an existing installation, by default must be the same bundle, labels and allows global + matchInterface: true # only match the interface, not the bundle too + matchNamespace: true # must be in the same namespace, disallow global + ignoreLabels: true # allow different labels +*/ + +// Dependency describes a dependency on another bundle +type Dependency struct { + // Name of the dependency + Name string `json:"name" mapstructure:"name"` + + // Bundle is the location of the bundle in a registry, for example REGISTRY/NAME:TAG + Bundle string `json:"bundle" mapstructure:"bundle"` + + // Version is a set of allowed versions + Version string `json:"version,omitempty" mapstructure:"version"` + + Interface *DependencyInterface `json:"interface,omitempty" mapstructure:"interface,omitempty"` + + Installation *DependencyInstallation `json:"installation,omitempty" mapstructure:"installation,omitempty"` + + Parameters map[string]DependencySource `json:"parameters,omitempty" mapstructure:"parameters,omitempty"` + Credentials map[string]DependencySource `json:"credentials,omitempty" mapstructure:"credentials,omitempty"` +} + +type DependencySource struct { + Value string `json:"value,omitempty" mapstructure:"value,omitempty"` + Dependency string `json:"dependency,omitempty" mapstructure:"dependency,omitempty"` + Credential string `json:"credential,omitempty" mapstructure:"credential,omitempty"` + Parameter string `json:"parameter,omitempty" mapstructure:"parameter,omitempty"` + Output string `json:"output,omitempty" mapstructure:"output,omitempty"` +} + +// ParseDependencySource identifies the components specified in a wiring string. +func ParseDependencySource(value string) (DependencySource, error) { + // TODO(PEP003): At build time, check if a dependency source was defined with templating and error out + // e.g. ${bundle.parameters.foo} should be bundle.parameters.foo + + regex := regexp.MustCompile(`bundle(\.dependencies\.([^.]+))?\.([^.]+)\.(.+)`) + matches := regex.FindStringSubmatch(value) + + // If it doesn't match our wiring syntax, assume that it is a hard coded value + if matches == nil || len(matches) < 5 { + return DependencySource{Value: value}, nil + } + + dependencyName := matches[2] // bundle.dependencies.DEPENDENCY_NAME + itemType := matches[3] // bundle.dependencies.dependency_name.PARAMETERS.name or bundle.OUTPUTS.name + itemName := matches[4] // bundle.dependencies.dependency_name.parameters.NAME or bundle.outputs.NAME + + result := DependencySource{Dependency: dependencyName} + switch itemType { + case "parameters": + result.Parameter = itemName + case "credentials": + result.Credential = itemName + case "outputs": + // Cannot pass the root bundle's output to a dependency + // Check that we are attempting to pass another dependency's output + if dependencyName == "" { + return DependencySource{}, errors.New("cannot pass the root bundle output to a dependency") + } + result.Output = itemName + } + return result, nil +} + +// AsBundleWiring is the wiring string representation in the bundle definition. +// For example, bundle.parameters.PARAM or bundle.dependencies.DEP.outputs.OUTPUT +func (s DependencySource) AsBundleWiring() string { + suffix := s.WiringSuffix() + if s.Dependency != "" { + return fmt.Sprintf("bundle.dependencies.%s.%s", s.Dependency, suffix) + } + + return fmt.Sprintf("bundle.%s", suffix) +} + +// AsWorkflowWiring is the wiring string representation in a workflow definition. +// For example, workflow.jobs.JOB.outputs.OUTPUT +func (s DependencySource) AsWorkflowWiring(jobID string) string { + return fmt.Sprintf("workflow.jobs.%s.%s", jobID, s.WiringSuffix()) +} + +// WiringSuffix identifies the data to retrieve from the source. +// For example, parameters.PARAM or outputs.OUTPUT +func (s DependencySource) WiringSuffix() string { + if s.Parameter != "" { + return fmt.Sprintf("parameters.%s", s.Parameter) + } + + if s.Credential != "" { + return fmt.Sprintf("credentials.%s", s.Credential) + } + + if s.Output != "" { + return fmt.Sprintf("outputs.%s", s.Output) + } + + return s.Value +} + +type DependencyInstallation struct { + Labels map[string]string `json:"labels,omitempty" mapstructure:"labels,omitempty"` + Criteria *InstallationCriteria `json:"criteria,omitempty" mapstructure:"criteria,omitempty"` +} + +type InstallationCriteria struct { + // MatchInterface specifies if the installation should use the same bundle or just needs to match the interface + MatchInterface bool `json:"matchInterface,omitempty" mapstructure:"matchInterface,omitEmpty"` + MatchNamespace bool `json:"matchNamespace,omitempty" mapstructure:"matchNamespace,omitEmpty"` + IgnoreLabels bool `json:"ignoreLabels,omitempty" mapstructure:"ignoreLabels,omitempty"` +} + +type DependencyInterface struct { + Reference string `json:"reference,omitempty" mapstructure:"reference,omitempty"` + Document *json.RawMessage `json:"document,omitempty" mapstructure:"document,omitempty"` +} diff --git a/pkg/cnab/extensions/dependencies/v2/types_test.go b/pkg/cnab/extensions/dependencies/v2/types_test.go new file mode 100644 index 0000000000..7cee2ce1d4 --- /dev/null +++ b/pkg/cnab/extensions/dependencies/v2/types_test.go @@ -0,0 +1,91 @@ +package v2 + +import ( + "testing" + + "get.porter.sh/porter/tests" + "github.com/stretchr/testify/require" +) + +func TestDependencySource(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + bundleWiring string + wantSource DependencySource + wantWorkflowWiring string + wantErr string + }{ + { + name: "parameter", + bundleWiring: "bundle.parameters.color", + wantSource: DependencySource{ + Parameter: "color", + }, + wantWorkflowWiring: "workflow.jobs.1.parameters.color", + }, + { + name: "credential", + bundleWiring: "bundle.credentials.kubeconfig", + wantSource: DependencySource{ + Credential: "kubeconfig", + }, + wantWorkflowWiring: "workflow.jobs.1.credentials.kubeconfig", + }, + { + name: "invalid: output", + bundleWiring: "bundle.outputs.port", + wantErr: "cannot pass the root bundle output to a dependency", + }, + { + name: "dependency parameter", + bundleWiring: "bundle.dependencies.mysql.parameters.name", + wantSource: DependencySource{ + Dependency: "mysql", + Parameter: "name", + }, + wantWorkflowWiring: "workflow.jobs.1.parameters.name", + }, + { + name: "dependency credential", + bundleWiring: "bundle.dependencies.mysql.credentials.password", + wantSource: DependencySource{ + Dependency: "mysql", + Credential: "password", + }, + wantWorkflowWiring: "workflow.jobs.1.credentials.password", + }, + { + name: "dependency output", + bundleWiring: "bundle.dependencies.mysql.outputs.connstr", + wantSource: DependencySource{ + Dependency: "mysql", + Output: "connstr", + }, + wantWorkflowWiring: "workflow.jobs.1.outputs.connstr", + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + gotSource, err := ParseDependencySource(tc.bundleWiring) + if tc.wantErr == "" { + require.Equal(t, tc.wantSource, gotSource, "incorrect DependencySource was parsed") + + // Check that we can convert it back to a bundle wiring string + gotBundleWiring := gotSource.AsBundleWiring() + require.Equal(t, tc.bundleWiring, gotBundleWiring, "incorrect bundle wiring was returned") + + // Check that we can convert to a workflow wiring form + gotWorkflowWiring := gotSource.AsWorkflowWiring("1") + require.Equal(t, tc.wantWorkflowWiring, gotWorkflowWiring, "incorrect workflow wiring was returned") + } else { + tests.RequireErrorContains(t, err, tc.wantErr) + } + }) + } +} diff --git a/pkg/cnab/extensions_test.go b/pkg/cnab/extensions_test.go index 4045244d69..907018d2d2 100644 --- a/pkg/cnab/extensions_test.go +++ b/pkg/cnab/extensions_test.go @@ -1,12 +1,108 @@ package cnab import ( + "fmt" "testing" + depsv1ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v1" "github.com/cnabio/cnab-go/bundle" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestProcessRequiredExtensions(t *testing.T) { + t.Parallel() + + t.Run("supported", func(t *testing.T) { + t.Parallel() + + bun := ReadTestBundle(t, "testdata/bundle.json") + exts, err := bun.ProcessRequiredExtensions() + require.NoError(t, err, "could not process required extensions") + + expected := ProcessedExtensions{ + "sh.porter.file-parameters": nil, + "io.cnab.dependencies": depsv1ext.Dependencies{ + Requires: map[string]depsv1ext.Dependency{ + "storage": depsv1ext.Dependency{ + Bundle: "somecloud/blob-storage", + }, + "mysql": depsv1ext.Dependency{ + Bundle: "somecloud/mysql", + Version: &depsv1ext.DependencyVersion{ + AllowPrereleases: true, + Ranges: []string{"5.7.x"}, + }, + }, + }, + }, + "io.cnab.parameter-sources": ParameterSources{ + "tfstate": ParameterSource{ + Priority: []string{ParameterSourceTypeOutput}, + Sources: ParameterSourceMap{ + ParameterSourceTypeOutput: OutputParameterSource{"tfstate"}, + }, + }, + "mysql_connstr": ParameterSource{ + Priority: []string{ParameterSourceTypeDependencyOutput}, + Sources: ParameterSourceMap{ + ParameterSourceTypeDependencyOutput: DependencyOutputParameterSource{ + Dependency: "mysql", + OutputName: "connstr", + }, + }, + }, + }, + } + require.Equal(t, expected, exts) + }) + + t.Run("supported unprocessable", func(t *testing.T) { + t.Parallel() + + bun := ReadTestBundle(t, "testdata/bundle-supported-unprocessable.json") + _, err := bun.ProcessRequiredExtensions() + require.EqualError(t, err, "unable to process extension: io.cnab.docker: no custom extension configuration found") + }) + + t.Run("unsupported", func(t *testing.T) { + t.Parallel() + + bun := ReadTestBundle(t, "testdata/bundle-unsupported-required.json") + _, err := bun.ProcessRequiredExtensions() + require.EqualError(t, err, "unsupported required extension: donuts") + }) +} + +func TestGetSupportedExtension(t *testing.T) { + t.Parallel() + + for _, supported := range SupportedExtensions { + t.Run(fmt.Sprintf("%s - shorthand", supported.Shorthand), func(t *testing.T) { + t.Parallel() + + ext, err := GetSupportedExtension(supported.Shorthand) + require.NoError(t, err) + require.Equal(t, supported.Key, ext.Key) + }) + + t.Run(fmt.Sprintf("%s - key", supported.Key), func(t *testing.T) { + t.Parallel() + + ext, err := GetSupportedExtension(supported.Key) + require.NoError(t, err) + require.Equal(t, supported.Key, ext.Key) + }) + } + + t.Run("unsupported", func(t *testing.T) { + t.Parallel() + + _, err := GetSupportedExtension("donuts") + require.EqualError(t, err, "unsupported required extension: donuts") + }) +} + func TestSupportsExtension(t *testing.T) { t.Run("key present", func(t *testing.T) { b := NewBundle(bundle.Bundle{RequiredExtensions: []string{"io.test.thing"}}) diff --git a/pkg/cnab/reference.go b/pkg/cnab/oci_reference.go similarity index 100% rename from pkg/cnab/reference.go rename to pkg/cnab/oci_reference.go diff --git a/pkg/cnab/reference_test.go b/pkg/cnab/oci_reference_test.go similarity index 100% rename from pkg/cnab/reference_test.go rename to pkg/cnab/oci_reference_test.go diff --git a/pkg/cnab/required.go b/pkg/cnab/required.go deleted file mode 100644 index 98888eba6f..0000000000 --- a/pkg/cnab/required.go +++ /dev/null @@ -1,65 +0,0 @@ -package cnab - -import "fmt" - -// RequiredExtension represents a required extension that is known and supported by Porter -type RequiredExtension struct { - Shorthand string - Key string - Schema string - Reader func(b ExtendedBundle) (interface{}, error) -} - -// SupportedExtensions represent a listing of the current required extensions -// that Porter supports -var SupportedExtensions = []RequiredExtension{ - DependenciesV1Extension, - DockerExtension, - FileParameterExtension, - ParameterSourcesExtension, -} - -// ProcessedExtensions represents a map of the extension name to the -// processed extension configuration -type ProcessedExtensions map[string]interface{} - -// ProcessRequiredExtensions checks all required extensions in the provided -// bundle and makes sure Porter supports them. -// -// If an unsupported required extension is found, an error is returned. -// -// For each supported required extension, the configuration for that extension -// is read and returned in the form of a map of the extension name to -// the extension configuration -func (b ExtendedBundle) ProcessRequiredExtensions() (ProcessedExtensions, error) { - processed := ProcessedExtensions{} - for _, reqExt := range b.RequiredExtensions { - supportedExtension, err := GetSupportedExtension(reqExt) - if err != nil { - return processed, err - } - - raw, err := supportedExtension.Reader(b) - if err != nil { - return processed, fmt.Errorf("unable to process extension: %s: %w", reqExt, err) - } - - processed[supportedExtension.Key] = raw - } - - return processed, nil -} - -// GetSupportedExtension returns a supported extension according to the -// provided name, or an error -func GetSupportedExtension(e string) (*RequiredExtension, error) { - for _, ext := range SupportedExtensions { - // TODO(v1) we should only check for the key in v1.0.0 - // We are checking for both because of a bug in the cnab dependencies spec - // https://github.com/cnabio/cnab-spec/issues/403 - if e == ext.Key || e == ext.Shorthand { - return &ext, nil - } - } - return nil, fmt.Errorf("unsupported required extension: %s", e) -} diff --git a/pkg/cnab/required_test.go b/pkg/cnab/required_test.go deleted file mode 100644 index 433388cd0e..0000000000 --- a/pkg/cnab/required_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package cnab - -import ( - "fmt" - "testing" - - depsv1 "get.porter.sh/porter/pkg/cnab/dependencies/v1" - "github.com/stretchr/testify/require" -) - -func TestProcessRequiredExtensions(t *testing.T) { - t.Parallel() - - t.Run("supported", func(t *testing.T) { - t.Parallel() - - bun := ReadTestBundle(t, "testdata/bundle.json") - exts, err := bun.ProcessRequiredExtensions() - require.NoError(t, err, "could not process required extensions") - - expected := ProcessedExtensions{ - "sh.porter.file-parameters": nil, - "io.cnab.dependencies": depsv1.Dependencies{ - Requires: map[string]depsv1.Dependency{ - "storage": depsv1.Dependency{ - Bundle: "somecloud/blob-storage", - }, - "mysql": depsv1.Dependency{ - Bundle: "somecloud/mysql", - Version: &depsv1.DependencyVersion{ - AllowPrereleases: true, - Ranges: []string{"5.7.x"}, - }, - }, - }, - }, - "io.cnab.parameter-sources": ParameterSources{ - "tfstate": ParameterSource{ - Priority: []string{ParameterSourceTypeOutput}, - Sources: ParameterSourceMap{ - ParameterSourceTypeOutput: OutputParameterSource{"tfstate"}, - }, - }, - "mysql_connstr": ParameterSource{ - Priority: []string{ParameterSourceTypeDependencyOutput}, - Sources: ParameterSourceMap{ - ParameterSourceTypeDependencyOutput: DependencyOutputParameterSource{ - Dependency: "mysql", - OutputName: "connstr", - }, - }, - }, - }, - } - require.Equal(t, expected, exts) - }) - - t.Run("supported unprocessable", func(t *testing.T) { - t.Parallel() - - bun := ReadTestBundle(t, "testdata/bundle-supported-unprocessable.json") - _, err := bun.ProcessRequiredExtensions() - require.EqualError(t, err, "unable to process extension: io.cnab.docker: no custom extension configuration found") - }) - - t.Run("unsupported", func(t *testing.T) { - t.Parallel() - - bun := ReadTestBundle(t, "testdata/bundle-unsupported-required.json") - _, err := bun.ProcessRequiredExtensions() - require.EqualError(t, err, "unsupported required extension: donuts") - }) -} - -func TestGetSupportedExtension(t *testing.T) { - t.Parallel() - - for _, supported := range SupportedExtensions { - t.Run(fmt.Sprintf("%s - shorthand", supported.Shorthand), func(t *testing.T) { - t.Parallel() - - ext, err := GetSupportedExtension(supported.Shorthand) - require.NoError(t, err) - require.Equal(t, supported.Key, ext.Key) - }) - - t.Run(fmt.Sprintf("%s - key", supported.Key), func(t *testing.T) { - t.Parallel() - - ext, err := GetSupportedExtension(supported.Key) - require.NoError(t, err) - require.Equal(t, supported.Key, ext.Key) - }) - } - - t.Run("unsupported", func(t *testing.T) { - t.Parallel() - - _, err := GetSupportedExtension("donuts") - require.EqualError(t, err, "unsupported required extension: donuts") - }) -} diff --git a/pkg/cnab/testdata/bundle-depsv2.json b/pkg/cnab/testdata/bundle-depsv2.json new file mode 100644 index 0000000000..a60f874539 --- /dev/null +++ b/pkg/cnab/testdata/bundle-depsv2.json @@ -0,0 +1,140 @@ +{ + "name": "foo", + "version": "1.0", + "schemaVersion": "99.99", + "invocationImages": [ + { + "imageType": "docker", + "image": "technosophos/helloworld:0.1.0" + } + ], + "images": { + "image1": { + "description": "image1", + "image": "urn:image1uri", + "refs": [ + { + "path": "image1path", + "field": "image.1.field" + } + ] + }, + "image2": { + "name": "image2", + "uri": "urn:image2uri", + "refs": [ + { + "path": "image2path", + "field": "image.2.field" + } + ] + } + }, + "credentials": { + "foo": { + "path": "pfoo" + }, + "bar": { + "env": "ebar" + }, + "quux": { + "path": "pquux", + "env": "equux" + } + }, + "requiredExtensions": [ + "sh.porter.dependencies.v2", + "io.cnab.parameter-sources", + "sh.porter.file-parameters" + ], + "custom": { + "com.example.duffle-bag": { + "icon": "https://example.com/icon.png", + "iconType": "PNG" + }, + "com.example.backup-preferences": { + "enabled": true, + "frequency": "daily" + }, + "sh.porter.dependencies.v2": { + "requires": { + "storage": { + "bundle": "somecloud/blob-storage" + }, + "mysql": { + "bundle": "somecloud/mysql", + "version": "5.7.x" + } + } + }, + "io.cnab.parameter-sources": { + "tfstate": { + "priority": ["output"], + "sources": { + "output": { + "name": "tfstate" + } + } + }, + "mysql_connstr": { + "priority": ["dependencies.output"], + "sources": { + "dependencies.output": { + "dependency": "mysql", + "name": "connstr" + } + } + } + } + }, + "definitions": { + "complexThing": { + "type": "object", + "properties": { + "host": { + "default": "localhost", + "type": "string", + "minLength": 3, + "maxLength": 10 + }, + "port": { + "type": "integer", + "minimum": 8000 + } + }, + "required": [ + "port" + ] + }, + "mysql_connstr": { + "type": "string" + }, + "tfstate": { + "contentEncoding": "base64", + "type": "string" + } + }, + "parameters": { + "serverConfig": { + "definition": "complexThing", + "destination": { + "path": "/cnab/is/go" + } + }, + "tfstate": { + "applyTo": [ "upgrade", "uninstall" ], + "definition": "tfstate", + "required": true + }, + "mysql_connstr": { + "definition": "mysql_connstr" + } + }, + "outputs": { + "tfstate": { + "applyTo": [ "install", "upgrade", "uninstall" ], + "definition": "tfstate", + "path": "/cnab/app/outputs/tfstate" + } + } +} diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go index d1f2d389ae..9404b62435 100644 --- a/pkg/manifest/manifest.go +++ b/pkg/manifest/manifest.go @@ -273,6 +273,34 @@ func (m *Manifest) GetTemplatedDependencyOutputs() DependencyOutputReferences { return outputs } +// DetermineDependenciesExtensionUsed looks for how dependencies are used +// by the bundle and which version of the dependency extension can be used. +func (m *Manifest) DetermineDependenciesExtensionUsed() string { + if len(m.Dependencies.Requires) == 0 { + // dependencies are not used at all + return "" + } + + // Check if v2 deps are explicitly specified + for _, ext := range m.Required { + if ext.Name == cnab.DependenciesV2ExtensionShortHand || + ext.Name == cnab.DependenciesV2ExtensionKey { + return cnab.DependenciesV2ExtensionKey + } + } + + // Check each dependency for use of v2 only features + for _, dep := range m.Dependencies.Requires { + if dep.Installation != nil || + len(dep.Credentials) > 0 || + dep.Bundle.Interface != nil { + return cnab.DependenciesV2ExtensionKey + } + } + + return cnab.DependenciesV1ExtensionKey +} + type CustomDefinitions map[string]interface{} func (cd *CustomDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error { @@ -630,11 +658,14 @@ type Dependencies struct { type Dependency struct { Name string `yaml:"name"` - Bundle BundleCriteria `yaml:"bundle"` - - Parameters map[string]string `yaml:"parameters,omitempty"` + Bundle BundleCriteria `yaml:"bundle"` + Installation *DependencyInstallationConfig `yaml:"installation,omitempty"` + Parameters map[string]string `yaml:"parameters,omitempty"` + Credentials map[string]string `yaml:"credentials,omitempty"` } +type DependencySource string + type BundleCriteria struct { // Reference is the full bundle reference for the dependency // in the format REGISTRY/NAME:TAG @@ -645,7 +676,24 @@ type BundleCriteria struct { // This includes considering prereleases to be invalid if the ranges does not include one. // If you want to have it include pre-releases a simple solution is to include -0 in your range." // https://github.com/Masterminds/semver/blob/master/README.md#checking-version-constraints - Version string `yaml:"version,omitempty"` + Version string `yaml:"version,omitempty"` + Interface *BundleInterface `yaml:"interface,omitempty"` +} + +type BundleInterface struct { + Reference string `yaml:"reference,omitempty"` + Document map[string]interface{} `yaml:"bundle,omitempty"` +} + +type DependencyInstallationConfig struct { + Labels map[string]string `yaml:"labels,omitempty"` + Criteria *InstallationCriteria `yaml:"criteria,omitempty"` +} + +type InstallationCriteria struct { + MatchInterface bool `yaml:"matchInterface,omitempty"` + MatchNamespace bool `yaml:"matchNamespace,omitempty"` + IgnoreLabels bool `yaml:"ignoreLabels,omitempty"` } func (d *Dependency) Validate(cxt *portercontext.Context) error { @@ -657,8 +705,13 @@ func (d *Dependency) Validate(cxt *portercontext.Context) error { return fmt.Errorf("reference is required for dependency %q", d.Name) } - if strings.Contains(d.Bundle.Reference, ":") && len(d.Bundle.Version) > 0 { - return fmt.Errorf("reference for dependency %q can only specify REGISTRY/NAME when version ranges are specified", d.Name) + ref, err := cnab.ParseOCIReference(d.Bundle.Reference) + if err != nil { + return fmt.Errorf("invalid reference %s for dependency %s: %w", d.Bundle.Reference, d.Name, err) + } + + if ref.IsRepositoryOnly() && d.Bundle.Version == "" { + 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) } return nil diff --git a/pkg/manifest/manifest_test.go b/pkg/manifest/manifest_test.go index 08615fe937..d024a52cd2 100644 --- a/pkg/manifest/manifest_test.go +++ b/pkg/manifest/manifest_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "get.porter.sh/porter/pkg/cnab" "get.porter.sh/porter/pkg/config" "get.porter.sh/porter/pkg/portercontext" "get.porter.sh/porter/pkg/schema" @@ -93,24 +94,28 @@ func TestLoadManifestWithDependencies(t *testing.T) { assert.Equal(t, "getporter/azure-mysql:5.7", m.Dependencies.Requires[0].Bundle.Reference, "expected a v1 schema for the dependency delcaration") } -func TestLoadManifestWithDependenciesInOrder(t *testing.T) { +func TestLoadManifestWithDependenciesV2(t *testing.T) { c := config.NewTestConfig(t) - c.TestContext.AddTestFile("testdata/porter-with-deps.yaml", config.Name) + c.TestContext.AddTestFile("testdata/porter-depsv2.yaml", config.Name) c.TestContext.AddTestDirectory("testdata/bundles", "bundles") m, err := LoadManifestFrom(context.Background(), c.Config, config.Name) require.NoError(t, err, "could not load manifest") + assert.NotNil(t, m) + assert.Equal(t, []MixinDeclaration{{Name: "exec"}}, m.Mixins) + assert.Len(t, m.Install, 1) + + installStep := m.Install[0] + description, _ := installStep.GetDescription() + require.NotNil(t, description) - nginxDep := m.Dependencies.Requires[0] - assert.Equal(t, "nginx", nginxDep.Name) - assert.Equal(t, "localhost:5000/nginx:1.19", nginxDep.Bundle.Reference) + mixin := installStep.GetMixinName() + assert.Equal(t, "exec", mixin) - mysqlDep := m.Dependencies.Requires[1] - assert.Equal(t, "mysql", mysqlDep.Name) - assert.Equal(t, "getporter/azure-mysql:5.7", mysqlDep.Bundle.Reference) - assert.Len(t, mysqlDep.Parameters, 1) + require.Len(t, m.Dependencies.Requires, 1, "expected one dependency") + assert.Equal(t, "getporter/azure-mysql:5.7", m.Dependencies.Requires[0].Bundle.Reference, "expected a v2 schema for the dependency delcaration") } @@ -845,3 +850,104 @@ func TestManifest_getTemplatePrefix(t *testing.T) { }) } } + +func TestManifest_DetermineDependenciesExtensionUsed(t *testing.T) { + t.Run("no dependencies used", func(t *testing.T) { + m := Manifest{} + depsExt := m.DetermineDependenciesExtensionUsed() + assert.Empty(t, depsExt) + }) + + t.Run("v1 features only", func(t *testing.T) { + m := Manifest{ + Dependencies: Dependencies{Requires: []*Dependency{ + { + Name: "mysql", + Bundle: BundleCriteria{Reference: "mysql:5.7", Version: "5.7 - 6"}, + Parameters: map[string]string{"loglevel": "4"}, + }, + }}, + } + depsExt := m.DetermineDependenciesExtensionUsed() + assert.Equal(t, cnab.DependenciesV1ExtensionKey, depsExt) + }) + + t.Run("v2 declared but no deps defined", func(t *testing.T) { + m := Manifest{ + Required: []RequiredExtension{ + {Name: cnab.DependenciesV2ExtensionShortHand}, + }, + } + depsExt := m.DetermineDependenciesExtensionUsed() + assert.Empty(t, depsExt) + }) + + t.Run("v2 shorthand declared", func(t *testing.T) { + m := Manifest{ + Required: []RequiredExtension{ + {Name: cnab.DependenciesV2ExtensionShortHand}, + }, + Dependencies: Dependencies{Requires: []*Dependency{ + {Name: "mysql", Bundle: BundleCriteria{Reference: "mysql:5.7", Version: "5.7 - 6"}}, + }}, + } + depsExt := m.DetermineDependenciesExtensionUsed() + assert.Equal(t, cnab.DependenciesV2ExtensionKey, depsExt) + }) + + t.Run("v2 full key declared", func(t *testing.T) { + m := Manifest{ + Required: []RequiredExtension{ + {Name: cnab.DependenciesV2ExtensionKey}, + }, + Dependencies: Dependencies{Requires: []*Dependency{ + {Name: "mysql", Bundle: BundleCriteria{Reference: "mysql:5.7", Version: "5.7 - 6"}}, + }}, + } + depsExt := m.DetermineDependenciesExtensionUsed() + assert.Equal(t, cnab.DependenciesV2ExtensionKey, depsExt) + }) + + t.Run("bundle interface criteria used", func(t *testing.T) { + m := Manifest{ + Dependencies: Dependencies{Requires: []*Dependency{ + { + Name: "mysql", + Bundle: BundleCriteria{ + Reference: "mysql:5.7", + Version: "5.7 - 6", + Interface: &BundleInterface{}}}, + }}, + } + depsExt := m.DetermineDependenciesExtensionUsed() + assert.Equal(t, cnab.DependenciesV2ExtensionKey, depsExt) + }) + + t.Run("installation criteria used", func(t *testing.T) { + m := Manifest{ + Dependencies: Dependencies{Requires: []*Dependency{ + { + Name: "mysql", + Bundle: BundleCriteria{Reference: "mysql:5.7", Version: "5.7 - 6"}, + Installation: &DependencyInstallationConfig{}, + }, + }}, + } + depsExt := m.DetermineDependenciesExtensionUsed() + assert.Equal(t, cnab.DependenciesV2ExtensionKey, depsExt) + }) + + t.Run("credential wiring used", func(t *testing.T) { + m := Manifest{ + Dependencies: Dependencies{Requires: []*Dependency{ + { + Name: "mysql", + Bundle: BundleCriteria{Reference: "mysql:5.7", Version: "5.7 - 6"}, + Credentials: map[string]string{"kubeconfig": "bundle.credentials.kubeconfig"}, + }, + }}, + } + depsExt := m.DetermineDependenciesExtensionUsed() + assert.Equal(t, cnab.DependenciesV2ExtensionKey, depsExt) + }) +} diff --git a/pkg/manifest/testdata/porter-depsv2.yaml b/pkg/manifest/testdata/porter-depsv2.yaml new file mode 100644 index 0000000000..3a4cddfbef --- /dev/null +++ b/pkg/manifest/testdata/porter-depsv2.yaml @@ -0,0 +1,37 @@ +schemaVersion: 1.0.0-alpha.1 +name: mybun +version: 0.1.0 +registry: example.com + +mixins: + - exec + +dependencies: + requires: + - name: mysql + bundle: + reference: "getporter/azure-mysql:5.7" + parameters: + database-name: wordpress + +install: + - exec: + command: bash + flags: + c: echo Hello World + +uninstall: + - exec: + description: "Uninstall Hello World" + command: bash + flags: + c: echo Goodbye World + +custom: + foo: bar + +required: + - requiredExtension1 + - requiredExtension2: + config: true + diff --git a/pkg/porter/dependencies.go b/pkg/porter/dependencies.go index c8aac16b43..a89acdfb3b 100644 --- a/pkg/porter/dependencies.go +++ b/pkg/porter/dependencies.go @@ -52,7 +52,7 @@ func newDependencyExecutioner(p *Porter, installation storage.Installation, acti } type queuedDependency struct { - cnab.DependencyLock + depsv1.DependencyLock BundleReference cnab.BundleReference Parameters map[string]string @@ -158,7 +158,7 @@ func (e *dependencyExecutioner) identifyDependencies(ctx context.Context) error return span.Error(errors.New("identifyDependencies failed to load the bundle because no bundle was specified. Please report this bug to https://github.com/getporter/porter/issues/new/choose")) } - solver := &cnab.DependencySolver{} + solver := &depsv1.DependencySolver{} locks, err := solver.ResolveDependencies(bun) if err != nil { return span.Error(err) diff --git a/pkg/porter/explain.go b/pkg/porter/explain.go index 8b5ffbe613..29b6de8d4e 100644 --- a/pkg/porter/explain.go +++ b/pkg/porter/explain.go @@ -7,6 +7,8 @@ import ( "strconv" "strings" + depsv1 "get.porter.sh/porter/pkg/cnab/dependencies/v1" + "get.porter.sh/porter/pkg/cnab" configadapter "get.porter.sh/porter/pkg/cnab/config-adapter" "get.porter.sh/porter/pkg/portercontext" @@ -189,7 +191,7 @@ func generatePrintable(bun cnab.ExtendedBundle, action string) (*PrintableBundle stamp = configadapter.Stamp{} } - solver := &cnab.DependencySolver{} + solver := &depsv1.DependencySolver{} deps, err := solver.ResolveDependencies(bun) if err != nil { return nil, fmt.Errorf("error resolving bundle dependencies: %w", err) diff --git a/pkg/porter/explain_test.go b/pkg/porter/explain_test.go index d1023454ef..2b958aeb5e 100644 --- a/pkg/porter/explain_test.go +++ b/pkg/porter/explain_test.go @@ -5,7 +5,7 @@ import ( "testing" "get.porter.sh/porter/pkg/cnab" - depsv1 "get.porter.sh/porter/pkg/cnab/dependencies/v1" + depsv1ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v1" "get.porter.sh/porter/pkg/portercontext" "get.porter.sh/porter/pkg/test" "github.com/cnabio/cnab-go/bundle" @@ -417,9 +417,9 @@ func TestExplain_generatePrintableBundleDependencies(t *testing.T) { sequenceMock := []string{"nginx", "storage", "mysql"} bun := cnab.NewBundle(bundle.Bundle{ Custom: map[string]interface{}{ - cnab.DependenciesV1ExtensionKey: depsv1.Dependencies{ + cnab.DependenciesV1ExtensionKey: depsv1ext.Dependencies{ Sequence: sequenceMock, - Requires: map[string]depsv1.Dependency{ + Requires: map[string]depsv1ext.Dependency{ "mysql": { Name: "mysql", Bundle: "somecloud/mysql:0.1.0", diff --git a/pkg/porter/parameters.go b/pkg/porter/parameters.go index 4825f9693c..82605cc0b5 100644 --- a/pkg/porter/parameters.go +++ b/pkg/porter/parameters.go @@ -11,8 +11,9 @@ import ( "strings" "time" - "get.porter.sh/porter/pkg/cnab" depsv1 "get.porter.sh/porter/pkg/cnab/dependencies/v1" + + "get.porter.sh/porter/pkg/cnab" "get.porter.sh/porter/pkg/editor" "get.porter.sh/porter/pkg/encoding" "get.porter.sh/porter/pkg/generator" diff --git a/pkg/porter/testdata/schema.json b/pkg/porter/testdata/schema.json index d5fa1fcdd3..aa8430c8ca 100644 --- a/pkg/porter/testdata/schema.json +++ b/pkg/porter/testdata/schema.json @@ -95,10 +95,21 @@ "bundle": { "$ref": "#/definitions/bundle" }, + "credentials": { + "additionalProperties": { + "type": "string" + }, + "description": "Map of parameter names to a parameter source, such as bundle.parameters.PARAM, bundle.dependencies.DEP.outputs.OUTPUT, or bundle.credentials.CRED", + "type": "object" + }, "name": { "type": "string" }, "parameters": { + "additionalProperties": { + "type": "string" + }, + "description": "Map of parameter names to a parameter source, such as bundle.parameters.PARAM, bundle.dependencies.DEP.outputs.OUTPUT, or bundle.credentials.CRED", "type": "object" } }, @@ -108,6 +119,28 @@ ], "type": "object" }, + "dependencySource": { + "description": "Describes how Porter should set a dependency's parameter or credential", + "properties": { + "credential": { + "description": "The credential name from which the value should be sourced", + "type": "string" + }, + "dependency": { + "description": "The name of the dependency that defines the output used for the source", + "type": "string" + }, + "output": { + "description": "The output name defined in a dependency from which the value should be sourced", + "type": "string" + }, + "parameter": { + "description": "The parameter name from which the value should be sourced", + "type": "string" + } + }, + "type": "object" + }, "image": { "additionalProperties": false, "description": "An image represents an application image used in a bundle", diff --git a/pkg/runtime/runtime_manifest_test.go b/pkg/runtime/runtime_manifest_test.go index bf1e38ca1e..ca65ad9fb4 100644 --- a/pkg/runtime/runtime_manifest_test.go +++ b/pkg/runtime/runtime_manifest_test.go @@ -602,10 +602,15 @@ func TestDependencyV1_Validate(t *testing.T) { wantOutput: "", wantError: `reference is required for dependency "mysql"`, }, { - name: "version double specified", + name: "version not specified", + dep: manifest.Dependency{Name: "mysql", Bundle: manifest.BundleCriteria{Reference: "deislabs/azure-mysql", Version: ""}}, + wantOutput: "", + wantError: `reference for dependency "mysql" can specify only a repository, without a digest or tag, when a version constraint is specified`, + }, { // When a range is specified, but also a default version, we use the default version when we can't find a matching version from the range + name: "default version and range specified", dep: manifest.Dependency{Name: "mysql", Bundle: manifest.BundleCriteria{Reference: "deislabs/azure-mysql:5.7", Version: "5.7.x-6"}}, wantOutput: "", - wantError: `reference for dependency "mysql" can only specify REGISTRY/NAME when version ranges are specified`, + wantError: "", }, } @@ -618,7 +623,7 @@ func TestDependencyV1_Validate(t *testing.T) { if tc.wantError == "" { require.NoError(t, err) } else { - require.Equal(t, tc.wantError, err.Error()) + tests.RequireErrorContains(t, err, tc.wantError) } gotOutput := pCtx.GetOutput() diff --git a/pkg/schema/manifest.schema.json b/pkg/schema/manifest.schema.json index 23619b87f3..2e6156a92f 100644 --- a/pkg/schema/manifest.schema.json +++ b/pkg/schema/manifest.schema.json @@ -174,7 +174,18 @@ "$ref": "#/definitions/bundle" }, "parameters": { - "type": "object" + "description": "Map of parameter names to a parameter source, such as bundle.parameters.PARAM, bundle.dependencies.DEP.outputs.OUTPUT, or bundle.credentials.CRED", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "credentials": { + "description": "Map of parameter names to a parameter source, such as bundle.parameters.PARAM, bundle.dependencies.DEP.outputs.OUTPUT, or bundle.credentials.CRED", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "required": [ @@ -183,6 +194,28 @@ ], "type": "object" }, + "dependencySource": { + "description": "Describes how Porter should set a dependency's parameter or credential", + "type": "object", + "properties": { + "dependency": { + "description": "The name of the dependency that defines the output used for the source", + "type": "string" + }, + "parameter": { + "description": "The parameter name from which the value should be sourced", + "type": "string" + }, + "credential": { + "description": "The credential name from which the value should be sourced", + "type": "string" + }, + "output": { + "description": "The output name defined in a dependency from which the value should be sourced", + "type": "string" + } + } + }, "bundle": { "description": "The defintion of a bundle reference", "properties": { diff --git a/tests/integration/testdata/schema/schema.json b/tests/integration/testdata/schema/schema.json index d5fa1fcdd3..134038db1d 100644 --- a/tests/integration/testdata/schema/schema.json +++ b/tests/integration/testdata/schema/schema.json @@ -95,10 +95,21 @@ "bundle": { "$ref": "#/definitions/bundle" }, + "credentials": { + "additionalProperties": { + "type": "string" + }, + "description": "Map of parameter names to a parameter source, such as bundle.parameters.PARAM, bundle.dependencies.DEP.outputs.OUTPUT, or bundle.credentials.CRED", + "type": "object" + }, "name": { "type": "string" }, "parameters": { + "additionalProperties": { + "type": "string" + }, + "description": "Map of parameter names to a parameter source, such as bundle.parameters.PARAM, bundle.dependencies.DEP.outputs.OUTPUT, or bundle.credentials.CRED", "type": "object" } }, diff --git a/tests/testdata/mybuns/porter.yaml b/tests/testdata/mybuns/porter.yaml index c6e359e96d..c89990e18c 100644 --- a/tests/testdata/mybuns/porter.yaml +++ b/tests/testdata/mybuns/porter.yaml @@ -59,6 +59,10 @@ dependencies: reference: "localhost:5000/mydb:v0.1.0" parameters: database: bigdb + collation: bundle.parameters.db-collation + credentials: + # TODO(PEP003): Update the bundle to have 2 dependencies. Pass an output from one dep to another. + username: bundle.credentials.username images: whalesayd: diff --git a/tests/testdata/mydb/porter.yaml b/tests/testdata/mydb/porter.yaml index 569933f4c9..dbfc088aa9 100644 --- a/tests/testdata/mydb/porter.yaml +++ b/tests/testdata/mydb/porter.yaml @@ -9,7 +9,15 @@ registry: localhost:5000 parameters: - name: database type: string - default: "(default)" + default: "mydb" + - name: collation + type: string + default: "Latin1_General_100_CS_AS" + +credentials: + - name: username + env: USERNAME + required: false outputs: - name: connStr