package cmd import ( "fmt" "os" "path/filepath" "testing" "time" "github.com/SAP/jenkins-library/pkg/cloudfoundry" "github.com/SAP/jenkins-library/pkg/command" "github.com/SAP/jenkins-library/pkg/mock" "github.com/SAP/jenkins-library/pkg/piperutils" "github.com/SAP/jenkins-library/pkg/yaml" "github.com/stretchr/testify/assert" ) type manifestMock struct { manifestFileName string apps []map[string]interface{} } func (m manifestMock) GetAppName(index int) (string, error) { val, err := m.GetApplicationProperty(index, "name") if err != nil { return "", err } if v, ok := val.(string); ok { return v, nil } return "", fmt.Errorf("Cannot resolve application name") } func (m manifestMock) ApplicationHasProperty(index int, name string) (bool, error) { _, exists := m.apps[index][name] return exists, nil } func (m manifestMock) GetApplicationProperty(index int, name string) (interface{}, error) { return m.apps[index][name], nil } func (m manifestMock) GetFileName() string { return m.manifestFileName } func (m manifestMock) Transform() error { return nil } func (m manifestMock) IsModified() bool { return false } func (m manifestMock) GetApplications() ([]map[string]interface{}, error) { return m.apps, nil } func (m manifestMock) WriteManifest() error { return nil } func TestCfDeployment(t *testing.T) { defer func() { fileUtils = &piperutils.Files{} _replaceVariables = yaml.Substitute }() filesMock := mock.FilesMock{} filesMock.AddDir("/home/me") err := filesMock.Chdir("/home/me") assert.NoError(t, err) fileUtils = &filesMock // everything below in the config map annotated with '//default' is a default in the metadata // since we don't get injected these values during the tests we set it here. defaultConfig := cloudFoundryDeployOptions{ Org: "myOrg", Space: "mySpace", Username: "me", Password: "******", APIEndpoint: "https://examples.sap.com/cf", SmokeTestStatusCode: 200, // default Manifest: "manifest.yml", //default MtaDeployParameters: "-f", // default DeployType: "standard", // default } config := defaultConfig successfulLogin := cloudfoundry.LoginOptions{ CfAPIEndpoint: "https://examples.sap.com/cf", CfOrg: "myOrg", CfSpace: "mySpace", Username: "me", Password: "******", CfLoginOpts: []string{}, } var loginOpts cloudfoundry.LoginOptions var logoutCalled bool noopCfAPICalls := func(t *testing.T, s mock.ExecMockRunner) { assert.Empty(t, s.Calls) // --> in case of an invalid deploy tool there must be no cf api calls assert.Empty(t, loginOpts) // no login options: login has not been called assert.False(t, logoutCalled) } prepareDefaultManifestMocking := func(manifestName string, appNames []string) func() { filesMock.AddFile(manifestName, []byte("file content does not matter")) apps := []map[string]interface{}{} for _, appName := range appNames { apps = append(apps, map[string]interface{}{"name": appName}) } _getManifest = func(name string) (cloudfoundry.Manifest, error) { return manifestMock{ manifestFileName: manifestName, apps: apps, }, nil } return func() { _ = filesMock.FileRemove(manifestName) // slightly mis-use since that is intended to be used by code under test, not test code _getManifest = getManifest } } withLoginAndLogout := func(t *testing.T, asserts func(t *testing.T)) { assert.Equal(t, successfulLogin, loginOpts) asserts(t) assert.True(t, logoutCalled) } cleanup := func() { loginOpts = cloudfoundry.LoginOptions{} logoutCalled = false config = defaultConfig } defer func() { _cfLogin = cfLogin _cfLogout = cfLogout }() _cfLogin = func(c command.ExecRunner, opts cloudfoundry.LoginOptions) error { loginOpts = opts return nil } _cfLogout = func(c command.ExecRunner) error { logoutCalled = true return nil } _replaceVariables = func(manifest string, replacements map[string]interface{}, replacementsFiles []string) (bool, error) { return false, nil } t.Run("Test invalid appname", func(t *testing.T) { defer cleanup() config.AppName = "a_z" s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) assert.EqualError(t, err, "Your application name 'a_z' contains a '_' (underscore) which is not allowed, only letters, dashes and numbers can be used. Please change the name to fit this requirement(s). For more details please visit https://docs.cloudfoundry.org/devguide/deploy-apps/deploy-app.html#basic-settings.") }) t.Run("Manifest substitution", func(t *testing.T) { defer func() { cleanup() _replaceVariables = func(manifest string, replacements map[string]interface{}, replacementsFiles []string) (bool, error) { return false, nil } }() s := mock.ExecMockRunner{} var manifestForSubstitution string var replacements map[string]interface{} var replacementFiles []string defer prepareDefaultManifestMocking("substitute-manifest.yml", []string{"testAppName"})() config.DeployTool = "cf_native" config.DeployType = "blue-green" config.AppName = "myApp" config.Manifest = "substitute-manifest.yml" _replaceVariables = func(manifest string, _replacements map[string]interface{}, _replacementsFiles []string) (bool, error) { manifestForSubstitution = manifest replacements = _replacements replacementFiles = _replacementsFiles return false, nil } t.Run("straight forward", func(t *testing.T) { defer func() { config.ManifestVariables = []string{} config.ManifestVariablesFiles = []string{} }() config.ManifestVariables = []string{"k1=v1"} config.ManifestVariablesFiles = []string{"myVars.yml"} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { assert.Equal(t, "substitute-manifest.yml", manifestForSubstitution) assert.Equal(t, map[string]interface{}{"k1": "v1"}, replacements) assert.Equal(t, []string{"myVars.yml"}, replacementFiles) } }) t.Run("empty", func(t *testing.T) { defer func() { config.ManifestVariables = []string{} config.ManifestVariablesFiles = []string{} }() config.ManifestVariables = []string{} config.ManifestVariablesFiles = []string{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { assert.Equal(t, "substitute-manifest.yml", manifestForSubstitution) assert.Equal(t, map[string]interface{}{}, replacements) assert.Equal(t, []string{}, replacementFiles) } }) }) t.Run("Invalid deploytool", func(t *testing.T) { defer cleanup() s := mock.ExecMockRunner{} config.DeployTool = "invalid" err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { noopCfAPICalls(t, s) } }) t.Run("deploytool cf native", func(t *testing.T) { defer cleanup() defer prepareDefaultManifestMocking("manifest.yml", []string{"testAppName"})() config.DeployTool = "cf_native" config.CfHome = "/home/me1" config.CfPluginHome = "/home/me2" s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check cf api calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{"push", "-f", "manifest.yml"}}, }, s.Calls) }) }) t.Run("check environment variables", func(t *testing.T) { assert.Contains(t, s.Env, "CF_HOME=/home/me1") assert.Contains(t, s.Env, "CF_PLUGIN_HOME=/home/me2") assert.Contains(t, s.Env, "STATUS_CODE=200") }) } }) t.Run("influx reporting", func(t *testing.T) { defer cleanup() s := mock.ExecMockRunner{} defer func() { _now = time.Now }() _now = func() time.Time { // There was the big eclipse in Karlsruhe return time.Date(1999, time.August, 11, 12, 32, 0, 0, time.UTC) } defer prepareDefaultManifestMocking("manifest.yml", []string{"testAppName"})() config.DeployTool = "cf_native" config.ArtifactVersion = "0.1.2" config.CommitHash = "123456" influxData := cloudFoundryDeployInflux{} err := runCloudFoundryDeploy(&config, nil, &influxData, &s) if assert.NoError(t, err) { expected := cloudFoundryDeployInflux{} expected.deployment_data.fields.artifactURL = "n/a" expected.deployment_data.fields.deployTime = "AUG 11 1999 12:32:00" expected.deployment_data.fields.jobTrigger = "n/a" expected.deployment_data.fields.commitHash = "123456" expected.deployment_data.tags.artifactVersion = "0.1.2" expected.deployment_data.tags.deployUser = "me" expected.deployment_data.tags.deployResult = "SUCCESS" expected.deployment_data.tags.cfAPIEndpoint = "https://examples.sap.com/cf" expected.deployment_data.tags.cfOrg = "myOrg" expected.deployment_data.tags.cfSpace = "mySpace" assert.Equal(t, expected, influxData) } }) t.Run("deploy cf native with docker image and docker username", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.DeployDockerImage = "repo/image:tag" config.DockerUsername = "me" config.AppName = "testAppName" config.Manifest = "" s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{"push", "testAppName", "--docker-image", "repo/image:tag", "--docker-username", "me", }}, }, s.Calls) }) } }) t.Run("deploy_cf_native with manifest and docker credentials", func(t *testing.T) { defer cleanup() // Docker image can be done via manifest.yml. // if a private Docker registry is used, --docker-username and DOCKER_PASSWORD // must be set; this is checked by this test config.DeployTool = "cf_native" config.DeployDockerImage = "repo/image:tag" config.DockerUsername = "test_cf_docker" config.DockerPassword = "********" config.AppName = "testAppName" config.Manifest = "" s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{"push", "testAppName", "--docker-image", "repo/image:tag", "--docker-username", "test_cf_docker", }}, }, s.Calls) }) }) t.Run("check environment variables", func(t *testing.T) { //REVISIT: in the corresponding groovy test we checked for "${'********'}" // I don't understand why, but we should discuss ... assert.Contains(t, s.Env, "CF_DOCKER_PASSWORD=********") }) } }) t.Run("deploy cf native blue green with manifest and docker credentials", func(t *testing.T) { defer cleanup() // Blue Green Deploy cf cli plugin does not support --docker-username and --docker-image parameters // docker username and docker image have to be set in the manifest file // if a private docker repository is used the CF_DOCKER_PASSWORD env variable must be set config.DeployTool = "cf_native" config.DeployType = "blue-green" config.DockerUsername = "test_cf_docker" config.DockerPassword = "********" config.AppName = "testAppName" defer prepareDefaultManifestMocking("manifest.yml", []string{"testAppName"})() s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{ "blue-green-deploy", "testAppName", "--delete-old-apps", "-f", "manifest.yml", }}, }, s.Calls) }) }) t.Run("check environment variables", func(t *testing.T) { //REVISIT: in the corresponding groovy test we checked for "${'********'}" // I don't understand why, but we should discuss ... assert.Contains(t, s.Env, "CF_DOCKER_PASSWORD=********") }) } }) t.Run("deploy cf native app name from manifest", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.Manifest = "test-manifest.yml" // app name is not asserted since it does not appear in the cf calls // but it is checked that an app name is present, hence we need it here. defer prepareDefaultManifestMocking("test-manifest.yml", []string{"dummyApp"})() s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{ "push", "-f", "test-manifest.yml", }}, }, s.Calls) }) }) } }) t.Run("get app name from default manifest with cf native deployment", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.Manifest = "" config.AppName = "" //app name does not need to be set if it can be found in the manifest.yml //manifest name does not need to be set- the default manifest.yml will be used if not set defer prepareDefaultManifestMocking("manifest.yml", []string{"newAppName"})() s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{ "push", }}, }, s.Calls) }) }) } }) t.Run("deploy cf native without app name", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.Manifest = "test-manifest.yml" // Here we don't provide an application name from the mock. To make that // more explicit we provide the empty string default explicitly. defer prepareDefaultManifestMocking("test-manifest.yml", []string{""})() s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.EqualError(t, err, "appName from manifest 'test-manifest.yml' is empty") { t.Run("check shell calls", func(t *testing.T) { noopCfAPICalls(t, s) }) } }) // tests from groovy checking for keep old instances are already contained above. Search for '--delete-old-apps' t.Run("deploy cf native blue green keep old instance", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.DeployType = "blue-green" config.Manifest = "test-manifest.yml" config.AppName = "myTestApp" config.KeepOldInstance = true s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{ "blue-green-deploy", "myTestApp", "-f", "test-manifest.yml", }}, {Exec: "cf", Params: []string{ "stop", "myTestApp-old", // MIGRATE FFROM GROOVY: in contrast to groovy there is not redirect of everything &> to a file since we // read the stream directly now. }}, }, s.Calls) }) }) } }) t.Run("cf deploy blue green multiple applications", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.DeployType = "blue-green" config.Manifest = "test-manifest.yml" config.AppName = "myTestApp" defer prepareDefaultManifestMocking("test-manifest.yml", []string{"app1", "app2"})() s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.EqualError(t, err, "Your manifest contains more than one application. For blue green deployments your manifest file may contain only one application") { t.Run("check shell calls", func(t *testing.T) { noopCfAPICalls(t, s) }) } }) t.Run("cf native deploy blue green with no route", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.DeployType = "blue-green" config.Manifest = "test-manifest.yml" config.AppName = "myTestApp" defer func() { _ = filesMock.FileRemove("test-manifest.yml") _getManifest = getManifest }() filesMock.AddFile("test-manifest.yml", []byte("Content does not matter")) _getManifest = func(name string) (cloudfoundry.Manifest, error) { return manifestMock{ manifestFileName: "test-manifest.yml", apps: []map[string]interface{}{ { "name": "app1", "no-route": true, }, }, }, nil } s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{ "push", "myTestApp", "-f", "test-manifest.yml", }}, }, s.Calls) }) }) } }) t.Run("cf native deployment failure", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.DeployType = "blue-green" config.Manifest = "test-manifest.yml" config.AppName = "myTestApp" defer prepareDefaultManifestMocking("test-manifest.yml", []string{"app"})() s := mock.ExecMockRunner{} s.ShouldFailOnCommand = map[string]error{"cf.*deploy.*": fmt.Errorf("cf deploy failed")} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.EqualError(t, err, "cf deploy failed") { t.Run("check shell calls", func(t *testing.T) { // we should try to logout in this case assert.True(t, logoutCalled) }) } }) t.Run("cf native deployment failure when logging in", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.DeployType = "blue-green" config.Manifest = "test-manifest.yml" config.AppName = "myTestApp" defer func() { _cfLogin = func(c command.ExecRunner, opts cloudfoundry.LoginOptions) error { loginOpts = opts return nil } }() _cfLogin = func(c command.ExecRunner, opts cloudfoundry.LoginOptions) error { loginOpts = opts return fmt.Errorf("Unable to login") } defer prepareDefaultManifestMocking("test-manifest.yml", []string{"app1"})() s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.EqualError(t, err, "Unable to login") { t.Run("check shell calls", func(t *testing.T) { // no calls to the cf client in this case assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, }, s.Calls) // no logout assert.False(t, logoutCalled) }) } }) // TODO testCfNativeBlueGreenKeepOldInstanceShouldThrowErrorOnStopError t.Run("cf native deploy standard should not stop instance", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.DeployType = "standard" config.Manifest = "test-manifest.yml" config.AppName = "myTestApp" config.KeepOldInstance = true defer prepareDefaultManifestMocking("test-manifest.yml", []string{"app"})() s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{ "push", "myTestApp", "-f", "test-manifest.yml", }}, // // There is no cf stop // }, s.Calls) }) }) } }) t.Run("testCfNativeWithoutAppNameBlueGreen", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.DeployType = "blue-green" config.Manifest = "test-manifest.yml" defer func() { _ = filesMock.FileRemove("test-manifest.yml") _getManifest = getManifest }() filesMock.AddFile("test-manifest.yml", []byte("The content does not matter")) _getManifest = func(name string) (cloudfoundry.Manifest, error) { return manifestMock{ manifestFileName: "test-manifest.yml", apps: []map[string]interface{}{ { "there-is": "no-app-name", }, }, }, nil } s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.EqualError(t, err, "Blue-green plugin requires app name to be passed (see https://github.com/bluemixgaragelondon/cf-blue-green-deploy/issues/27)") { t.Run("check shell calls", func(t *testing.T) { noopCfAPICalls(t, s) }) } }) // TODO add test for testCfNativeFailureInShellCall t.Run("deploytool mtaDeployPlugin blue green", func(t *testing.T) { defer cleanup() config.DeployTool = "mtaDeployPlugin" config.DeployType = "blue-green" config.MtaPath = "target/test.mtar" defer func() { _ = filesMock.FileRemove("target/test.mtar") }() filesMock.AddFile("target/test.mtar", []byte("content does not matter")) s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{ "bg-deploy", "target/test.mtar", "-f", "--no-confirm", }}, // // There is no cf stop // }, s.Calls) }) }) } }) // TODO: add test for influx reporting (influx reporting is missing at the moment) t.Run("cf push with variables from file and as list", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.Manifest = "test-manifest.yml" config.ManifestVariablesFiles = []string{"vars.yaml"} config.ManifestVariables = []string{"appName=testApplicationFromVarsList"} config.AppName = "testAppName" defer func() { _getManifest = getManifest _getVarsOptions = cloudfoundry.GetVarsOptions _getVarsFileOptions = cloudfoundry.GetVarsFileOptions }() _getVarsOptions = func(vars []string) ([]string, error) { return []string{"--var", "appName=testApplicationFromVarsList"}, nil } _getVarsFileOptions = func(varFiles []string) ([]string, error) { return []string{"--vars-file", "vars.yaml"}, nil } filesMock.AddFile("test-manifest.yml", []byte("content does not matter")) _getManifest = func(name string) (cloudfoundry.Manifest, error) { return manifestMock{ manifestFileName: "test-manifest.yml", apps: []map[string]interface{}{ { "name": "myApp", }, }, }, nil } s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { // Revisit: we don't verify a log message in case of a non existing vars file assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{ "push", "testAppName", "--var", "appName=testApplicationFromVarsList", "--vars-file", "vars.yaml", "-f", "test-manifest.yml", }}, }, s.Calls) }) }) } }) t.Run("cf push with variables from file which does not exist", func(t *testing.T) { defer cleanup() config.DeployTool = "cf_native" config.Manifest = "test-manifest.yml" config.ManifestVariablesFiles = []string{"vars.yaml", "vars-does-not-exist.yaml"} config.AppName = "testAppName" defer func() { _ = filesMock.FileRemove("test-manifest.yml") _ = filesMock.FileRemove("vars.yaml") _getManifest = getManifest _getVarsOptions = cloudfoundry.GetVarsOptions _getVarsFileOptions = cloudfoundry.GetVarsFileOptions }() filesMock.AddFile("test-manifest.yml", []byte("content does not matter")) _getManifest = func(name string) (cloudfoundry.Manifest, error) { return manifestMock{ manifestFileName: "test-manifest.yml", apps: []map[string]interface{}{ { "name": "myApp", }, }, }, nil } s := mock.ExecMockRunner{} var receivedVarOptions []string var receivedVarsFileOptions []string _getVarsOptions = func(vars []string) ([]string, error) { receivedVarOptions = vars return []string{}, nil } _getVarsFileOptions = func(varFiles []string) ([]string, error) { receivedVarsFileOptions = varFiles return []string{"--vars-file", "vars.yaml"}, nil } err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { t.Run("check received vars options", func(t *testing.T) { assert.Empty(t, receivedVarOptions) }) t.Run("check received vars file options", func(t *testing.T) { assert.Equal(t, []string{"vars.yaml", "vars-does-not-exist.yaml"}, receivedVarsFileOptions) }) t.Run("check shell calls", func(t *testing.T) { withLoginAndLogout(t, func(t *testing.T) { // Revisit: we don't verify a log message in case of a non existing vars file assert.Equal(t, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{ "push", "testAppName", "--vars-file", "vars.yaml", "-f", "test-manifest.yml", }}, }, s.Calls) }) }) } }) // TODO: testCfPushDeploymentWithoutVariableSubstitution is already handled above (?) // TODO: testCfBlueGreenDeploymentWithVariableSubstitution variable substitution is not handled at the moment (pr pending). // but anyway we should not test the full cycle here, but only that the variables substitution tool is called in the appropriate way. // variable substitution should be tested at the variables substitution tool itself (yaml util) t.Run("deploytool mtaDeployPlugin", func(t *testing.T) { defer cleanup() config.DeployTool = "mtaDeployPlugin" config.MtaDeployParameters = "-f" t.Run("mta config file from project sources", func(t *testing.T) { defer func() { _ = filesMock.FileRemove("xyz.mtar") }() // The mock is inaccurat here. // AddFile() adds the file absolute, prefix with the current working directory // Glob() returns the absolute path - but without leading slash - , whereas // the real Glob returns the path relative to the current workdir. // In order to mimic the behavior in the free wild we add the mtar at the root dir. filesMock.AddDir("/") assert.NoError(t, filesMock.Chdir("/")) filesMock.AddFile("xyz.mtar", []byte("content does not matter")) // restor the expected working dir. assert.NoError(t, filesMock.Chdir("/home/me")) s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) if assert.NoError(t, err) { withLoginAndLogout(t, func(t *testing.T) { assert.Equal(t, s.Calls, []mock.ExecCall{ {Exec: "cf", Params: []string{"version"}}, {Exec: "cf", Params: []string{"plugins"}}, {Exec: "cf", Params: []string{"deploy", "xyz.mtar", "-f"}}}) }) } }) t.Run("mta config file from project config does not exist", func(t *testing.T) { defer func() { config.MtaPath = "" }() config.MtaPath = "my.mtar" s := mock.ExecMockRunner{} err := runCloudFoundryDeploy(&config, nil, nil, &s) assert.EqualError(t, err, "mtar file 'my.mtar' retrieved from configuration does not exist") }) // TODO: add test for mtar file from project config which does exist in project sources }) } func TestValidateDeployTool(t *testing.T) { testCases := []struct { runName string deployToolGiven string buildTool string deployToolExpected string }{ {"no params", "", "", ""}, {"build tool MTA", "", "mta", "mtaDeployPlugin"}, {"build tool other", "", "other", "cf_native"}, {"deploy and build tool given", "given", "unknown", "given"}, {"only deploy tool given", "given", "", "given"}, } t.Parallel() for _, test := range testCases { t.Run(test.runName, func(t *testing.T) { config := cloudFoundryDeployOptions{BuildTool: test.buildTool, DeployTool: test.deployToolGiven} validateDeployTool(&config) assert.Equal(t, test.deployToolExpected, config.DeployTool, "expected different deployTool result") }) } } func TestMtarLookup(t *testing.T) { defer func() { fileUtils = piperutils.Files{} }() filesMock := mock.FilesMock{} fileUtils = &filesMock t.Run("One MTAR", func(t *testing.T) { defer func() { _ = filesMock.FileRemove("x.mtar") }() filesMock.AddFile("x.mtar", []byte("content does not matter")) path, err := findMtar() if assert.NoError(t, err) { assert.Equal(t, "x.mtar", path) } }) t.Run("No MTAR", func(t *testing.T) { // nothing needs to be configures. There is simply no // mtar in the file system mock, so no mtar will be found. _, err := findMtar() assert.EqualError(t, err, "No mtar file matching pattern '**/*.mtar' found") }) t.Run("Several MTARs", func(t *testing.T) { defer func() { _ = filesMock.FileRemove("x.mtar") _ = filesMock.FileRemove("y.mtar") }() filesMock.AddFile("x.mtar", []byte("content does not matter")) filesMock.AddFile("y.mtar", []byte("content does not matter")) _, err := findMtar() assert.EqualError(t, err, "Found multiple mtar files matching pattern '**/*.mtar' (x.mtar,y.mtar), please specify file via parameter 'mtarPath'") }) } func TestSmokeTestScriptHandling(t *testing.T) { filesMock := mock.FilesMock{} filesMock.AddDir("/home/me") err := filesMock.Chdir("/home/me") assert.NoError(t, err) filesMock.AddFileWithMode("mySmokeTestScript.sh", []byte("Content does not matter"), 0644) fileUtils = &filesMock var canExec os.FileMode = 0755 t.Run("non default existing smoke test file", func(t *testing.T) { parts, err := handleSmokeTestScript("mySmokeTestScript.sh") if assert.NoError(t, err) { // when the none-default file name is provided the file must already exist // in the project sources. assert.False(t, filesMock.HasWrittenFile("mySmokeTestScript.sh")) info, e := filesMock.Stat("mySmokeTestScript.sh") if assert.NoError(t, e) { assert.Equal(t, canExec, info.Mode()) } assert.Equal(t, []string{ "--smoke-test", filepath.FromSlash("/home/me/mySmokeTestScript.sh"), }, parts) } }) t.Run("non default not existing smoke test file", func(t *testing.T) { parts, err := handleSmokeTestScript("notExistingSmokeTestScript.sh") if assert.EqualError(t, err, "failed to make smoke-test script executable: chmod: notExistingSmokeTestScript.sh: No such file or directory") { assert.False(t, filesMock.HasWrittenFile("notExistingSmokeTestScript.sh")) assert.Equal(t, []string{}, parts) } }) t.Run("default smoke test file", func(t *testing.T) { parts, err := handleSmokeTestScript("blueGreenCheckScript.sh") if assert.NoError(t, err) { info, e := filesMock.Stat("blueGreenCheckScript.sh") if assert.NoError(t, e) { assert.Equal(t, canExec, info.Mode()) } // in this case we provide the file. We overwrite in case there is already such a file ... assert.True(t, filesMock.HasWrittenFile("blueGreenCheckScript.sh")) content, e := filesMock.FileRead("blueGreenCheckScript.sh") if assert.NoError(t, e) { assert.Equal(t, "#!/usr/bin/env bash\n# this is simply testing if the application root returns HTTP STATUS_CODE\ncurl -so /dev/null -w '%{response_code}' https://$1 | grep $STATUS_CODE", string(content)) } assert.Equal(t, []string{ "--smoke-test", filepath.FromSlash("/home/me/blueGreenCheckScript.sh"), }, parts) } }) } func TestDefaultManifestVariableFilesHandling(t *testing.T) { filesMock := mock.FilesMock{} filesMock.AddDir("/home/me") err := filesMock.Chdir("/home/me") assert.NoError(t, err) fileUtils = &filesMock t.Run("default manifest variable file is the only one and exists", func(t *testing.T) { defer func() { _ = filesMock.FileRemove("manifest-variables.yml") }() filesMock.AddFile("manifest-variables.yml", []byte("Content does not matter")) manifestFiles, err := validateManifestVariablesFiles( []string{ "manifest-variables.yml", }, ) if assert.NoError(t, err) { assert.Equal(t, []string{ "manifest-variables.yml", }, manifestFiles) } }) t.Run("default manifest variable file is the only one and does not exist", func(t *testing.T) { manifestFiles, err := validateManifestVariablesFiles( []string{ "manifest-variables.yml", }, ) if assert.NoError(t, err) { assert.Equal(t, []string{}, manifestFiles) } }) t.Run("default manifest variable file among others remains if it does not exist", func(t *testing.T) { // in this case we might fail later. manifestFiles, err := validateManifestVariablesFiles( []string{ "manifest-variables.yml", "a-second-file.yml", }, ) if assert.NoError(t, err) { // the order in which the files are returned is significant. assert.Equal(t, []string{ "manifest-variables.yml", "a-second-file.yml", }, manifestFiles) } }) } func TestExtensionDescriptorsWithMinusE(t *testing.T) { t.Run("ExtensionDescriptorsWithMinusE", func(t *testing.T) { extDesc, _ := handleMtaExtensionDescriptors("-e 1.yaml -e 2.yaml") assert.Equal(t, []string{ "-e", "1.yaml,2.yaml", }, extDesc) }) t.Run("ExtensionDescriptorsFirstOneWithoutMinusE", func(t *testing.T) { extDesc, _ := handleMtaExtensionDescriptors("1.yaml -e 2.yaml") assert.Equal(t, []string{ "-e", "1.yaml,2.yaml", }, extDesc) }) t.Run("NoExtensionDescriptors", func(t *testing.T) { extDesc, _ := handleMtaExtensionDescriptors("") assert.Equal(t, []string{}, extDesc) }) } func TestAppNameChecks(t *testing.T) { t.Run("appName with alpha-numeric chars should work", func(t *testing.T) { err := validateAppName("myValidAppName123") assert.NoError(t, err) }) t.Run("appName with alpha-numeric chars and dash should work", func(t *testing.T) { err := validateAppName("my-Valid-AppName123") assert.NoError(t, err) }) t.Run("empty appName should work", func(t *testing.T) { // we consider the empty string as valid appname since we only check app names handed over from outside // in case there is no (real) app name provided from outside we might still find an appname in the metadata // That app name in turn is not checked. err := validateAppName("") assert.NoError(t, err) }) t.Run("single char appName should work", func(t *testing.T) { err := validateAppName("a") assert.NoError(t, err) }) t.Run("appName with alpha-numeric chars and trailing dash should throw an error", func(t *testing.T) { err := validateAppName("my-Invalid-AppName123-") assert.EqualError(t, err, "Your application name 'my-Invalid-AppName123-' starts or ends with a '-' (dash) which is not allowed, only letters and numbers can be used. Please change the name to fit this requirement(s). For more details please visit https://docs.cloudfoundry.org/devguide/deploy-apps/deploy-app.html#basic-settings.") }) t.Run("appName with underscores should throw an error", func(t *testing.T) { err := validateAppName("my_invalid_app_name") assert.EqualError(t, err, "Your application name 'my_invalid_app_name' contains a '_' (underscore) which is not allowed, only letters, dashes and numbers can be used. Please change the name to fit this requirement(s). For more details please visit https://docs.cloudfoundry.org/devguide/deploy-apps/deploy-app.html#basic-settings.") }) } func TestMtaExtensionCredentials(t *testing.T) { filesMock := mock.FilesMock{} filesMock.AddDir("/home/me") err := filesMock.Chdir("/home/me") assert.NoError(t, err) fileUtils = &filesMock _environ = func() []string { return []string{ "MY_CRED_ENV_VAR1=**$0****", "MY_CRED_ENV_VAR2=++$1++++", } } defer func() { fileUtils = piperutils.Files{} _environ = os.Environ }() t.Run("extension file does not exist", func(t *testing.T) { _, _, err := handleMtaExtensionCredentials("mtaextDoesNotExist.mtaext", map[string]interface{}{}) assert.EqualError(t, err, "Cannot handle credentials for mta extension file 'mtaextDoesNotExist.mtaext': could not read 'mtaextDoesNotExist.mtaext'") }) t.Run("credential cannot be retrieved", func(t *testing.T) { filesMock.AddFile("mtaext.mtaext", []byte( `'_schema-version: '3.1' ID: test.ext extends: test parameters test-credentials1: "<%= testCred1 %>" test-credentials2: "<%=testCred2%>"`)) _, _, err := handleMtaExtensionCredentials( "mtaext.mtaext", map[string]interface{}{ "testCred1": "myCredEnvVar1NotDefined", "testCred2": "myCredEnvVar2NotDefined", }, ) assert.EqualError(t, err, "cannot handle mta extension credentials: No credentials found for '[myCredEnvVar1NotDefined myCredEnvVar2NotDefined]'/'[MY_CRED_ENV_VAR1_NOT_DEFINED MY_CRED_ENV_VAR2_NOT_DEFINED]'. Are these credentials maintained?") }) t.Run("irrelevant credentials do not cause failures", func(t *testing.T) { filesMock.AddFile("mtaext.mtaext", []byte( `'_schema-version: '3.1' ID: test.ext extends: test parameters test-credentials1: "<%= testCred1 %>" test-credentials2: "<%=testCred2%>`)) _, _, err := handleMtaExtensionCredentials( "mtaext.mtaext", map[string]interface{}{ "testCred1": "myCredEnvVar1", "testCred2": "myCredEnvVar2", "testCredNotUsed": "myCredEnvVarWhichDoesNotExist", //<-- This here is not used. }, ) assert.NoError(t, err) }) t.Run("invalid chars in credential key name", func(t *testing.T) { filesMock.AddFile("mtaext.mtaext", []byte( `'_schema-version: '3.1' ID: test.ext extends: test parameters test-credentials1: "<%= testCred1 %>" test-credentials2: "<%=testCred2%>`)) _, _, err := handleMtaExtensionCredentials("mtaext.mtaext", map[string]interface{}{ "test.*Cred1": "myCredEnvVar1", }, ) assert.EqualError(t, err, "credential key name 'test.*Cred1' contains unsupported character. Must contain only ^[-_A-Za-z0-9]+$") }) t.Run("unresolved placeholders does not cause an error", func(t *testing.T) { // we emit a log message, but it does not fail filesMock.AddFile("mtaext-unresolved.mtaext", []byte("<%= unresolved %>")) updated, containsUnresolved, err := handleMtaExtensionCredentials("mtaext-unresolved.mtaext", map[string]interface{}{}) assert.True(t, containsUnresolved) assert.False(t, updated) assert.NoError(t, err) }) t.Run("replace straight forward", func(t *testing.T) { mtaFileName := "mtaext.mtaext" filesMock.AddFile(mtaFileName, []byte( `'_schema-version: '3.1' ID: test.ext extends: test parameters test-credentials1: "<%= testCred1 %>" test-credentials2: "<%=testCred2%>" test-credentials3: "<%= testCred2%>" test-credentials4: "<%=testCred2 %>" test-credentials5: "<%= testCred2 %>"`)) updated, containsUnresolved, err := handleMtaExtensionCredentials( mtaFileName, map[string]interface{}{ "testCred1": "myCredEnvVar1", "testCred2": "myCredEnvVar2", }, ) if assert.NoError(t, err) { b, e := fileUtils.FileRead(mtaFileName) if e != nil { assert.Fail(t, "Cannot read mta extension file: %v", e) } content := string(b) assert.Contains(t, content, "test-credentials1: \"**$0****\"") assert.Contains(t, content, "test-credentials2: \"++$1++++\"") assert.Contains(t, content, "test-credentials3: \"++$1++++\"") assert.Contains(t, content, "test-credentials4: \"++$1++++\"") assert.Contains(t, content, "test-credentials5: \"++$1++++\"") assert.True(t, updated) assert.False(t, containsUnresolved) } }) } func TestEnvVarKeyModification(t *testing.T) { envVarCompatibleKey := toEnvVarKey("Mta.EXtensionCredential~Credential_Id1Abc") assert.Equal(t, "MTA_EXTENSION_CREDENTIAL_CREDENTIAL_ID1_ABC", envVarCompatibleKey) }