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 }