get.porter.sh/porter@v1.3.0/pkg/cnab/config-adapter/adapter_test.go (about)

     1  package configadapter
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"testing"
     9  
    10  	"get.porter.sh/porter/pkg/cnab"
    11  	depsv1ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v1"
    12  	depsv2ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v2"
    13  	"get.porter.sh/porter/pkg/config"
    14  	"get.porter.sh/porter/pkg/experimental"
    15  	"get.porter.sh/porter/pkg/manifest"
    16  	"get.porter.sh/porter/pkg/mixin"
    17  	"get.porter.sh/porter/pkg/pkgmgmt"
    18  	"github.com/cnabio/cnab-go/bundle"
    19  	"github.com/cnabio/cnab-go/bundle/definition"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  func TestManifestConverter(t *testing.T) {
    25  
    26  	testcases := []struct {
    27  		name          string
    28  		configHandler func(c *config.Config)
    29  		manifestPath  string
    30  		goldenFile    string
    31  		preserveTags  bool
    32  	}{
    33  		{name: "mybuns depsv1",
    34  			configHandler: func(c *config.Config) {},
    35  			manifestPath:  "tests/testdata/mybuns/porter.yaml",
    36  			goldenFile:    "testdata/mybuns-depsv1.bundle.json",
    37  			preserveTags:  false},
    38  		{name: "mybuns depsv1 preserveTags",
    39  			configHandler: func(c *config.Config) {},
    40  			manifestPath:  "tests/testdata/mybuns/porter.yaml",
    41  			goldenFile:    "testdata/mybuns-depsv1.bundle.preserveTags.json",
    42  			preserveTags:  true},
    43  		{name: "mybuns depsv2",
    44  			configHandler: func(c *config.Config) {
    45  				c.SetExperimentalFlags(experimental.FlagDependenciesV2)
    46  			},
    47  			manifestPath: "tests/testdata/mybuns/porter.yaml",
    48  			goldenFile:   "testdata/mybuns-depsv2.bundle.json",
    49  			preserveTags: false},
    50  		{name: "mybuns depsv2 preserveTags",
    51  			configHandler: func(c *config.Config) {
    52  				c.SetExperimentalFlags(experimental.FlagDependenciesV2)
    53  			},
    54  			manifestPath: "tests/testdata/mybuns/porter.yaml",
    55  			goldenFile:   "testdata/mybuns-depsv2.bundle.preserveTags.json",
    56  			preserveTags: true},
    57  		{name: "myenv depsv2",
    58  			configHandler: func(c *config.Config) {
    59  				c.SetExperimentalFlags(experimental.FlagDependenciesV2)
    60  			},
    61  			manifestPath: "tests/testdata/myenv/porter.yaml",
    62  			goldenFile:   "testdata/myenv-depsv2.bundle.json",
    63  			preserveTags: false},
    64  	}
    65  
    66  	for _, tc := range testcases {
    67  		tc := tc
    68  		t.Run(tc.name, func(t *testing.T) {
    69  			t.Parallel()
    70  
    71  			c := config.NewTestConfig(t)
    72  			tc.configHandler(c.Config)
    73  			c.TestContext.AddTestFileFromRoot(tc.manifestPath, config.Name)
    74  
    75  			ctx := context.Background()
    76  			m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
    77  			require.NoError(t, err, "could not load manifest")
    78  
    79  			installedMixins := []mixin.Metadata{
    80  				{Name: "exec", VersionInfo: pkgmgmt.VersionInfo{Version: "v1.2.3"}},
    81  			}
    82  
    83  			a := NewManifestConverter(c.Config, m, nil, installedMixins, tc.preserveTags)
    84  
    85  			bun, err := a.ToBundle(ctx)
    86  			require.NoError(t, err, "ToBundle failed")
    87  
    88  			// Compare the regular json, not the canonical, because that's hard to diff
    89  			prepBundleForDiff(&bun.Bundle)
    90  			bunD, err := json.MarshalIndent(bun, "", "  ")
    91  			require.NoError(t, err)
    92  			c.TestContext.CompareGoldenFile(tc.goldenFile, string(bunD))
    93  		})
    94  	}
    95  }
    96  
    97  func prepBundleForDiff(b *bundle.Bundle) {
    98  	// Unset the digest when we are comparing test bundle files because
    99  	// otherwise the digest changes based on the version of the porter binary +
   100  	// mixins that generated it, which makes the file change a lot
   101  	// unnecessarily.
   102  	stamp := b.Custom[cnab.PorterExtension].(Stamp)
   103  	stamp.ManifestDigest = ""
   104  	b.Custom[cnab.PorterExtension] = stamp
   105  }
   106  
   107  func TestManifestConverter_ToBundle(t *testing.T) {
   108  	testcases := []struct {
   109  		name         string
   110  		preserveTags bool
   111  	}{
   112  		{name: "not preserving tags", preserveTags: false},
   113  		{name: "preserving tags", preserveTags: true},
   114  	}
   115  
   116  	for _, tc := range testcases {
   117  		tc := tc
   118  		t.Run(tc.name, func(t *testing.T) {
   119  			t.Parallel()
   120  
   121  			c := config.NewTestConfig(t)
   122  			c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
   123  
   124  			ctx := context.Background()
   125  			m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   126  			require.NoError(t, err, "could not load manifest")
   127  
   128  			a := NewManifestConverter(c.Config, m, nil, nil, tc.preserveTags)
   129  
   130  			bun, err := a.ToBundle(ctx)
   131  			require.NoError(t, err, "ToBundle failed")
   132  
   133  			assert.Equal(t, cnab.BundleSchemaVersion(), bun.SchemaVersion)
   134  			assert.Equal(t, "mybuns", bun.Name)
   135  			assert.Equal(t, "0.1.2", bun.Version)
   136  			assert.Equal(t, "A very thorough test bundle", bun.Description)
   137  
   138  			stamp, err := LoadStamp(bun)
   139  			require.NoError(t, err, "could not load porter's stamp")
   140  			assert.NotNil(t, stamp)
   141  
   142  			assert.Equal(t, tc.preserveTags, stamp.PreserveTags)
   143  
   144  			assert.Contains(t, bun.Actions, "status", "custom action 'status' was not populated")
   145  
   146  			require.Len(t, bun.Credentials, 2, "expected two credentials")
   147  			assert.Contains(t, bun.Parameters, "porter-debug", "porter-debug parameter was not defined")
   148  			assert.Contains(t, bun.Definitions, "porter-debug-parameter", "porter-debug definition was not defined")
   149  
   150  			assert.True(t, bun.HasDependenciesV1(), "DependenciesV1 was not populated")
   151  			assert.Contains(t, bun.RequiredExtensions, "io.cnab.dependencies")
   152  
   153  			assert.NotEmpty(t, bun.Outputs, "expected multiple outputs generated")
   154  		})
   155  	}
   156  }
   157  
   158  func TestManifestConverter_generateBundleCredentials(t *testing.T) {
   159  	t.Parallel()
   160  
   161  	c := config.NewTestConfig(t)
   162  	c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
   163  
   164  	ctx := context.Background()
   165  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   166  	require.NoError(t, err, "could not load manifest")
   167  
   168  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   169  
   170  	bun, err := a.ToBundle(ctx)
   171  	require.NoError(t, err, "ToBundle failed")
   172  
   173  	assert.Contains(t, bun.Credentials, "username", "credential 'username' was not populated")
   174  	username := bun.Credentials["username"]
   175  	assert.Equal(t, "The name you want on the audit log", username.Description, "credential.Description was not populated")
   176  	assert.False(t, username.Required, "credential.Required was not populated correctly")
   177  	assert.Equal(t, "ROOT_USERNAME", username.EnvironmentVariable, "credential.EnvironmentVariable was not populated")
   178  
   179  	assert.Contains(t, bun.Credentials, "password", "credential 'password' was not populated")
   180  	password := bun.Credentials["password"]
   181  	assert.True(t, password.Required, "credential.Required was not populated correctly")
   182  	assert.Equal(t, []string{"boom"}, password.ApplyTo, "credential.ApplyTo was not populated")
   183  	assert.Equal(t, "/tmp/password", password.Path, "credential.Path was not populated")
   184  }
   185  
   186  func TestManifestConverter_generateBundleParametersSchema(t *testing.T) {
   187  	testcases := []struct {
   188  		propname  string
   189  		wantParam bundle.Parameter
   190  		wantDef   definition.Schema
   191  	}{
   192  		{"ainteger",
   193  			bundle.Parameter{
   194  				Definition: "ainteger-parameter",
   195  				Destination: &bundle.Location{
   196  					EnvironmentVariable: "AINTEGER",
   197  				},
   198  			},
   199  			definition.Schema{
   200  				Type:    "integer",
   201  				Default: 1,
   202  				Minimum: toFloat(0),
   203  				Maximum: toFloat(10),
   204  			},
   205  		},
   206  		{"anumber",
   207  			bundle.Parameter{
   208  				Definition: "anumber-parameter",
   209  				Destination: &bundle.Location{
   210  					EnvironmentVariable: "ANUMBER",
   211  				},
   212  			},
   213  			definition.Schema{
   214  				Type:             "number",
   215  				Default:          0.5,
   216  				ExclusiveMinimum: toFloat(0),
   217  				ExclusiveMaximum: toFloat(1),
   218  			},
   219  		},
   220  		{
   221  			"astringenum",
   222  			bundle.Parameter{
   223  				Definition: "astringenum-parameter",
   224  				Destination: &bundle.Location{
   225  					EnvironmentVariable: "ASTRINGENUM",
   226  				},
   227  			},
   228  			definition.Schema{
   229  				Type:    "string",
   230  				Default: "blue",
   231  				Enum:    []interface{}{"blue", "red", "purple", "pink"},
   232  			},
   233  		},
   234  		{
   235  			"astring",
   236  			bundle.Parameter{
   237  				Definition: "astring-parameter",
   238  				Destination: &bundle.Location{
   239  					EnvironmentVariable: "ASTRING",
   240  				},
   241  				Required: true,
   242  			},
   243  			definition.Schema{
   244  				Type:      "string",
   245  				MinLength: toInt(1),
   246  				MaxLength: toInt(10),
   247  			},
   248  		},
   249  		{
   250  			"aboolean",
   251  			bundle.Parameter{
   252  				Definition: "aboolean-parameter",
   253  				Destination: &bundle.Location{
   254  					EnvironmentVariable: "ABOOLEAN",
   255  				},
   256  			},
   257  			definition.Schema{
   258  				Type:    "boolean",
   259  				Default: true,
   260  			},
   261  		},
   262  		{
   263  			"installonly",
   264  			bundle.Parameter{
   265  				Definition: "installonly-parameter",
   266  				Destination: &bundle.Location{
   267  					EnvironmentVariable: "INSTALLONLY",
   268  				},
   269  				ApplyTo: []string{
   270  					"install",
   271  				},
   272  				Required: true,
   273  			},
   274  			definition.Schema{
   275  				Type: "boolean",
   276  			},
   277  		},
   278  		{
   279  			"sensitive",
   280  			bundle.Parameter{
   281  				Definition: "sensitive-parameter",
   282  				Destination: &bundle.Location{
   283  					EnvironmentVariable: "SENSITIVE",
   284  				},
   285  				Required: true,
   286  			},
   287  			definition.Schema{
   288  				Type:      "string",
   289  				WriteOnly: toBool(true),
   290  			},
   291  		},
   292  		{
   293  			"jsonobject",
   294  			bundle.Parameter{
   295  				Definition: "jsonobject-parameter",
   296  				Destination: &bundle.Location{
   297  					EnvironmentVariable: "JSONOBJECT",
   298  				},
   299  			},
   300  			definition.Schema{
   301  				Type:    "string",
   302  				Default: `"myobject": { "foo": "true", "bar": [ 1, 2, 3 ] }`,
   303  			},
   304  		},
   305  		{
   306  			"afile",
   307  			bundle.Parameter{
   308  				Definition: "afile-parameter",
   309  				Destination: &bundle.Location{
   310  					Path: "/home/nonroot/.kube/config",
   311  				},
   312  				Required: true,
   313  			},
   314  			definition.Schema{
   315  				Type:            "string",
   316  				ContentEncoding: "base64",
   317  			},
   318  		},
   319  		{
   320  			"notype-file",
   321  			bundle.Parameter{
   322  				Definition: "notype-file-parameter",
   323  				Destination: &bundle.Location{
   324  					Path: "/cnab/app/config.toml",
   325  				},
   326  				Required: true,
   327  			},
   328  			definition.Schema{
   329  				Type:            "string",
   330  				ContentEncoding: "base64",
   331  			},
   332  		},
   333  		{
   334  			"notype-string",
   335  			bundle.Parameter{
   336  				Definition: "notype-string-parameter",
   337  				Destination: &bundle.Location{
   338  					EnvironmentVariable: "NOTYPE_STRING",
   339  				},
   340  				Required: true,
   341  			},
   342  			definition.Schema{
   343  				Type: "string",
   344  			},
   345  		},
   346  	}
   347  
   348  	for _, tc := range testcases {
   349  		tc := tc
   350  		t.Run(tc.propname, func(t *testing.T) {
   351  			t.Parallel()
   352  			c := config.NewTestConfig(t)
   353  			c.TestContext.AddTestFile("testdata/porter-with-parameters.yaml", config.Name)
   354  
   355  			ctx := context.Background()
   356  			m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   357  			require.NoError(t, err, "could not load manifest")
   358  
   359  			a := NewManifestConverter(c.Config, m, nil, nil, false)
   360  
   361  			defs := make(definition.Definitions, len(m.Parameters))
   362  			params := a.generateBundleParameters(ctx, &defs)
   363  
   364  			param, ok := params[tc.propname]
   365  			require.True(t, ok, "parameter definition was not generated")
   366  
   367  			def, ok := defs[param.Definition]
   368  			require.True(t, ok, "property definition was not generated")
   369  
   370  			assert.Equal(t, tc.wantParam, param)
   371  			assert.Equal(t, tc.wantDef, *def)
   372  		})
   373  	}
   374  }
   375  
   376  func TestManifestConverter_buildDefaultPorterParameters(t *testing.T) {
   377  	t.Parallel()
   378  
   379  	c := config.NewTestConfig(t)
   380  	c.TestContext.AddTestFileFromRoot("pkg/manifest/testdata/simple.porter.yaml", config.Name)
   381  
   382  	ctx := context.Background()
   383  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   384  	require.NoError(t, err, "could not load manifest")
   385  
   386  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   387  
   388  	defs := make(definition.Definitions, len(m.Parameters))
   389  	params := a.generateBundleParameters(ctx, &defs)
   390  
   391  	debugParam, ok := params["porter-debug"]
   392  	assert.True(t, ok, "porter-debug parameter was not defined")
   393  	assert.Equal(t, "porter-debug-parameter", debugParam.Definition)
   394  	assert.Equal(t, "PORTER_DEBUG", debugParam.Destination.EnvironmentVariable)
   395  
   396  	debugDef, ok := defs["porter-debug-parameter"]
   397  	require.True(t, ok, "porter-debug definition was not defined")
   398  	assert.Equal(t, "boolean", debugDef.Type)
   399  	assert.Equal(t, false, debugDef.Default)
   400  }
   401  
   402  func TestManifestConverter_generateImages(t *testing.T) {
   403  	t.Parallel()
   404  
   405  	c := config.NewTestConfig(t)
   406  	c.TestContext.AddTestFileFromRoot("pkg/manifest/testdata/simple.porter.yaml", config.Name)
   407  
   408  	ctx := context.Background()
   409  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   410  	require.NoError(t, err, "could not load manifest")
   411  
   412  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   413  
   414  	mappedImage := manifest.MappedImage{
   415  		Description: "un petite server",
   416  		Repository:  "getporter/myserver",
   417  		ImageType:   "docker",
   418  		Digest:      "abc123",
   419  		Size:        12,
   420  		MediaType:   "download",
   421  		Labels: map[string]string{
   422  			"OS":           "linux",
   423  			"Architecture": "amd64",
   424  		},
   425  	}
   426  	a.Manifest.ImageMap = map[string]manifest.MappedImage{
   427  		"server": mappedImage,
   428  	}
   429  
   430  	images := a.generateBundleImages()
   431  
   432  	require.Len(t, images, 1)
   433  	img := images["server"]
   434  	assert.Equal(t, mappedImage.Description, img.Description)
   435  	assert.Equal(t, fmt.Sprintf("%s@%s", mappedImage.Repository, mappedImage.Digest), img.Image)
   436  	assert.Equal(t, mappedImage.ImageType, img.ImageType)
   437  	assert.Equal(t, mappedImage.Digest, img.Digest)
   438  	assert.Equal(t, mappedImage.Size, img.Size)
   439  	assert.Equal(t, mappedImage.MediaType, img.MediaType)
   440  	assert.Equal(t, mappedImage.Labels["OS"], img.Labels["OS"])
   441  	assert.Equal(t, mappedImage.Labels["Architecture"], img.Labels["Architecture"])
   442  }
   443  
   444  func TestManifestConverter_generateBundleImages_EmptyLabels(t *testing.T) {
   445  	t.Parallel()
   446  
   447  	c := config.NewTestConfig(t)
   448  	c.TestContext.AddTestFileFromRoot("pkg/manifest/testdata/simple.porter.yaml", config.Name)
   449  
   450  	ctx := context.Background()
   451  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   452  	require.NoError(t, err, "could not load manifest")
   453  
   454  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   455  
   456  	mappedImage := manifest.MappedImage{
   457  		Description: "un petite server",
   458  		Repository:  "getporter/myserver",
   459  		Tag:         "1.0.0",
   460  		ImageType:   "docker",
   461  		Labels:      nil,
   462  	}
   463  	a.Manifest.ImageMap = map[string]manifest.MappedImage{
   464  		"server": mappedImage,
   465  	}
   466  
   467  	images := a.generateBundleImages()
   468  	require.Len(t, images, 1)
   469  	img := images["server"]
   470  	assert.Nil(t, img.Labels)
   471  	assert.Equal(t, fmt.Sprintf("%s:%s", mappedImage.Repository, mappedImage.Tag), img.Image)
   472  }
   473  
   474  func TestManifestConverter_generateBundleOutputs(t *testing.T) {
   475  	t.Parallel()
   476  
   477  	c := config.NewTestConfig(t)
   478  	c.TestContext.AddTestFileFromRoot("pkg/manifest/testdata/simple.porter.yaml", config.Name)
   479  
   480  	ctx := context.Background()
   481  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   482  	require.NoError(t, err, "could not load manifest")
   483  
   484  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   485  
   486  	outputDefinitions := manifest.OutputDefinitions{
   487  		"output1": {
   488  			Name: "output1",
   489  			ApplyTo: []string{
   490  				"install",
   491  				"upgrade",
   492  			},
   493  			Schema: definition.Schema{
   494  				Type:        "string",
   495  				Description: "Description of output1",
   496  			},
   497  			Sensitive: true,
   498  		},
   499  		"output2": {
   500  			Name: "output2",
   501  			Schema: definition.Schema{
   502  				Type:        "boolean",
   503  				Description: "Description of output2",
   504  			},
   505  		},
   506  		"kubeconfig": {
   507  			Name: "kubeconfig",
   508  			Path: "/home/nonroot/.kube/config",
   509  			Schema: definition.Schema{
   510  				Type:        "file",
   511  				Description: "Description of kubeconfig",
   512  			},
   513  		},
   514  		"notype-string": {
   515  			Name: "notype-string",
   516  		},
   517  		"notype-file": {
   518  			Name: "notype-file",
   519  			Path: "/home/nonroot/.kube/config",
   520  		},
   521  	}
   522  
   523  	a.Manifest.Outputs = outputDefinitions
   524  
   525  	defs := make(definition.Definitions, len(a.Manifest.Outputs))
   526  	outputs := a.generateBundleOutputs(ctx, &defs)
   527  	require.Len(t, defs, 6)
   528  
   529  	wantOutputDefinitions := map[string]bundle.Output{
   530  		"output1": {
   531  			Definition:  "output1-output",
   532  			Description: "Description of output1",
   533  			ApplyTo: []string{
   534  				"install",
   535  				"upgrade",
   536  			},
   537  			Path: "/cnab/app/outputs/output1",
   538  		},
   539  		"output2": {
   540  			Definition:  "output2-output",
   541  			Description: "Description of output2",
   542  			Path:        "/cnab/app/outputs/output2",
   543  		},
   544  		"kubeconfig": {
   545  			Definition:  "kubeconfig-output",
   546  			Description: "Description of kubeconfig",
   547  			Path:        "/cnab/app/outputs/kubeconfig",
   548  		},
   549  		"notype-string": {
   550  			Definition: "notype-string-output",
   551  			Path:       "/cnab/app/outputs/notype-string",
   552  		},
   553  		"notype-file": {
   554  			Definition: "notype-file-output",
   555  			Path:       "/cnab/app/outputs/notype-file",
   556  		},
   557  		"porter-state": {
   558  			Description: "Supports persisting state for bundles. Porter internal parameter that should not be set manually.",
   559  			Definition:  "porter-state",
   560  			Path:        "/cnab/app/outputs/porter-state",
   561  		},
   562  	}
   563  
   564  	require.Equal(t, wantOutputDefinitions, outputs)
   565  
   566  	wantDefinitions := definition.Definitions{
   567  		"output1-output": &definition.Schema{
   568  			Type:        "string",
   569  			Description: "Description of output1",
   570  			WriteOnly:   toBool(true),
   571  		},
   572  		"output2-output": &definition.Schema{
   573  			Type:        "boolean",
   574  			Description: "Description of output2",
   575  		},
   576  		"kubeconfig-output": &definition.Schema{
   577  			Type:            "string",
   578  			ContentEncoding: "base64",
   579  			Description:     "Description of kubeconfig",
   580  		},
   581  		"notype-string-output": &definition.Schema{
   582  			Type: "string",
   583  		},
   584  		"notype-file-output": &definition.Schema{
   585  			Type:            "string",
   586  			ContentEncoding: "base64",
   587  		},
   588  		"porter-state": &definition.Schema{
   589  			ID:              "https://porter.sh/generated-bundle/#porter-state",
   590  			Comment:         "porter-internal",
   591  			Description:     "Supports persisting state for bundles. Porter internal parameter that should not be set manually.",
   592  			Type:            "string",
   593  			ContentEncoding: "base64",
   594  		},
   595  	}
   596  
   597  	require.Equal(t, wantDefinitions, defs)
   598  }
   599  
   600  func TestManifestConverter_generateDependenciesv1(t *testing.T) {
   601  	t.Parallel()
   602  
   603  	testcases := []struct {
   604  		name    string
   605  		wantDep depsv1ext.Dependency
   606  	}{
   607  		{"no-version", depsv1ext.Dependency{
   608  			Name:   "mysql",
   609  			Bundle: "getporter/azure-mysql:5.7",
   610  		}},
   611  		{"no-ranges, uses prerelease", depsv1ext.Dependency{
   612  			Name:   "ad",
   613  			Bundle: "getporter/azure-active-directory",
   614  			Version: &depsv1ext.DependencyVersion{
   615  				AllowPrereleases: true,
   616  				Ranges:           []string{"1.0.0-0"},
   617  			},
   618  		}},
   619  		{"with-ranges", depsv1ext.Dependency{
   620  			Name:   "storage",
   621  			Bundle: "getporter/azure-blob-storage",
   622  			Version: &depsv1ext.DependencyVersion{
   623  				Ranges: []string{
   624  					"1.x - 2,2.1 - 3.x",
   625  				},
   626  			},
   627  		}},
   628  	}
   629  
   630  	for _, tc := range testcases {
   631  		tc := tc
   632  
   633  		t.Run(tc.name, func(t *testing.T) {
   634  			t.Parallel()
   635  			c := config.NewTestConfig(t)
   636  			c.TestContext.AddTestFile("testdata/porter-with-deps.yaml", config.Name)
   637  
   638  			ctx := context.Background()
   639  			m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   640  			require.NoError(t, err, "could not load manifest")
   641  
   642  			a := NewManifestConverter(c.Config, m, nil, nil, false)
   643  			defs := make(definition.Definitions, len(m.Parameters))
   644  
   645  			depsExt, depsExtKey, err := a.generateDependencies(ctx, &defs)
   646  			require.NoError(t, err)
   647  			require.Equal(t, cnab.DependenciesV1ExtensionKey, depsExtKey, "expected the v1 dependencies extension key")
   648  			require.IsType(t, &depsv1ext.Dependencies{}, depsExt, "expected a v1 dependencies extension section")
   649  			deps := depsExt.(*depsv1ext.Dependencies)
   650  			require.Len(t, deps.Requires, 3, "incorrect number of dependencies were generated")
   651  			require.Equal(t, []string{"mysql", "ad", "storage"}, deps.Sequence, "incorrect sequence was generated")
   652  
   653  			var dep *depsv1ext.Dependency
   654  			for _, d := range deps.Requires {
   655  				if d.Bundle == tc.wantDep.Bundle {
   656  					dep = &d
   657  					break
   658  				}
   659  			}
   660  
   661  			require.NotNil(t, dep, "could not find bundle %s", tc.wantDep.Bundle)
   662  			assert.Equal(t, &tc.wantDep, dep)
   663  		})
   664  	}
   665  }
   666  
   667  func TestManifestConverter_generateDependenciesv2(t *testing.T) {
   668  	t.Parallel()
   669  
   670  	testcases := []struct {
   671  		name     string
   672  		wantDep  depsv2ext.Dependency
   673  		wantDefs definition.Definitions
   674  	}{
   675  		{name: "all fields", wantDep: depsv2ext.Dependency{
   676  			Name:    "mysql",
   677  			Bundle:  "getporter/azure-mysql:5.7",
   678  			Version: "5.7.x",
   679  			Interface: &depsv2ext.DependencyInterface{
   680  				ID:        "https://porter.sh/interfaces/#mysql",
   681  				Reference: "getporter/mysql-spec:5.7",
   682  				Document: depsv2ext.DependencyInterfaceDocument{
   683  					Outputs: map[string]bundle.Output{
   684  						"myoutput": {
   685  							Definition:  "myoutput-output",
   686  							Description: "worlds smallest output",
   687  							Path:        "/cnab/app/outputs/myoutput",
   688  						},
   689  					},
   690  					Parameters: map[string]bundle.Parameter{
   691  						"myparam": {
   692  							Definition:  "myparam-parameter",
   693  							Description: "worlds biggest param",
   694  							Required:    false,
   695  							Destination: &bundle.Location{
   696  								Path:                "",
   697  								EnvironmentVariable: "MYPARAM",
   698  							},
   699  						},
   700  					},
   701  					Credentials: map[string]bundle.Credential{
   702  						"mycred": {
   703  							Description: "credential",
   704  							Required:    true,
   705  						},
   706  					},
   707  				},
   708  			},
   709  			Sharing: depsv2ext.SharingCriteria{
   710  				Mode:  true,
   711  				Group: depsv2ext.SharingGroup{Name: "myapp"},
   712  			},
   713  			Parameters: map[string]string{
   714  				"database":  "wordpress",
   715  				"collation": "${bundle.parameters.db_collation}",
   716  			},
   717  			Credentials: map[string]string{
   718  				"user": "${bundle.credentials.username}",
   719  			},
   720  		},
   721  			wantDefs: map[string]*definition.Schema{
   722  				"myoutput-output": {
   723  					Type:        "string",
   724  					Description: "worlds smallest output",
   725  				},
   726  				"myparam-parameter": {
   727  					Type:        "string",
   728  					Default:     false,
   729  					Description: "worlds biggest param",
   730  				},
   731  			},
   732  		},
   733  	}
   734  
   735  	for _, tc := range testcases {
   736  		tc := tc
   737  
   738  		t.Run(tc.name, func(t *testing.T) {
   739  			t.Parallel()
   740  			c := config.NewTestConfig(t)
   741  			c.SetExperimentalFlags(experimental.FlagDependenciesV2)
   742  			c.TestContext.AddTestFile("testdata/porter-with-depsv2.yaml", config.Name)
   743  
   744  			ctx := context.Background()
   745  			m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   746  			require.NoError(t, err, "could not load manifest")
   747  
   748  			a := NewManifestConverter(c.Config, m, nil, nil, false)
   749  			defs := make(definition.Definitions, len(m.Parameters))
   750  
   751  			depsExt, depsExtKey, err := a.generateDependencies(ctx, &defs)
   752  			require.NoError(t, err)
   753  			require.Equal(t, cnab.DependenciesV2ExtensionKey, depsExtKey, "expected the v1 dependencies extension key")
   754  			require.IsType(t, &depsv2ext.Dependencies{}, depsExt, "expected a v1 dependencies extension section")
   755  			deps := depsExt.(*depsv2ext.Dependencies)
   756  			require.Len(t, deps.Requires, 3, "incorrect number of dependencies were generated")
   757  
   758  			var dep *depsv2ext.Dependency
   759  			for _, d := range deps.Requires {
   760  				if d.Bundle == tc.wantDep.Bundle {
   761  					dep = &d
   762  					break
   763  				}
   764  			}
   765  
   766  			require.NotNil(t, dep, "could not find bundle %s", tc.wantDep.Bundle)
   767  			assert.Equal(t, &tc.wantDep, dep)
   768  			assert.Equal(t, tc.wantDefs, defs)
   769  		})
   770  	}
   771  }
   772  
   773  func TestManifestConverter_generateParameterSources(t *testing.T) {
   774  	t.Parallel()
   775  
   776  	c := config.NewTestConfig(t)
   777  	c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
   778  
   779  	ctx := context.Background()
   780  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   781  	require.NoError(t, err, "could not load manifest")
   782  
   783  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   784  
   785  	b, err := a.ToBundle(ctx)
   786  	require.NoError(t, err, "ToBundle failed")
   787  	sources, err := b.ReadParameterSources()
   788  	require.NoError(t, err, "ReadParameterSources failed")
   789  
   790  	want := cnab.ParameterSources{}
   791  	want.SetParameterFromOutput("porter-msg-output", "msg")
   792  	want.SetParameterFromOutput("tfstate", "tfstate")
   793  	want.SetParameterFromOutput("porter-state", "porter-state")
   794  	want.SetParameterFromDependencyOutput("mysql-connstr", "db", "connstr")
   795  
   796  	assert.Equal(t, want, sources)
   797  }
   798  
   799  func TestNewManifestConverter_generateOutputWiringParameter(t *testing.T) {
   800  	t.Parallel()
   801  
   802  	c := config.NewTestConfig(t)
   803  	c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
   804  
   805  	ctx := context.Background()
   806  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   807  	require.NoError(t, err, "could not load manifest")
   808  
   809  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   810  
   811  	outputDef := definition.Schema{
   812  		Type: "string",
   813  	}
   814  	b := cnab.NewBundle(bundle.Bundle{
   815  		Outputs: map[string]bundle.Output{
   816  			"msg": {
   817  				Definition: "stringDef",
   818  			},
   819  			"some-thing": {
   820  				Definition: "stringDef",
   821  			},
   822  		},
   823  		Definitions: map[string]*definition.Schema{
   824  			"stringDef": &outputDef,
   825  		},
   826  	})
   827  
   828  	t.Run("generate parameter", func(t *testing.T) {
   829  		t.Parallel()
   830  
   831  		name, param, paramDef := a.generateOutputWiringParameter(b, "msg")
   832  
   833  		assert.Equal(t, "porter-msg-output", name, "unexpected parameter name")
   834  		assert.False(t, param.Required, "wiring parameters should NOT be required")
   835  		require.NotNil(t, param.Destination, "wiring parameters should have a destination set")
   836  		assert.Equal(t, "PORTER_MSG_OUTPUT", param.Destination.EnvironmentVariable, "unexpected destination environment variable set")
   837  
   838  		assert.Equal(t, "https://porter.sh/generated-bundle/#porter-parameter-source-definition", paramDef.ID, "wiring parameter should have a schema id set")
   839  		assert.NotSame(t, &outputDef, &paramDef, "wiring parameter definition should be a copy")
   840  		assert.Equal(t, outputDef.Type, paramDef.Type, "output def and param def should have the same type")
   841  		assert.Equal(t, cnab.PorterInternal, paramDef.Comment, "wiring parameter should be flagged as internal")
   842  	})
   843  
   844  	t.Run("param with hyphen", func(t *testing.T) {
   845  		t.Parallel()
   846  
   847  		name, param, _ := a.generateOutputWiringParameter(b, "some-thing")
   848  
   849  		assert.Equal(t, "porter-some-thing-output", name, "unexpected parameter name")
   850  		require.NotNil(t, param.Destination, "wiring parameters should have a destination set")
   851  		assert.Equal(t, "PORTER_SOME_THING_OUTPUT", param.Destination.EnvironmentVariable, "unexpected destination environment variable set")
   852  	})
   853  }
   854  
   855  func TestNewManifestConverter_generateDependencyOutputWiringParameter(t *testing.T) {
   856  	t.Parallel()
   857  
   858  	c := config.NewTestConfig(t)
   859  	c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
   860  
   861  	ctx := context.Background()
   862  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   863  	require.NoError(t, err, "could not load manifest")
   864  
   865  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   866  
   867  	ref := manifest.DependencyOutputReference{Dependency: "mysql", Output: "mysql-password"}
   868  	name, param, paramDef := a.generateDependencyOutputWiringParameter(ref)
   869  
   870  	assert.Equal(t, "porter-mysql-mysql-password-dep-output", name, "unexpected parameter name")
   871  	assert.False(t, param.Required, "wiring parameters should NOT be required")
   872  	require.NotNil(t, param.Destination, "wiring parameters should have a destination set")
   873  	assert.Equal(t, "PORTER_MYSQL_MYSQL_PASSWORD_DEP_OUTPUT", param.Destination.EnvironmentVariable, "unexpected destination environment variable set")
   874  
   875  	assert.Equal(t, "https://porter.sh/generated-bundle/#porter-parameter-source-definition", paramDef.ID, "wiring parameter should have a schema id set")
   876  	assert.Equal(t, cnab.PorterInternal, paramDef.Comment, "wiring parameter should be flagged as internal")
   877  	assert.Empty(t, paramDef.Type, "dependency output types are of unknown types and should not be defined")
   878  }
   879  
   880  func TestManifestConverter_generateRequiredExtensions_ParameterSources(t *testing.T) {
   881  	t.Parallel()
   882  
   883  	c := config.NewTestConfig(t)
   884  	c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
   885  
   886  	ctx := context.Background()
   887  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   888  	require.NoError(t, err, "could not load manifest")
   889  
   890  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   891  
   892  	bun, err := a.ToBundle(ctx)
   893  	require.NoError(t, err, "ToBundle failed")
   894  	assert.Contains(t, bun.RequiredExtensions, "io.cnab.parameter-sources")
   895  }
   896  
   897  func TestManifestConverter_generateRequiredExtensions(t *testing.T) {
   898  	t.Parallel()
   899  
   900  	c := config.NewTestConfig(t)
   901  	c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
   902  
   903  	ctx := context.Background()
   904  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   905  	require.NoError(t, err, "could not load manifest")
   906  
   907  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   908  
   909  	bun, err := a.ToBundle(ctx)
   910  	require.NoError(t, err, "ToBundle failed")
   911  
   912  	expected := []string{"sh.porter.file-parameters", "io.cnab.dependencies", "io.cnab.parameter-sources", "io.cnab.docker"}
   913  	assert.Equal(t, expected, bun.RequiredExtensions)
   914  }
   915  
   916  func TestManifestConverter_generateCustomExtensions_withRequired(t *testing.T) {
   917  	t.Parallel()
   918  
   919  	c := config.NewTestConfig(t)
   920  	c.TestContext.AddTestFile("testdata/porter-with-required-extensions.yaml", config.Name)
   921  
   922  	ctx := context.Background()
   923  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   924  	require.NoError(t, err, "could not load manifest")
   925  
   926  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   927  
   928  	bun, err := a.ToBundle(ctx)
   929  	require.NoError(t, err, "ToBundle failed")
   930  	assert.Contains(t, bun.Custom, cnab.FileParameterExtensionKey)
   931  	assert.Contains(t, bun.Custom, "requiredExtension1")
   932  	assert.Contains(t, bun.Custom, "requiredExtension2")
   933  	assert.Equal(t, map[string]interface{}{"config": true}, bun.Custom["requiredExtension2"])
   934  }
   935  
   936  func TestManifestConverter_GenerateCustomActionDefinitions(t *testing.T) {
   937  	t.Parallel()
   938  
   939  	c := config.NewTestConfig(t)
   940  	c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
   941  
   942  	ctx := context.Background()
   943  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
   944  	require.NoError(t, err, "could not load manifest")
   945  
   946  	a := NewManifestConverter(c.Config, m, nil, nil, false)
   947  
   948  	defs := a.generateCustomActionDefinitions()
   949  	require.Len(t, defs, 3, "expected 3 custom action definitions to be generated")
   950  
   951  	require.Contains(t, defs, "status")
   952  	statusDef := defs["status"]
   953  	assert.Equal(t, "Print the installation status", statusDef.Description)
   954  	assert.False(t, statusDef.Stateless, "expected the status custom action to not be stateless")
   955  	assert.False(t, statusDef.Modifies, "expected the status custom action to not modify resources")
   956  
   957  	require.Contains(t, defs, "boom")
   958  	boomDef := defs["boom"]
   959  	assert.False(t, boomDef.Stateless, "expected the dry-run custom action to default to not stateless")
   960  	assert.True(t, boomDef.Modifies, "expected the dry-run custom action to default to modifying resources")
   961  }
   962  
   963  func TestManifestConverter_generateDefaultAction(t *testing.T) {
   964  	t.Parallel()
   965  
   966  	testcases := []struct {
   967  		action     string
   968  		wantAction bundle.Action
   969  	}{
   970  		{"dry-run", bundle.Action{
   971  			Description: "Execute the installation in a dry-run mode, allowing to see what would happen with the given set of parameter values",
   972  			Modifies:    false,
   973  			Stateless:   true,
   974  		}},
   975  		{
   976  			"help", bundle.Action{
   977  				Description: "Print a help message to the standard output",
   978  				Modifies:    false,
   979  				Stateless:   true,
   980  			}},
   981  		{"log", bundle.Action{
   982  			Description: "Print logs of the installed system to the standard output",
   983  			Modifies:    false,
   984  			Stateless:   false,
   985  		}},
   986  		{"status", bundle.Action{
   987  			Description: "Print a human readable status message to the standard output",
   988  			Modifies:    false,
   989  			Stateless:   false,
   990  		}},
   991  		{"zombies", bundle.Action{
   992  			Description: "zombies",
   993  			Modifies:    true,
   994  			Stateless:   false,
   995  		}},
   996  	}
   997  
   998  	for _, tc := range testcases {
   999  		tc := tc
  1000  		t.Run(tc.action, func(t *testing.T) {
  1001  			t.Parallel()
  1002  
  1003  			a := ManifestConverter{}
  1004  			gotAction := a.generateDefaultAction(tc.action)
  1005  			assert.Equal(t, tc.wantAction, gotAction)
  1006  		})
  1007  	}
  1008  }
  1009  
  1010  func TestManifestConverter_generateCustomMetadata(t *testing.T) {
  1011  	t.Parallel()
  1012  
  1013  	c := config.NewTestConfig(t)
  1014  	c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
  1015  
  1016  	ctx := context.Background()
  1017  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
  1018  	require.NoError(t, err, "could not load manifest")
  1019  
  1020  	a := NewManifestConverter(c.Config, m, nil, nil, false)
  1021  
  1022  	bun, err := a.ToBundle(ctx)
  1023  	require.NoError(t, err, "ToBundle failed")
  1024  	assert.Len(t, bun.Custom, 7)
  1025  
  1026  	f, err := os.CreateTemp("", "")
  1027  	require.NoError(t, err, "Failed to create bundle file")
  1028  	defer os.Remove(f.Name())
  1029  
  1030  	_, err = bun.WriteTo(f)
  1031  	require.NoError(t, err, "Failed to write bundle file")
  1032  
  1033  	expectedCustomMetaData := `"app":{"version":"1.2.3"},"foo":{"test1":true,"test2":1,"test3":"value","test4":["one","two","three"],"test5":{"1":"one","two":"two"}}`
  1034  	bundleData, err := os.ReadFile(f.Name())
  1035  	require.NoError(t, err, "Failed to read bundle file")
  1036  
  1037  	assert.Contains(t, string(bundleData), expectedCustomMetaData, "Created bundle should be equal to expected bundle ")
  1038  }
  1039  
  1040  func TestManifestConverter_generatedMaintainers(t *testing.T) {
  1041  	want := []bundle.Maintainer{
  1042  		{Name: "John Doe", Email: "john.doe@example.com", URL: "https://example.com/a"},
  1043  		{Name: "Jane Doe", Email: "", URL: "https://example.com/b"},
  1044  		{Name: "Janine Doe", Email: "janine.doe@example.com", URL: ""},
  1045  		{Name: "", Email: "mike.doe@example.com", URL: "https://example.com/c"},
  1046  	}
  1047  
  1048  	c := config.NewTestConfig(t)
  1049  	c.TestContext.AddTestFileFromRoot("tests/testdata/mybuns/porter.yaml", config.Name)
  1050  
  1051  	ctx := context.Background()
  1052  	m, err := manifest.LoadManifestFrom(ctx, c.Config, config.Name)
  1053  	require.NoError(t, err, "could not load manifest")
  1054  
  1055  	a := NewManifestConverter(c.Config, m, nil, nil, false)
  1056  
  1057  	got := a.generateBundleMaintainers()
  1058  	assert.Len(t, got, len(want), "Created bundle should contain desired maintainers")
  1059  
  1060  	for _, wanted := range want {
  1061  		gm, err := getMaintainerByName(got, wanted.Name)
  1062  		if err != nil {
  1063  			t.Errorf("Created bundle should container maintainer '%s'", wanted.Name)
  1064  		}
  1065  		assert.Equal(t, wanted.Email, gm.Email, "Created bundle should specify email '%s' for maintainer '%s'", wanted.Email, wanted.Name)
  1066  		assert.Equal(t, wanted.URL, gm.URL, "Created bundle should specify url '%s' for maintainer '%s'", wanted.URL, wanted.Name)
  1067  	}
  1068  }
  1069  
  1070  func getMaintainerByName(source []bundle.Maintainer, name string) (bundle.Maintainer, error) {
  1071  	for _, m := range source {
  1072  		if m.Name == name {
  1073  			return m, nil
  1074  		}
  1075  	}
  1076  	return bundle.Maintainer{}, fmt.Errorf("Could not find maintainer with name '%s'", name)
  1077  }