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