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 }