github.com/xgoffin/jenkins-library@v1.154.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 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 }