github.com/jaylevin/jenkins-library@v1.230.4/cmd/cloudFoundryDeploy.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/SAP/jenkins-library/pkg/cloudfoundry"
    17  	"github.com/SAP/jenkins-library/pkg/command"
    18  	"github.com/SAP/jenkins-library/pkg/log"
    19  	"github.com/SAP/jenkins-library/pkg/piperutils"
    20  	"github.com/SAP/jenkins-library/pkg/telemetry"
    21  	"github.com/SAP/jenkins-library/pkg/yaml"
    22  	"github.com/elliotchance/orderedmap"
    23  	"github.com/pkg/errors"
    24  )
    25  
    26  type cfFileUtil interface {
    27  	FileExists(string) (bool, error)
    28  	FileRename(string, string) error
    29  	FileRead(string) ([]byte, error)
    30  	FileWrite(path string, content []byte, perm os.FileMode) error
    31  	Getwd() (string, error)
    32  	Glob(string) ([]string, error)
    33  	Chmod(string, os.FileMode) error
    34  	Copy(string, string) (int64, error)
    35  	Stat(path string) (os.FileInfo, error)
    36  }
    37  
    38  var _now = time.Now
    39  var _cfLogin = cfLogin
    40  var _cfLogout = cfLogout
    41  var _getManifest = getManifest
    42  var _replaceVariables = yaml.Substitute
    43  var _getVarsOptions = cloudfoundry.GetVarsOptions
    44  var _getVarsFileOptions = cloudfoundry.GetVarsFileOptions
    45  var _environ = os.Environ
    46  var fileUtils cfFileUtil = piperutils.Files{}
    47  
    48  // for simplify mocking. Maybe we find a more elegant way (mock for CFUtils)
    49  func cfLogin(c command.ExecRunner, options cloudfoundry.LoginOptions) error {
    50  	cf := &cloudfoundry.CFUtils{Exec: c}
    51  	return cf.Login(options)
    52  }
    53  
    54  // for simplify mocking. Maybe we find a more elegant way (mock for CFUtils)
    55  func cfLogout(c command.ExecRunner) error {
    56  	cf := &cloudfoundry.CFUtils{Exec: c}
    57  	return cf.Logout()
    58  }
    59  
    60  const defaultSmokeTestScript = `#!/usr/bin/env bash
    61  # this is simply testing if the application root returns HTTP STATUS_CODE
    62  curl -so /dev/null -w '%{response_code}' https://$1 | grep $STATUS_CODE`
    63  
    64  func cloudFoundryDeploy(config cloudFoundryDeployOptions, telemetryData *telemetry.CustomData, influxData *cloudFoundryDeployInflux) {
    65  	// for command execution use Command
    66  	c := command.Command{}
    67  	// reroute command output to logging framework
    68  	c.Stdout(log.Writer())
    69  	c.Stderr(log.Writer())
    70  
    71  	// for http calls import  piperhttp "github.com/SAP/jenkins-library/pkg/http"
    72  	// and use a  &piperhttp.Client{} in a custom system
    73  	// Example: step checkmarxExecuteScan.go
    74  
    75  	// error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end
    76  	err := runCloudFoundryDeploy(&config, telemetryData, influxData, &c)
    77  	if err != nil {
    78  		log.Entry().WithError(err).Fatalf("step execution failed: %s", err)
    79  	}
    80  }
    81  
    82  func runCloudFoundryDeploy(config *cloudFoundryDeployOptions, telemetryData *telemetry.CustomData, influxData *cloudFoundryDeployInflux, command command.ExecRunner) error {
    83  
    84  	log.Entry().Infof("General parameters: deployTool='%s', deployType='%s', cfApiEndpoint='%s', cfOrg='%s', cfSpace='%s'",
    85  		config.DeployTool, config.DeployType, config.APIEndpoint, config.Org, config.Space)
    86  
    87  	err := validateAppName(config.AppName)
    88  
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	validateDeployTool(config)
    94  
    95  	var deployTriggered bool
    96  
    97  	if config.DeployTool == "mtaDeployPlugin" {
    98  		deployTriggered = true
    99  		err = handleMTADeployment(config, command)
   100  	} else if config.DeployTool == "cf_native" {
   101  		deployTriggered = true
   102  		err = handleCFNativeDeployment(config, command)
   103  	} else {
   104  		log.Entry().Warningf("Found unsupported deployTool ('%s'). Skipping deployment. Supported deploy tools: 'mtaDeployPlugin', 'cf_native'", config.DeployTool)
   105  	}
   106  
   107  	if deployTriggered {
   108  		prepareInflux(err == nil, config, influxData)
   109  	}
   110  
   111  	return err
   112  }
   113  
   114  func validateDeployTool(config *cloudFoundryDeployOptions) {
   115  	if config.DeployTool != "" || config.BuildTool == "" {
   116  		return
   117  	}
   118  
   119  	switch config.BuildTool {
   120  	case "mta":
   121  		config.DeployTool = "mtaDeployPlugin"
   122  	default:
   123  		config.DeployTool = "cf_native"
   124  	}
   125  	log.Entry().Infof("Parameter deployTool not specified - deriving from buildTool '%s': '%s'",
   126  		config.BuildTool, config.DeployTool)
   127  }
   128  
   129  func validateAppName(appName string) error {
   130  	// for the sake of brevity we consider the empty string as valid app name here
   131  	isValidAppName, err := regexp.MatchString("^$|^[a-zA-Z0-9]$|^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$", appName)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	if isValidAppName {
   136  		return nil
   137  	}
   138  	const (
   139  		underscore = "_"
   140  		dash       = "-"
   141  		docuLink   = "https://docs.cloudfoundry.org/devguide/deploy-apps/deploy-app.html#basic-settings"
   142  	)
   143  
   144  	log.Entry().Warningf("Your application name '%s' contains non-alphanumeric characters which may lead to errors in the future, "+
   145  		"as they are not supported by CloudFoundry. For more details please visit %s", appName, docuLink)
   146  
   147  	var fail bool
   148  	message := []string{fmt.Sprintf("Your application name '%s'", appName)}
   149  	if strings.Contains(appName, underscore) {
   150  		message = append(message, fmt.Sprintf("contains a '%s' (underscore) which is not allowed, only letters, dashes and numbers can be used.", underscore))
   151  		fail = true
   152  	}
   153  	if strings.HasPrefix(appName, dash) || strings.HasSuffix(appName, dash) {
   154  		message = append(message, fmt.Sprintf("starts or ends with a '%s' (dash) which is not allowed, only letters and numbers can be used.", dash))
   155  		fail = true
   156  	}
   157  	message = append(message, fmt.Sprintf("Please change the name to fit this requirement(s). For more details please visit %s.", docuLink))
   158  	if fail {
   159  		return fmt.Errorf(strings.Join(message, " "))
   160  	}
   161  	return nil
   162  }
   163  
   164  func prepareInflux(success bool, config *cloudFoundryDeployOptions, influxData *cloudFoundryDeployInflux) {
   165  
   166  	if influxData == nil {
   167  		return
   168  	}
   169  
   170  	result := "FAILURE"
   171  
   172  	if success {
   173  		result = "SUCCESS"
   174  	}
   175  
   176  	influxData.deployment_data.tags.artifactVersion = config.ArtifactVersion
   177  	influxData.deployment_data.tags.deployUser = config.Username
   178  	influxData.deployment_data.tags.deployResult = result
   179  	influxData.deployment_data.tags.cfAPIEndpoint = config.APIEndpoint
   180  	influxData.deployment_data.tags.cfOrg = config.Org
   181  	influxData.deployment_data.tags.cfSpace = config.Space
   182  
   183  	// n/a (literally) is also reported in groovy
   184  	influxData.deployment_data.fields.artifactURL = "n/a"
   185  	influxData.deployment_data.fields.commitHash = config.CommitHash
   186  
   187  	influxData.deployment_data.fields.deployTime = strings.ToUpper(_now().Format("Jan 02 2006 15:04:05"))
   188  
   189  	// we should discuss how we handle the job trigger
   190  	// 1.) outside Jenkins
   191  	// 2.) inside Jenkins (how to get)
   192  	influxData.deployment_data.fields.jobTrigger = "n/a"
   193  }
   194  
   195  func handleMTADeployment(config *cloudFoundryDeployOptions, command command.ExecRunner) error {
   196  
   197  	mtarFilePath := config.MtaPath
   198  
   199  	if len(mtarFilePath) == 0 {
   200  
   201  		var err error
   202  		mtarFilePath, err = findMtar()
   203  
   204  		if err != nil {
   205  			return err
   206  		}
   207  
   208  		log.Entry().Debugf("Using mtar file '%s' found in workspace", mtarFilePath)
   209  
   210  	} else {
   211  
   212  		exists, err := fileUtils.FileExists(mtarFilePath)
   213  
   214  		if err != nil {
   215  			return errors.Wrapf(err, "Cannot check if file path '%s' exists", mtarFilePath)
   216  		}
   217  
   218  		if !exists {
   219  			return fmt.Errorf("mtar file '%s' retrieved from configuration does not exist", mtarFilePath)
   220  		}
   221  
   222  		log.Entry().Debugf("Using mtar file '%s' from configuration", mtarFilePath)
   223  	}
   224  
   225  	return deployMta(config, mtarFilePath, command)
   226  }
   227  
   228  type deployConfig struct {
   229  	DeployCommand   string
   230  	DeployOptions   []string
   231  	AppName         string
   232  	ManifestFile    string
   233  	SmokeTestScript []string
   234  }
   235  
   236  func handleCFNativeDeployment(config *cloudFoundryDeployOptions, command command.ExecRunner) error {
   237  
   238  	deployType, err := checkAndUpdateDeployTypeForNotSupportedManifest(config)
   239  
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	var deployCommand string
   245  	var smokeTestScript []string
   246  	var deployOptions []string
   247  
   248  	// deploy command will be provided by the prepare functions below
   249  
   250  	if deployType == "blue-green" {
   251  		deployCommand, deployOptions, smokeTestScript, err = prepareBlueGreenCfNativeDeploy(config)
   252  		if err != nil {
   253  			return errors.Wrapf(err, "Cannot prepare cf native deployment. DeployType '%s'", deployType)
   254  		}
   255  	} else if deployType == "standard" {
   256  		deployCommand, deployOptions, smokeTestScript, err = prepareCfPushCfNativeDeploy(config)
   257  		if err != nil {
   258  			return errors.Wrapf(err, "Cannot prepare cf push native deployment. DeployType '%s'", deployType)
   259  		}
   260  	} else {
   261  		return fmt.Errorf("Invalid deploy type received: '%s'. Supported values: %v", deployType, []string{"blue-green", "standard"})
   262  	}
   263  
   264  	appName, err := getAppName(config)
   265  	if err != nil {
   266  		return err
   267  	}
   268  
   269  	log.Entry().Infof("CF native deployment ('%s') with:", config.DeployType)
   270  	log.Entry().Infof("cfAppName='%s'", appName)
   271  	log.Entry().Infof("cfManifest='%s'", config.Manifest)
   272  	log.Entry().Infof("cfManifestVariables: '%v'", config.ManifestVariables)
   273  	log.Entry().Infof("cfManifestVariablesFiles: '%v'", config.ManifestVariablesFiles)
   274  	log.Entry().Infof("cfdeployDockerImage: '%s'", config.DeployDockerImage)
   275  	log.Entry().Infof("smokeTestScript: '%s'", config.SmokeTestScript)
   276  
   277  	additionalEnvironment := []string{
   278  		"STATUS_CODE=" + strconv.FormatInt(int64(config.SmokeTestStatusCode), 10),
   279  	}
   280  
   281  	if len(config.DockerPassword) > 0 {
   282  		additionalEnvironment = append(additionalEnvironment, "CF_DOCKER_PASSWORD="+config.DockerPassword)
   283  	}
   284  
   285  	myDeployConfig := deployConfig{
   286  		DeployCommand:   deployCommand,
   287  		DeployOptions:   deployOptions,
   288  		AppName:         config.AppName,
   289  		ManifestFile:    config.Manifest,
   290  		SmokeTestScript: smokeTestScript,
   291  	}
   292  
   293  	log.Entry().Infof("DeployConfig: %v", myDeployConfig)
   294  
   295  	return deployCfNative(myDeployConfig, config, additionalEnvironment, command)
   296  }
   297  
   298  func deployCfNative(deployConfig deployConfig, config *cloudFoundryDeployOptions, additionalEnvironment []string, cmd command.ExecRunner) error {
   299  
   300  	deployStatement := []string{
   301  		deployConfig.DeployCommand,
   302  	}
   303  
   304  	if len(deployConfig.AppName) > 0 {
   305  		deployStatement = append(deployStatement, deployConfig.AppName)
   306  	}
   307  
   308  	if len(deployConfig.DeployOptions) > 0 {
   309  		deployStatement = append(deployStatement, deployConfig.DeployOptions...)
   310  	}
   311  
   312  	if len(deployConfig.ManifestFile) > 0 {
   313  		deployStatement = append(deployStatement, "-f")
   314  		deployStatement = append(deployStatement, deployConfig.ManifestFile)
   315  	}
   316  
   317  	if len(config.DeployDockerImage) > 0 && config.DeployType != "blue-green" {
   318  		deployStatement = append(deployStatement, "--docker-image", config.DeployDockerImage)
   319  	}
   320  
   321  	if len(config.DockerUsername) > 0 && config.DeployType != "blue-green" {
   322  		deployStatement = append(deployStatement, "--docker-username", config.DockerUsername)
   323  	}
   324  
   325  	if len(deployConfig.SmokeTestScript) > 0 {
   326  		deployStatement = append(deployStatement, deployConfig.SmokeTestScript...)
   327  	}
   328  
   329  	if len(config.CfNativeDeployParameters) > 0 {
   330  		deployStatement = append(deployStatement, strings.Fields(config.CfNativeDeployParameters)...)
   331  	}
   332  
   333  	stopOldAppIfRunning := func(_cmd command.ExecRunner) error {
   334  
   335  		if config.KeepOldInstance && config.DeployType == "blue-green" {
   336  			oldAppName := deployConfig.AppName + "-old"
   337  
   338  			var buff bytes.Buffer
   339  
   340  			_cmd.Stdout(&buff)
   341  
   342  			defer func() {
   343  				_cmd.Stdout(log.Writer())
   344  			}()
   345  
   346  			err := _cmd.RunExecutable("cf", "stop", oldAppName)
   347  
   348  			if err != nil {
   349  
   350  				cfStopLog := buff.String()
   351  
   352  				if !strings.Contains(cfStopLog, oldAppName+" not found") {
   353  					return fmt.Errorf("Could not stop application '%s'. Error: %s", oldAppName, cfStopLog)
   354  				}
   355  				log.Entry().Infof("Cannot stop application '%s' since this appliation was not found.", oldAppName)
   356  
   357  			} else {
   358  				log.Entry().Infof("Old application '%s' has been stopped.", oldAppName)
   359  			}
   360  		}
   361  
   362  		return nil
   363  	}
   364  
   365  	return cfDeploy(config, deployStatement, additionalEnvironment, stopOldAppIfRunning, cmd)
   366  }
   367  
   368  func getManifest(name string) (cloudfoundry.Manifest, error) {
   369  	return cloudfoundry.ReadManifest(name)
   370  }
   371  
   372  func getAppName(config *cloudFoundryDeployOptions) (string, error) {
   373  
   374  	if len(config.AppName) > 0 {
   375  		return config.AppName, nil
   376  	}
   377  	if config.DeployType == "blue-green" {
   378  		return "", fmt.Errorf("Blue-green plugin requires app name to be passed (see https://github.com/bluemixgaragelondon/cf-blue-green-deploy/issues/27)")
   379  	}
   380  	if len(config.Manifest) == 0 {
   381  		return "", fmt.Errorf("Manifest file not provided in configuration. Cannot retrieve app name")
   382  	}
   383  	fileExists, err := fileUtils.FileExists(config.Manifest)
   384  	if err != nil {
   385  		return "", errors.Wrapf(err, "Cannot check if file '%s' exists", config.Manifest)
   386  	}
   387  	if !fileExists {
   388  		return "", fmt.Errorf("Manifest file '%s' not found. Cannot retrieve app name", config.Manifest)
   389  	}
   390  	manifest, err := _getManifest(config.Manifest)
   391  	if err != nil {
   392  		return "", err
   393  	}
   394  	apps, err := manifest.GetApplications()
   395  	if err != nil {
   396  		return "", err
   397  	}
   398  
   399  	if len(apps) == 0 {
   400  		return "", fmt.Errorf("No apps declared in manifest '%s'", config.Manifest)
   401  	}
   402  	namePropertyExists, err := manifest.ApplicationHasProperty(0, "name")
   403  	if err != nil {
   404  		return "", err
   405  	}
   406  	if !namePropertyExists {
   407  		return "", fmt.Errorf("No appName available in manifest '%s'", config.Manifest)
   408  	}
   409  	appName, err := manifest.GetApplicationProperty(0, "name")
   410  	if err != nil {
   411  		return "", err
   412  	}
   413  	var name string
   414  	var ok bool
   415  	if name, ok = appName.(string); !ok {
   416  		return "", fmt.Errorf("appName from manifest '%s' has wrong type", config.Manifest)
   417  	}
   418  	if len(name) == 0 {
   419  		return "", fmt.Errorf("appName from manifest '%s' is empty", config.Manifest)
   420  	}
   421  	return name, nil
   422  }
   423  
   424  func handleSmokeTestScript(smokeTestScript string) ([]string, error) {
   425  
   426  	if smokeTestScript == "blueGreenCheckScript.sh" {
   427  		// what should we do if there is already a script with the given name? Should we really overwrite ...
   428  		err := fileUtils.FileWrite(smokeTestScript, []byte(defaultSmokeTestScript), 0755)
   429  		if err != nil {
   430  			return []string{}, fmt.Errorf("failed to write default smoke-test script: %w", err)
   431  		}
   432  		log.Entry().Debugf("smoke test script '%s' has been written.", smokeTestScript)
   433  	}
   434  
   435  	if len(smokeTestScript) > 0 {
   436  		err := fileUtils.Chmod(smokeTestScript, 0755)
   437  		if err != nil {
   438  			return []string{}, fmt.Errorf("failed to make smoke-test script executable: %w", err)
   439  		}
   440  		pwd, err := fileUtils.Getwd()
   441  
   442  		if err != nil {
   443  			return []string{}, fmt.Errorf("failed to get current working directory for execution of smoke-test script: %w", err)
   444  		}
   445  
   446  		return []string{"--smoke-test", fmt.Sprintf("%s", filepath.Join(pwd, smokeTestScript))}, nil
   447  	}
   448  	return []string{}, nil
   449  }
   450  
   451  func prepareBlueGreenCfNativeDeploy(config *cloudFoundryDeployOptions) (string, []string, []string, error) {
   452  
   453  	smokeTest, err := handleSmokeTestScript(config.SmokeTestScript)
   454  	if err != nil {
   455  		return "", []string{}, []string{}, err
   456  	}
   457  
   458  	var deployOptions = []string{}
   459  
   460  	if !config.KeepOldInstance {
   461  		deployOptions = append(deployOptions, "--delete-old-apps")
   462  	}
   463  
   464  	if len(config.Manifest) > 0 {
   465  		manifestFileExists, err := fileUtils.FileExists(config.Manifest)
   466  		if err != nil {
   467  			return "", []string{}, []string{}, errors.Wrapf(err, "Cannot check if file '%s' exists", config.Manifest)
   468  		}
   469  
   470  		if !manifestFileExists {
   471  
   472  			log.Entry().Infof("Manifest file '%s' does not exist", config.Manifest)
   473  
   474  		} else {
   475  
   476  			manifestVariables, err := toStringInterfaceMap(toParameterMap(config.ManifestVariables))
   477  			if err != nil {
   478  				return "", []string{}, []string{}, errors.Wrapf(err, "Cannot prepare manifest variables: '%v'", config.ManifestVariables)
   479  			}
   480  
   481  			manifestVariablesFiles, err := validateManifestVariablesFiles(config.ManifestVariablesFiles)
   482  			if err != nil {
   483  				return "", []string{}, []string{}, errors.Wrapf(err, "Cannot validate manifest variables files '%v'", config.ManifestVariablesFiles)
   484  			}
   485  
   486  			modified, err := _replaceVariables(config.Manifest, manifestVariables, manifestVariablesFiles)
   487  			if err != nil {
   488  				return "", []string{}, []string{}, errors.Wrap(err, "Cannot prepare manifest file")
   489  			}
   490  
   491  			if modified {
   492  				log.Entry().Infof("Manifest file '%s' has been updated (variable substitution)", config.Manifest)
   493  			} else {
   494  				log.Entry().Infof("Manifest file '%s' has not been updated (no variable substitution)", config.Manifest)
   495  			}
   496  
   497  			err = handleLegacyCfManifest(config.Manifest)
   498  			if err != nil {
   499  				return "", []string{}, []string{}, errors.Wrapf(err, "Cannot handle legacy manifest '%s'", config.Manifest)
   500  			}
   501  		}
   502  	} else {
   503  		log.Entry().Info("No manifest file configured")
   504  	}
   505  	return "blue-green-deploy", deployOptions, smokeTest, nil
   506  }
   507  
   508  // validateManifestVariablesFiles: in case the only provided file is 'manifest-variables.yml' and this file does not
   509  // exist we ignore that file. For any other file there is no check if that file exists. In case several files are
   510  // provided we also do not check for the default file 'manifest-variables.yml'
   511  func validateManifestVariablesFiles(manifestVariablesFiles []string) ([]string, error) {
   512  
   513  	const defaultManifestVariableFileName = "manifest-variables.yml"
   514  	if len(manifestVariablesFiles) == 1 && manifestVariablesFiles[0] == defaultManifestVariableFileName {
   515  		// we have only the default file. Most likely this is not configured, but we simply have the default.
   516  		// In case this file does not exist we ignore that file.
   517  		exists, err := fileUtils.FileExists(defaultManifestVariableFileName)
   518  		if err != nil {
   519  			return []string{}, errors.Wrapf(err, "Cannot check if file '%s' exists", defaultManifestVariableFileName)
   520  		}
   521  		if !exists {
   522  			return []string{}, nil
   523  		}
   524  	}
   525  	return manifestVariablesFiles, nil
   526  }
   527  
   528  func toParameterMap(parameters []string) (*orderedmap.OrderedMap, error) {
   529  
   530  	parameterMap := orderedmap.NewOrderedMap()
   531  
   532  	for _, p := range parameters {
   533  		keyVal := strings.Split(p, "=")
   534  		if len(keyVal) != 2 {
   535  			return nil, fmt.Errorf("Invalid parameter provided (expected format <key>=<val>: '%s'", p)
   536  		}
   537  		parameterMap.Set(keyVal[0], keyVal[1])
   538  	}
   539  	return parameterMap, nil
   540  }
   541  
   542  func handleLegacyCfManifest(manifestFile string) error {
   543  	manifest, err := _getManifest(manifestFile)
   544  	if err != nil {
   545  		return err
   546  	}
   547  
   548  	err = manifest.Transform()
   549  	if err != nil {
   550  		return err
   551  	}
   552  	if manifest.IsModified() {
   553  
   554  		err = manifest.WriteManifest()
   555  
   556  		if err != nil {
   557  			return err
   558  		}
   559  		log.Entry().Infof("Manifest file '%s' was in legacy format has been transformed and updated.", manifestFile)
   560  	} else {
   561  		log.Entry().Debugf("Manifest file '%s' was not in legacy format. No transformation needed, no update performed.", manifestFile)
   562  	}
   563  	return nil
   564  }
   565  
   566  func prepareCfPushCfNativeDeploy(config *cloudFoundryDeployOptions) (string, []string, []string, error) {
   567  
   568  	deployOptions := []string{}
   569  	varOptions, err := _getVarsOptions(config.ManifestVariables)
   570  	if err != nil {
   571  		return "", []string{}, []string{}, errors.Wrapf(err, "Cannot prepare var-options: '%v'", config.ManifestVariables)
   572  	}
   573  
   574  	varFileOptions, err := _getVarsFileOptions(config.ManifestVariablesFiles)
   575  	if err != nil {
   576  		if e, ok := err.(*cloudfoundry.VarsFilesNotFoundError); ok {
   577  			for _, missingVarFile := range e.MissingFiles {
   578  				log.Entry().Warningf("We skip adding not-existing file '%s' as a vars-file to the cf create-service-push call", missingVarFile)
   579  			}
   580  		} else {
   581  			return "", []string{}, []string{}, errors.Wrapf(err, "Cannot prepare var-file-options: '%v'", config.ManifestVariablesFiles)
   582  		}
   583  	}
   584  
   585  	deployOptions = append(deployOptions, varOptions...)
   586  	deployOptions = append(deployOptions, varFileOptions...)
   587  
   588  	return "push", deployOptions, []string{}, nil
   589  }
   590  
   591  func toStringInterfaceMap(in *orderedmap.OrderedMap, err error) (map[string]interface{}, error) {
   592  
   593  	out := map[string]interface{}{}
   594  
   595  	if err == nil {
   596  		for _, key := range in.Keys() {
   597  			if k, ok := key.(string); ok {
   598  				val, exists := in.Get(key)
   599  				if exists {
   600  					out[k] = val
   601  				} else {
   602  					return nil, fmt.Errorf("No entry found for '%v'", key)
   603  				}
   604  			} else {
   605  				return nil, fmt.Errorf("Cannot cast key '%v' to string", key)
   606  			}
   607  		}
   608  	}
   609  
   610  	return out, err
   611  }
   612  
   613  func checkAndUpdateDeployTypeForNotSupportedManifest(config *cloudFoundryDeployOptions) (string, error) {
   614  
   615  	manifestFile := config.Manifest
   616  	if len(manifestFile) == 0 {
   617  		manifestFile = "manifest.yml"
   618  	}
   619  
   620  	manifestFileExists, err := fileUtils.FileExists(manifestFile)
   621  	if err != nil {
   622  		return "", err
   623  	}
   624  
   625  	if config.DeployType == "blue-green" && manifestFileExists {
   626  
   627  		manifest, _ := _getManifest(manifestFile)
   628  
   629  		apps, err := manifest.GetApplications()
   630  
   631  		if err != nil {
   632  			return "", fmt.Errorf("failed to obtain applications from manifest: %w", err)
   633  		}
   634  		if len(apps) > 1 {
   635  			return "", fmt.Errorf("Your manifest contains more than one application. For blue green deployments your manifest file may contain only one application")
   636  		}
   637  
   638  		hasNoRouteProperty, err := manifest.ApplicationHasProperty(0, "no-route")
   639  		if err != nil {
   640  			return "", errors.Wrap(err, "Failed to obtain 'no-route' property from manifest")
   641  		}
   642  		if len(apps) == 1 && hasNoRouteProperty {
   643  
   644  			const deployTypeStandard = "standard"
   645  			log.Entry().Warningf("Blue green deployment is not possible for application without route. Using deployment type '%s' instead.", deployTypeStandard)
   646  			return deployTypeStandard, nil
   647  		}
   648  	}
   649  
   650  	return config.DeployType, nil
   651  }
   652  
   653  func deployMta(config *cloudFoundryDeployOptions, mtarFilePath string, command command.ExecRunner) error {
   654  
   655  	deployCommand := "deploy"
   656  	deployParams := []string{}
   657  
   658  	if len(config.MtaDeployParameters) > 0 {
   659  		deployParams = append(deployParams, strings.Split(config.MtaDeployParameters, " ")...)
   660  	}
   661  
   662  	if config.DeployType == "bg-deploy" || config.DeployType == "blue-green" {
   663  
   664  		deployCommand = "bg-deploy"
   665  
   666  		const noConfirmFlag = "--no-confirm"
   667  		if !piperutils.ContainsString(deployParams, noConfirmFlag) {
   668  			deployParams = append(deployParams, noConfirmFlag)
   669  		}
   670  	}
   671  
   672  	cfDeployParams := []string{
   673  		deployCommand,
   674  		mtarFilePath,
   675  	}
   676  
   677  	if len(deployParams) > 0 {
   678  		cfDeployParams = append(cfDeployParams, deployParams...)
   679  	}
   680  
   681  	extFileParams, extFiles := handleMtaExtensionDescriptors(config.MtaExtensionDescriptor)
   682  
   683  	for _, extFile := range extFiles {
   684  		_, err := fileUtils.Copy(extFile, extFile+".original")
   685  		if err != nil {
   686  			return fmt.Errorf("Cannot prepare mta extension files: %w", err)
   687  		}
   688  		_, _, err = handleMtaExtensionCredentials(extFile, config.MtaExtensionCredentials)
   689  		if err != nil {
   690  			return fmt.Errorf("Cannot handle credentials inside mta extension files: %w", err)
   691  		}
   692  	}
   693  
   694  	cfDeployParams = append(cfDeployParams, extFileParams...)
   695  
   696  	err := cfDeploy(config, cfDeployParams, nil, nil, command)
   697  
   698  	for _, extFile := range extFiles {
   699  		renameError := fileUtils.FileRename(extFile+".original", extFile)
   700  		if err == nil && renameError != nil {
   701  			return renameError
   702  		}
   703  	}
   704  
   705  	return err
   706  }
   707  
   708  func handleMtaExtensionCredentials(extFile string, credentials map[string]interface{}) (updated, containsUnresolved bool, err error) {
   709  
   710  	log.Entry().Debugf("Inserting credentials into extension file '%s'", extFile)
   711  
   712  	b, err := fileUtils.FileRead(extFile)
   713  	if err != nil {
   714  		return false, false, errors.Wrapf(err, "Cannot handle credentials for mta extension file '%s'", extFile)
   715  	}
   716  	content := string(b)
   717  
   718  	env, err := toMap(_environ(), "=")
   719  	if err != nil {
   720  		return false, false, errors.Wrap(err, "Cannot handle mta extension credentials.")
   721  	}
   722  
   723  	missingCredentials := []string{}
   724  	for name, credentialKey := range credentials {
   725  		credKey, ok := credentialKey.(string)
   726  		if !ok {
   727  			return false, false, fmt.Errorf("cannot handle mta extension credentials: Cannot cast '%v' (type %T) to string", credentialKey, credentialKey)
   728  		}
   729  
   730  		const allowedVariableNamePattern = "^[-_A-Za-z0-9]+$"
   731  		alphaNumOnly := regexp.MustCompile(allowedVariableNamePattern)
   732  		if !alphaNumOnly.MatchString(name) {
   733  			return false, false, fmt.Errorf("credential key name '%s' contains unsupported character. Must contain only %s", name, allowedVariableNamePattern)
   734  		}
   735  		pattern := regexp.MustCompile("<%=\\s*" + name + "\\s*%>")
   736  		if pattern.MatchString(content) {
   737  			cred := env[toEnvVarKey(credKey)]
   738  			if len(cred) == 0 {
   739  				missingCredentials = append(missingCredentials, credKey)
   740  				continue
   741  			}
   742  			content = pattern.ReplaceAllLiteralString(content, cred)
   743  			updated = true
   744  			log.Entry().Debugf("Mta extension credentials handling: Placeholder '%s' has been replaced by credential denoted by '%s'/'%s' in file '%s'", name, credKey, toEnvVarKey(credKey), extFile)
   745  		} else {
   746  			log.Entry().Debugf("Mta extension credentials handling: Variable '%s' is not used in file '%s'", name, extFile)
   747  		}
   748  	}
   749  	if len(missingCredentials) > 0 {
   750  		missinCredsEnvVarKeyCompatible := []string{}
   751  		for _, missingKey := range missingCredentials {
   752  			missinCredsEnvVarKeyCompatible = append(missinCredsEnvVarKeyCompatible, toEnvVarKey(missingKey))
   753  		}
   754  		// ensure stable order of the entries. Needed e.g. for the tests.
   755  		sort.Strings(missingCredentials)
   756  		sort.Strings(missinCredsEnvVarKeyCompatible)
   757  		return false, false, fmt.Errorf("cannot handle mta extension credentials: No credentials found for '%s'/'%s'. Are these credentials maintained?", missingCredentials, missinCredsEnvVarKeyCompatible)
   758  	}
   759  	if !updated {
   760  		log.Entry().Debugf("Mta extension credentials handling: Extension file '%s' has not been updated. Seems to contain no credentials.", extFile)
   761  	} else {
   762  		fInfo, err := fileUtils.Stat(extFile)
   763  		fMode := fInfo.Mode()
   764  		if err != nil {
   765  			return false, false, errors.Wrap(err, "Cannot handle mta extension credentials.")
   766  		}
   767  		err = fileUtils.FileWrite(extFile, []byte(content), fMode)
   768  		if err != nil {
   769  			return false, false, errors.Wrap(err, "Cannot handle mta extension credentials.")
   770  		}
   771  		log.Entry().Debugf("Mta extension credentials handling: Extension file '%s' has been updated.", extFile)
   772  	}
   773  
   774  	re := regexp.MustCompile(`<%=.+%>`)
   775  	placeholders := re.FindAll([]byte(content), -1)
   776  	containsUnresolved = (len(placeholders) > 0)
   777  
   778  	if containsUnresolved {
   779  		log.Entry().Warningf("mta extension credential handling: Unresolved placeholders found after inserting credentials: %s", placeholders)
   780  	}
   781  
   782  	return updated, containsUnresolved, nil
   783  }
   784  
   785  func toEnvVarKey(key string) string {
   786  	key = regexp.MustCompile(`[^A-Za-z0-9]`).ReplaceAllString(key, "_")
   787  	return strings.ToUpper(regexp.MustCompile(`([a-z0-9])([A-Z])`).ReplaceAllString(key, "${1}_${2}"))
   788  }
   789  
   790  func toMap(keyValue []string, separator string) (map[string]string, error) {
   791  	result := map[string]string{}
   792  	for _, entry := range keyValue {
   793  		kv := strings.Split(entry, separator)
   794  		if len(kv) < 2 {
   795  			return map[string]string{}, fmt.Errorf("Cannot convert to map: separator '%s' not found in entry '%s'", separator, entry)
   796  		}
   797  		result[kv[0]] = strings.Join(kv[1:], separator)
   798  	}
   799  	return result, nil
   800  }
   801  
   802  func handleMtaExtensionDescriptors(mtaExtensionDescriptor string) ([]string, []string) {
   803  	var result = []string{}
   804  	var extFiles = []string{}
   805  	for _, part := range strings.Fields(strings.Trim(mtaExtensionDescriptor, " ")) {
   806  		if part == "-e" || part == "" {
   807  			continue
   808  		}
   809  		// REVISIT: maybe check if the extension descriptor exists
   810  		extFiles = append(extFiles, part)
   811  	}
   812  	if len(extFiles) > 0 {
   813  		result = append(result, "-e")
   814  		result = append(result, strings.Join(extFiles, ","))
   815  	}
   816  	return result, extFiles
   817  }
   818  
   819  func cfDeploy(
   820  	config *cloudFoundryDeployOptions,
   821  	cfDeployParams []string,
   822  	additionalEnvironment []string,
   823  	postDeployAction func(command command.ExecRunner) error,
   824  	command command.ExecRunner) error {
   825  
   826  	const cfLogFile = "cf.log"
   827  	var err error
   828  	var loginPerformed bool
   829  
   830  	additionalEnvironment = append(additionalEnvironment, "CF_TRACE="+cfLogFile)
   831  
   832  	if len(config.CfHome) > 0 {
   833  		additionalEnvironment = append(additionalEnvironment, "CF_HOME="+config.CfHome)
   834  	}
   835  
   836  	if len(config.CfPluginHome) > 0 {
   837  		additionalEnvironment = append(additionalEnvironment, "CF_PLUGIN_HOME="+config.CfPluginHome)
   838  	}
   839  
   840  	log.Entry().Infof("Using additional environment variables: %s", additionalEnvironment)
   841  
   842  	// TODO set HOME to config.DockerWorkspace
   843  	command.SetEnv(additionalEnvironment)
   844  
   845  	err = command.RunExecutable("cf", "version")
   846  
   847  	if err == nil {
   848  		err = _cfLogin(command, cloudfoundry.LoginOptions{
   849  			CfAPIEndpoint: config.APIEndpoint,
   850  			CfOrg:         config.Org,
   851  			CfSpace:       config.Space,
   852  			Username:      config.Username,
   853  			Password:      config.Password,
   854  			CfLoginOpts:   strings.Fields(config.LoginParameters),
   855  		})
   856  	}
   857  
   858  	if err == nil {
   859  		loginPerformed = true
   860  		err = command.RunExecutable("cf", []string{"plugins"}...)
   861  		if err != nil {
   862  			log.Entry().WithError(err).Errorf("Command '%s' failed.", []string{"plugins"})
   863  		}
   864  	}
   865  
   866  	if err == nil {
   867  		err = command.RunExecutable("cf", cfDeployParams...)
   868  		if err != nil {
   869  			log.Entry().WithError(err).Errorf("Command '%s' failed.", cfDeployParams)
   870  		}
   871  	}
   872  
   873  	if err == nil && postDeployAction != nil {
   874  		err = postDeployAction(command)
   875  	}
   876  
   877  	if loginPerformed {
   878  
   879  		logoutErr := _cfLogout(command)
   880  
   881  		if logoutErr != nil {
   882  			log.Entry().WithError(logoutErr).Errorf("Cannot perform cf logout")
   883  			if err == nil {
   884  				err = logoutErr
   885  			}
   886  		}
   887  	}
   888  
   889  	if err != nil || GeneralConfig.Verbose {
   890  		e := handleCfCliLog(cfLogFile)
   891  		if e != nil {
   892  			log.Entry().WithError(err).Errorf("Error reading cf log file '%s'.", cfLogFile)
   893  		}
   894  	}
   895  
   896  	return err
   897  }
   898  
   899  func findMtar() (string, error) {
   900  
   901  	const pattern = "**/*.mtar"
   902  
   903  	mtars, err := fileUtils.Glob(pattern)
   904  
   905  	if err != nil {
   906  		return "", err
   907  	}
   908  
   909  	if len(mtars) == 0 {
   910  		return "", fmt.Errorf("No mtar file matching pattern '%s' found", pattern)
   911  	}
   912  
   913  	if len(mtars) > 1 {
   914  		sMtars := []string{}
   915  		for _, mtar := range mtars {
   916  			sMtars = append(sMtars, mtar)
   917  		}
   918  		return "", fmt.Errorf("Found multiple mtar files matching pattern '%s' (%s), please specify file via parameter 'mtarPath'", pattern, strings.Join(sMtars, ","))
   919  	}
   920  
   921  	return mtars[0], nil
   922  }
   923  
   924  func handleCfCliLog(logFile string) error {
   925  
   926  	fExists, err := fileUtils.FileExists(logFile)
   927  
   928  	if err != nil {
   929  		return err
   930  	}
   931  
   932  	log.Entry().Info("### START OF CF CLI TRACE OUTPUT ###")
   933  
   934  	if fExists {
   935  
   936  		f, err := os.Open(logFile)
   937  
   938  		if err != nil {
   939  			return err
   940  		}
   941  
   942  		defer f.Close()
   943  
   944  		bReader := bufio.NewReader(f)
   945  		for {
   946  			line, err := bReader.ReadString('\n')
   947  			if err == nil || err == io.EOF {
   948  				// maybe inappropriate to log as info. Maybe the line from the
   949  				// log indicates an error, but that is something like a project
   950  				// standard.
   951  				log.Entry().Info(strings.TrimSuffix(line, "\n"))
   952  			}
   953  			if err != nil {
   954  				break
   955  			}
   956  		}
   957  	} else {
   958  		log.Entry().Warningf("No trace file found at '%s'", logFile)
   959  	}
   960  
   961  	log.Entry().Info("### END OF CF CLI TRACE OUTPUT ###")
   962  
   963  	return err
   964  }