github.com/SAP/jenkins-library@v1.362.0/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  		log.Entry().Warn("[WARN] Blue-green deployment type is deprecated for cf native builds " +
   252  			"and will be completely removed by 15.06.2024" +
   253  			"Instead set parameter `cfNativeDeployParameters: '--strategy rolling'`. " +
   254  			"Please refer to the Cloud Foundry documentation for further information: " +
   255  			"https://docs.cloudfoundry.org/devguide/deploy-apps/rolling-deploy.html." +
   256  			"Or alternatively, switch to mta build tool. Please refer to mta build tool" +
   257  			"documentation for further information: https://sap.github.io/cloud-mta-build-tool/configuration/.")
   258  		deployCommand, deployOptions, smokeTestScript, err = prepareBlueGreenCfNativeDeploy(config)
   259  		if err != nil {
   260  			return errors.Wrapf(err, "Cannot prepare cf native deployment. DeployType '%s'", deployType)
   261  		}
   262  	} else if deployType == "standard" {
   263  		deployCommand, deployOptions, smokeTestScript, err = prepareCfPushCfNativeDeploy(config)
   264  		if err != nil {
   265  			return errors.Wrapf(err, "Cannot prepare cf push native deployment. DeployType '%s'", deployType)
   266  		}
   267  	} else {
   268  		return fmt.Errorf("Invalid deploy type received: '%s'. Supported values: %v", deployType, []string{"blue-green", "standard"})
   269  	}
   270  
   271  	appName, err := getAppName(config)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	manifestFile, err := getManifestFileName(config)
   277  
   278  	log.Entry().Infof("CF native deployment ('%s') with:", config.DeployType)
   279  	log.Entry().Infof("cfAppName='%s'", appName)
   280  	log.Entry().Infof("cfManifest='%s'", manifestFile)
   281  	log.Entry().Infof("cfManifestVariables: '%v'", config.ManifestVariables)
   282  	log.Entry().Infof("cfManifestVariablesFiles: '%v'", config.ManifestVariablesFiles)
   283  	log.Entry().Infof("cfdeployDockerImage: '%s'", config.DeployDockerImage)
   284  	log.Entry().Infof("smokeTestScript: '%s'", config.SmokeTestScript)
   285  
   286  	additionalEnvironment := []string{
   287  		"STATUS_CODE=" + strconv.FormatInt(int64(config.SmokeTestStatusCode), 10),
   288  	}
   289  
   290  	if len(config.DockerPassword) > 0 {
   291  		additionalEnvironment = append(additionalEnvironment, "CF_DOCKER_PASSWORD="+config.DockerPassword)
   292  	}
   293  
   294  	myDeployConfig := deployConfig{
   295  		DeployCommand:   deployCommand,
   296  		DeployOptions:   deployOptions,
   297  		AppName:         config.AppName,
   298  		ManifestFile:    config.Manifest,
   299  		SmokeTestScript: smokeTestScript,
   300  	}
   301  
   302  	log.Entry().Infof("DeployConfig: %v", myDeployConfig)
   303  
   304  	return deployCfNative(myDeployConfig, config, additionalEnvironment, command)
   305  }
   306  
   307  func deployCfNative(deployConfig deployConfig, config *cloudFoundryDeployOptions, additionalEnvironment []string, cmd command.ExecRunner) error {
   308  
   309  	deployStatement := []string{
   310  		deployConfig.DeployCommand,
   311  	}
   312  
   313  	if len(deployConfig.AppName) > 0 {
   314  		deployStatement = append(deployStatement, deployConfig.AppName)
   315  	}
   316  
   317  	if len(deployConfig.DeployOptions) > 0 {
   318  		deployStatement = append(deployStatement, deployConfig.DeployOptions...)
   319  	}
   320  
   321  	if len(deployConfig.ManifestFile) > 0 {
   322  		deployStatement = append(deployStatement, "-f")
   323  		deployStatement = append(deployStatement, deployConfig.ManifestFile)
   324  	}
   325  
   326  	if len(config.DeployDockerImage) > 0 && config.DeployType != "blue-green" {
   327  		deployStatement = append(deployStatement, "--docker-image", config.DeployDockerImage)
   328  	}
   329  
   330  	if len(config.DockerUsername) > 0 && config.DeployType != "blue-green" {
   331  		deployStatement = append(deployStatement, "--docker-username", config.DockerUsername)
   332  	}
   333  
   334  	if len(deployConfig.SmokeTestScript) > 0 {
   335  		deployStatement = append(deployStatement, deployConfig.SmokeTestScript...)
   336  	}
   337  
   338  	if len(config.CfNativeDeployParameters) > 0 {
   339  		deployStatement = append(deployStatement, strings.Fields(config.CfNativeDeployParameters)...)
   340  	}
   341  
   342  	stopOldAppIfRunning := func(_cmd command.ExecRunner) error {
   343  
   344  		if config.KeepOldInstance && config.DeployType == "blue-green" {
   345  			oldAppName := deployConfig.AppName + "-old"
   346  
   347  			var buff bytes.Buffer
   348  
   349  			_cmd.Stdout(&buff)
   350  
   351  			defer func() {
   352  				_cmd.Stdout(log.Writer())
   353  			}()
   354  
   355  			err := _cmd.RunExecutable("cf", "stop", oldAppName)
   356  
   357  			if err != nil {
   358  
   359  				cfStopLog := buff.String()
   360  
   361  				if !strings.Contains(cfStopLog, oldAppName+" not found") {
   362  					return fmt.Errorf("Could not stop application '%s'. Error: %s", oldAppName, cfStopLog)
   363  				}
   364  				log.Entry().Infof("Cannot stop application '%s' since this appliation was not found.", oldAppName)
   365  
   366  			} else {
   367  				log.Entry().Infof("Old application '%s' has been stopped.", oldAppName)
   368  			}
   369  		}
   370  
   371  		return nil
   372  	}
   373  
   374  	return cfDeploy(config, deployStatement, additionalEnvironment, stopOldAppIfRunning, cmd)
   375  }
   376  
   377  func getManifest(name string) (cloudfoundry.Manifest, error) {
   378  	return cloudfoundry.ReadManifest(name)
   379  }
   380  
   381  func getManifestFileName(config *cloudFoundryDeployOptions) (string, error) {
   382  
   383  	manifestFileName := config.Manifest
   384  	if len(manifestFileName) == 0 {
   385  		manifestFileName = "manifest.yml"
   386  	}
   387  	return manifestFileName, nil
   388  }
   389  
   390  func getAppName(config *cloudFoundryDeployOptions) (string, error) {
   391  
   392  	if len(config.AppName) > 0 {
   393  		return config.AppName, nil
   394  	}
   395  	if config.DeployType == "blue-green" {
   396  		return "", fmt.Errorf("Blue-green plugin requires app name to be passed (see https://github.com/bluemixgaragelondon/cf-blue-green-deploy/issues/27)")
   397  	}
   398  	manifestFile, err := getManifestFileName(config)
   399  
   400  	fileExists, err := fileUtils.FileExists(manifestFile)
   401  	if err != nil {
   402  		return "", errors.Wrapf(err, "Cannot check if file '%s' exists", manifestFile)
   403  	}
   404  	if !fileExists {
   405  		return "", fmt.Errorf("Manifest file '%s' not found. Cannot retrieve app name", manifestFile)
   406  	}
   407  	manifest, err := _getManifest(manifestFile)
   408  	if err != nil {
   409  		return "", err
   410  	}
   411  	apps, err := manifest.GetApplications()
   412  	if err != nil {
   413  		return "", err
   414  	}
   415  
   416  	if len(apps) == 0 {
   417  		return "", fmt.Errorf("No apps declared in manifest '%s'", manifestFile)
   418  	}
   419  	namePropertyExists, err := manifest.ApplicationHasProperty(0, "name")
   420  	if err != nil {
   421  		return "", err
   422  	}
   423  	if !namePropertyExists {
   424  		return "", fmt.Errorf("No appName available in manifest '%s'", manifestFile)
   425  	}
   426  	appName, err := manifest.GetApplicationProperty(0, "name")
   427  	if err != nil {
   428  		return "", err
   429  	}
   430  	var name string
   431  	var ok bool
   432  	if name, ok = appName.(string); !ok {
   433  		return "", fmt.Errorf("appName from manifest '%s' has wrong type", manifestFile)
   434  	}
   435  	if len(name) == 0 {
   436  		return "", fmt.Errorf("appName from manifest '%s' is empty", manifestFile)
   437  	}
   438  	return name, nil
   439  }
   440  
   441  func handleSmokeTestScript(smokeTestScript string) ([]string, error) {
   442  
   443  	if smokeTestScript == "blueGreenCheckScript.sh" {
   444  		// what should we do if there is already a script with the given name? Should we really overwrite ...
   445  		err := fileUtils.FileWrite(smokeTestScript, []byte(defaultSmokeTestScript), 0755)
   446  		if err != nil {
   447  			return []string{}, fmt.Errorf("failed to write default smoke-test script: %w", err)
   448  		}
   449  		log.Entry().Debugf("smoke test script '%s' has been written.", smokeTestScript)
   450  	}
   451  
   452  	if len(smokeTestScript) > 0 {
   453  		err := fileUtils.Chmod(smokeTestScript, 0755)
   454  		if err != nil {
   455  			return []string{}, fmt.Errorf("failed to make smoke-test script executable: %w", err)
   456  		}
   457  		pwd, err := fileUtils.Getwd()
   458  
   459  		if err != nil {
   460  			return []string{}, fmt.Errorf("failed to get current working directory for execution of smoke-test script: %w", err)
   461  		}
   462  
   463  		return []string{"--smoke-test", filepath.Join(pwd, smokeTestScript)}, nil
   464  	}
   465  	return []string{}, nil
   466  }
   467  
   468  func prepareBlueGreenCfNativeDeploy(config *cloudFoundryDeployOptions) (string, []string, []string, error) {
   469  
   470  	smokeTest, err := handleSmokeTestScript(config.SmokeTestScript)
   471  	if err != nil {
   472  		return "", []string{}, []string{}, err
   473  	}
   474  
   475  	var deployOptions = []string{}
   476  
   477  	if !config.KeepOldInstance {
   478  		deployOptions = append(deployOptions, "--delete-old-apps")
   479  	}
   480  
   481  	manifestFile, err := getManifestFileName(config)
   482  
   483  	manifestFileExists, err := fileUtils.FileExists(manifestFile)
   484  	if err != nil {
   485  		return "", []string{}, []string{}, errors.Wrapf(err, "Cannot check if file '%s' exists", manifestFile)
   486  	}
   487  
   488  	if !manifestFileExists {
   489  
   490  		log.Entry().Infof("Manifest file '%s' does not exist", manifestFile)
   491  
   492  	} else {
   493  
   494  		manifestVariables, err := toStringInterfaceMap(toParameterMap(config.ManifestVariables))
   495  		if err != nil {
   496  			return "", []string{}, []string{}, errors.Wrapf(err, "Cannot prepare manifest variables: '%v'", config.ManifestVariables)
   497  		}
   498  
   499  		manifestVariablesFiles, err := validateManifestVariablesFiles(config.ManifestVariablesFiles)
   500  		if err != nil {
   501  			return "", []string{}, []string{}, errors.Wrapf(err, "Cannot validate manifest variables files '%v'", config.ManifestVariablesFiles)
   502  		}
   503  
   504  		modified, err := _replaceVariables(manifestFile, manifestVariables, manifestVariablesFiles)
   505  		if err != nil {
   506  			return "", []string{}, []string{}, errors.Wrap(err, "Cannot prepare manifest file")
   507  		}
   508  
   509  		if modified {
   510  			log.Entry().Infof("Manifest file '%s' has been updated (variable substitution)", manifestFile)
   511  		} else {
   512  			log.Entry().Infof("Manifest file '%s' has not been updated (no variable substitution)", manifestFile)
   513  		}
   514  
   515  		err = handleLegacyCfManifest(manifestFile)
   516  		if err != nil {
   517  			return "", []string{}, []string{}, errors.Wrapf(err, "Cannot handle legacy manifest '%s'", manifestFile)
   518  		}
   519  	}
   520  
   521  	return "blue-green-deploy", deployOptions, smokeTest, nil
   522  }
   523  
   524  // validateManifestVariablesFiles: in case the only provided file is 'manifest-variables.yml' and this file does not
   525  // exist we ignore that file. For any other file there is no check if that file exists. In case several files are
   526  // provided we also do not check for the default file 'manifest-variables.yml'
   527  func validateManifestVariablesFiles(manifestVariablesFiles []string) ([]string, error) {
   528  
   529  	const defaultManifestVariableFileName = "manifest-variables.yml"
   530  	if len(manifestVariablesFiles) == 1 && manifestVariablesFiles[0] == defaultManifestVariableFileName {
   531  		// we have only the default file. Most likely this is not configured, but we simply have the default.
   532  		// In case this file does not exist we ignore that file.
   533  		exists, err := fileUtils.FileExists(defaultManifestVariableFileName)
   534  		if err != nil {
   535  			return []string{}, errors.Wrapf(err, "Cannot check if file '%s' exists", defaultManifestVariableFileName)
   536  		}
   537  		if !exists {
   538  			return []string{}, nil
   539  		}
   540  	}
   541  	return manifestVariablesFiles, nil
   542  }
   543  
   544  func toParameterMap(parameters []string) (*orderedmap.OrderedMap, error) {
   545  
   546  	parameterMap := orderedmap.NewOrderedMap()
   547  
   548  	for _, p := range parameters {
   549  		keyVal := strings.Split(p, "=")
   550  		if len(keyVal) != 2 {
   551  			return nil, fmt.Errorf("Invalid parameter provided (expected format <key>=<val>: '%s'", p)
   552  		}
   553  		parameterMap.Set(keyVal[0], keyVal[1])
   554  	}
   555  	return parameterMap, nil
   556  }
   557  
   558  func handleLegacyCfManifest(manifestFile string) error {
   559  	manifest, err := _getManifest(manifestFile)
   560  	if err != nil {
   561  		return err
   562  	}
   563  
   564  	err = manifest.Transform()
   565  	if err != nil {
   566  		return err
   567  	}
   568  	if manifest.IsModified() {
   569  
   570  		err = manifest.WriteManifest()
   571  
   572  		if err != nil {
   573  			return err
   574  		}
   575  		log.Entry().Infof("Manifest file '%s' was in legacy format has been transformed and updated.", manifestFile)
   576  	} else {
   577  		log.Entry().Debugf("Manifest file '%s' was not in legacy format. No transformation needed, no update performed.", manifestFile)
   578  	}
   579  	return nil
   580  }
   581  
   582  func prepareCfPushCfNativeDeploy(config *cloudFoundryDeployOptions) (string, []string, []string, error) {
   583  
   584  	deployOptions := []string{}
   585  	varOptions, err := _getVarsOptions(config.ManifestVariables)
   586  	if err != nil {
   587  		return "", []string{}, []string{}, errors.Wrapf(err, "Cannot prepare var-options: '%v'", config.ManifestVariables)
   588  	}
   589  
   590  	varFileOptions, err := _getVarsFileOptions(config.ManifestVariablesFiles)
   591  	if err != nil {
   592  		if e, ok := err.(*cloudfoundry.VarsFilesNotFoundError); ok {
   593  			for _, missingVarFile := range e.MissingFiles {
   594  				log.Entry().Warningf("We skip adding not-existing file '%s' as a vars-file to the cf create-service-push call", missingVarFile)
   595  			}
   596  		} else {
   597  			return "", []string{}, []string{}, errors.Wrapf(err, "Cannot prepare var-file-options: '%v'", config.ManifestVariablesFiles)
   598  		}
   599  	}
   600  
   601  	deployOptions = append(deployOptions, varOptions...)
   602  	deployOptions = append(deployOptions, varFileOptions...)
   603  
   604  	return "push", deployOptions, []string{}, nil
   605  }
   606  
   607  func toStringInterfaceMap(in *orderedmap.OrderedMap, err error) (map[string]interface{}, error) {
   608  
   609  	out := map[string]interface{}{}
   610  
   611  	if err == nil {
   612  		for _, key := range in.Keys() {
   613  			if k, ok := key.(string); ok {
   614  				val, exists := in.Get(key)
   615  				if exists {
   616  					out[k] = val
   617  				} else {
   618  					return nil, fmt.Errorf("No entry found for '%v'", key)
   619  				}
   620  			} else {
   621  				return nil, fmt.Errorf("Cannot cast key '%v' to string", key)
   622  			}
   623  		}
   624  	}
   625  
   626  	return out, err
   627  }
   628  
   629  func checkAndUpdateDeployTypeForNotSupportedManifest(config *cloudFoundryDeployOptions) (string, error) {
   630  
   631  	manifestFile, err := getManifestFileName(config)
   632  
   633  	manifestFileExists, err := fileUtils.FileExists(manifestFile)
   634  	if err != nil {
   635  		return "", err
   636  	}
   637  
   638  	if config.DeployType == "blue-green" && manifestFileExists {
   639  
   640  		manifest, _ := _getManifest(manifestFile)
   641  
   642  		apps, err := manifest.GetApplications()
   643  
   644  		if err != nil {
   645  			return "", fmt.Errorf("failed to obtain applications from manifest: %w", err)
   646  		}
   647  		if len(apps) > 1 {
   648  			return "", fmt.Errorf("Your manifest contains more than one application. For blue green deployments your manifest file may contain only one application")
   649  		}
   650  
   651  		hasNoRouteProperty, err := manifest.ApplicationHasProperty(0, "no-route")
   652  		if err != nil {
   653  			return "", errors.Wrap(err, "Failed to obtain 'no-route' property from manifest")
   654  		}
   655  		if len(apps) == 1 && hasNoRouteProperty {
   656  
   657  			const deployTypeStandard = "standard"
   658  			log.Entry().Warningf("Blue green deployment is not possible for application without route. Using deployment type '%s' instead.", deployTypeStandard)
   659  			return deployTypeStandard, nil
   660  		}
   661  	}
   662  
   663  	return config.DeployType, nil
   664  }
   665  
   666  func deployMta(config *cloudFoundryDeployOptions, mtarFilePath string, command command.ExecRunner) error {
   667  
   668  	deployCommand := "deploy"
   669  	deployParams := []string{}
   670  
   671  	if len(config.MtaDeployParameters) > 0 {
   672  		deployParams = append(deployParams, strings.Split(config.MtaDeployParameters, " ")...)
   673  	}
   674  
   675  	if config.DeployType == "bg-deploy" || config.DeployType == "blue-green" {
   676  
   677  		deployCommand = "bg-deploy"
   678  
   679  		const noConfirmFlag = "--no-confirm"
   680  		if !piperutils.ContainsString(deployParams, noConfirmFlag) {
   681  			deployParams = append(deployParams, noConfirmFlag)
   682  		}
   683  	}
   684  
   685  	cfDeployParams := []string{
   686  		deployCommand,
   687  		mtarFilePath,
   688  	}
   689  
   690  	if len(deployParams) > 0 {
   691  		cfDeployParams = append(cfDeployParams, deployParams...)
   692  	}
   693  
   694  	extFileParams, extFiles := handleMtaExtensionDescriptors(config.MtaExtensionDescriptor)
   695  
   696  	for _, extFile := range extFiles {
   697  		_, err := fileUtils.Copy(extFile, extFile+".original")
   698  		if err != nil {
   699  			return fmt.Errorf("Cannot prepare mta extension files: %w", err)
   700  		}
   701  		_, _, err = handleMtaExtensionCredentials(extFile, config.MtaExtensionCredentials)
   702  		if err != nil {
   703  			return fmt.Errorf("Cannot handle credentials inside mta extension files: %w", err)
   704  		}
   705  	}
   706  
   707  	cfDeployParams = append(cfDeployParams, extFileParams...)
   708  
   709  	err := cfDeploy(config, cfDeployParams, nil, nil, command)
   710  
   711  	for _, extFile := range extFiles {
   712  		renameError := fileUtils.FileRename(extFile+".original", extFile)
   713  		if err == nil && renameError != nil {
   714  			return renameError
   715  		}
   716  	}
   717  
   718  	return err
   719  }
   720  
   721  func handleMtaExtensionCredentials(extFile string, credentials map[string]interface{}) (updated, containsUnresolved bool, err error) {
   722  
   723  	log.Entry().Debugf("Inserting credentials into extension file '%s'", extFile)
   724  
   725  	b, err := fileUtils.FileRead(extFile)
   726  	if err != nil {
   727  		return false, false, errors.Wrapf(err, "Cannot handle credentials for mta extension file '%s'", extFile)
   728  	}
   729  	content := string(b)
   730  
   731  	env, err := toMap(_environ(), "=")
   732  	if err != nil {
   733  		return false, false, errors.Wrap(err, "Cannot handle mta extension credentials.")
   734  	}
   735  
   736  	missingCredentials := []string{}
   737  	for name, credentialKey := range credentials {
   738  		credKey, ok := credentialKey.(string)
   739  		if !ok {
   740  			return false, false, fmt.Errorf("cannot handle mta extension credentials: Cannot cast '%v' (type %T) to string", credentialKey, credentialKey)
   741  		}
   742  
   743  		const allowedVariableNamePattern = "^[-_A-Za-z0-9]+$"
   744  		alphaNumOnly := regexp.MustCompile(allowedVariableNamePattern)
   745  		if !alphaNumOnly.MatchString(name) {
   746  			return false, false, fmt.Errorf("credential key name '%s' contains unsupported character. Must contain only %s", name, allowedVariableNamePattern)
   747  		}
   748  		pattern := regexp.MustCompile("<%=\\s*" + name + "\\s*%>")
   749  		if pattern.MatchString(content) {
   750  			cred := env[toEnvVarKey(credKey)]
   751  			if len(cred) == 0 {
   752  				missingCredentials = append(missingCredentials, credKey)
   753  				continue
   754  			}
   755  			content = pattern.ReplaceAllLiteralString(content, cred)
   756  			updated = true
   757  			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)
   758  		} else {
   759  			log.Entry().Debugf("Mta extension credentials handling: Variable '%s' is not used in file '%s'", name, extFile)
   760  		}
   761  	}
   762  	if len(missingCredentials) > 0 {
   763  		missinCredsEnvVarKeyCompatible := []string{}
   764  		for _, missingKey := range missingCredentials {
   765  			missinCredsEnvVarKeyCompatible = append(missinCredsEnvVarKeyCompatible, toEnvVarKey(missingKey))
   766  		}
   767  		// ensure stable order of the entries. Needed e.g. for the tests.
   768  		sort.Strings(missingCredentials)
   769  		sort.Strings(missinCredsEnvVarKeyCompatible)
   770  		return false, false, fmt.Errorf("cannot handle mta extension credentials: No credentials found for '%s'/'%s'. Are these credentials maintained?", missingCredentials, missinCredsEnvVarKeyCompatible)
   771  	}
   772  	if !updated {
   773  		log.Entry().Debugf("Mta extension credentials handling: Extension file '%s' has not been updated. Seems to contain no credentials.", extFile)
   774  	} else {
   775  		fInfo, err := fileUtils.Stat(extFile)
   776  		fMode := fInfo.Mode()
   777  		if err != nil {
   778  			return false, false, errors.Wrap(err, "Cannot handle mta extension credentials.")
   779  		}
   780  		err = fileUtils.FileWrite(extFile, []byte(content), fMode)
   781  		if err != nil {
   782  			return false, false, errors.Wrap(err, "Cannot handle mta extension credentials.")
   783  		}
   784  		log.Entry().Debugf("Mta extension credentials handling: Extension file '%s' has been updated.", extFile)
   785  	}
   786  
   787  	re := regexp.MustCompile(`<%=.+%>`)
   788  	placeholders := re.FindAll([]byte(content), -1)
   789  	containsUnresolved = (len(placeholders) > 0)
   790  
   791  	if containsUnresolved {
   792  		log.Entry().Warningf("mta extension credential handling: Unresolved placeholders found after inserting credentials: %s", placeholders)
   793  	}
   794  
   795  	return updated, containsUnresolved, nil
   796  }
   797  
   798  func toEnvVarKey(key string) string {
   799  	key = regexp.MustCompile(`[^A-Za-z0-9]`).ReplaceAllString(key, "_")
   800  	return strings.ToUpper(regexp.MustCompile(`([a-z0-9])([A-Z])`).ReplaceAllString(key, "${1}_${2}"))
   801  }
   802  
   803  func toMap(keyValue []string, separator string) (map[string]string, error) {
   804  	result := map[string]string{}
   805  	for _, entry := range keyValue {
   806  		kv := strings.Split(entry, separator)
   807  		if len(kv) < 2 {
   808  			return map[string]string{}, fmt.Errorf("Cannot convert to map: separator '%s' not found in entry '%s'", separator, entry)
   809  		}
   810  		result[kv[0]] = strings.Join(kv[1:], separator)
   811  	}
   812  	return result, nil
   813  }
   814  
   815  func handleMtaExtensionDescriptors(mtaExtensionDescriptor string) ([]string, []string) {
   816  	var result = []string{}
   817  	var extFiles = []string{}
   818  	for _, part := range strings.Fields(strings.Trim(mtaExtensionDescriptor, " ")) {
   819  		if part == "-e" || part == "" {
   820  			continue
   821  		}
   822  		// REVISIT: maybe check if the extension descriptor exists
   823  		extFiles = append(extFiles, part)
   824  	}
   825  	if len(extFiles) > 0 {
   826  		result = append(result, "-e")
   827  		result = append(result, strings.Join(extFiles, ","))
   828  	}
   829  	return result, extFiles
   830  }
   831  
   832  func cfDeploy(
   833  	config *cloudFoundryDeployOptions,
   834  	cfDeployParams []string,
   835  	additionalEnvironment []string,
   836  	postDeployAction func(command command.ExecRunner) error,
   837  	command command.ExecRunner) error {
   838  
   839  	const cfLogFile = "cf.log"
   840  	var err error
   841  	var loginPerformed bool
   842  
   843  	additionalEnvironment = append(additionalEnvironment, "CF_TRACE="+cfLogFile)
   844  
   845  	if len(config.CfHome) > 0 {
   846  		additionalEnvironment = append(additionalEnvironment, "CF_HOME="+config.CfHome)
   847  	}
   848  
   849  	if len(config.CfPluginHome) > 0 {
   850  		additionalEnvironment = append(additionalEnvironment, "CF_PLUGIN_HOME="+config.CfPluginHome)
   851  	}
   852  
   853  	log.Entry().Infof("Using additional environment variables: %s", additionalEnvironment)
   854  
   855  	// TODO set HOME to config.DockerWorkspace
   856  	command.SetEnv(additionalEnvironment)
   857  
   858  	err = command.RunExecutable("cf", "version")
   859  
   860  	if err == nil {
   861  		err = _cfLogin(command, cloudfoundry.LoginOptions{
   862  			CfAPIEndpoint: config.APIEndpoint,
   863  			CfOrg:         config.Org,
   864  			CfSpace:       config.Space,
   865  			Username:      config.Username,
   866  			Password:      config.Password,
   867  			CfLoginOpts:   strings.Fields(config.LoginParameters),
   868  		})
   869  	}
   870  
   871  	if err == nil {
   872  		loginPerformed = true
   873  		err = command.RunExecutable("cf", []string{"plugins"}...)
   874  		if err != nil {
   875  			log.Entry().WithError(err).Errorf("Command '%s' failed.", []string{"plugins"})
   876  		}
   877  	}
   878  
   879  	if err == nil {
   880  		err = command.RunExecutable("cf", cfDeployParams...)
   881  		if err != nil {
   882  			log.Entry().WithError(err).Errorf("Command '%s' failed.", cfDeployParams)
   883  		}
   884  	}
   885  
   886  	if err == nil && postDeployAction != nil {
   887  		err = postDeployAction(command)
   888  	}
   889  
   890  	if loginPerformed {
   891  
   892  		logoutErr := _cfLogout(command)
   893  
   894  		if logoutErr != nil {
   895  			log.Entry().WithError(logoutErr).Errorf("Cannot perform cf logout")
   896  			if err == nil {
   897  				err = logoutErr
   898  			}
   899  		}
   900  	}
   901  
   902  	if err != nil || GeneralConfig.Verbose {
   903  		e := handleCfCliLog(cfLogFile)
   904  		if e != nil {
   905  			log.Entry().WithError(err).Errorf("Error reading cf log file '%s'.", cfLogFile)
   906  		}
   907  	}
   908  
   909  	return err
   910  }
   911  
   912  func findMtar() (string, error) {
   913  
   914  	const pattern = "**/*.mtar"
   915  
   916  	mtars, err := fileUtils.Glob(pattern)
   917  
   918  	if err != nil {
   919  		return "", err
   920  	}
   921  
   922  	if len(mtars) == 0 {
   923  		return "", fmt.Errorf("No mtar file matching pattern '%s' found", pattern)
   924  	}
   925  
   926  	if len(mtars) > 1 {
   927  		sMtars := []string{}
   928  		sMtars = append(sMtars, mtars...)
   929  		return "", fmt.Errorf("Found multiple mtar files matching pattern '%s' (%s), please specify file via parameter 'mtarPath'", pattern, strings.Join(sMtars, ","))
   930  	}
   931  
   932  	return mtars[0], nil
   933  }
   934  
   935  func handleCfCliLog(logFile string) error {
   936  
   937  	fExists, err := fileUtils.FileExists(logFile)
   938  
   939  	if err != nil {
   940  		return err
   941  	}
   942  
   943  	log.Entry().Info("### START OF CF CLI TRACE OUTPUT ###")
   944  
   945  	if fExists {
   946  
   947  		f, err := os.Open(logFile)
   948  
   949  		if err != nil {
   950  			return err
   951  		}
   952  
   953  		defer f.Close()
   954  
   955  		bReader := bufio.NewReader(f)
   956  		for {
   957  			line, err := bReader.ReadString('\n')
   958  			if err == nil || err == io.EOF {
   959  				// maybe inappropriate to log as info. Maybe the line from the
   960  				// log indicates an error, but that is something like a project
   961  				// standard.
   962  				log.Entry().Info(strings.TrimSuffix(line, "\n"))
   963  			}
   964  			if err != nil {
   965  				break
   966  			}
   967  		}
   968  	} else {
   969  		log.Entry().Warningf("No trace file found at '%s'", logFile)
   970  	}
   971  
   972  	log.Entry().Info("### END OF CF CLI TRACE OUTPUT ###")
   973  
   974  	return err
   975  }