github.com/SAP/jenkins-library@v1.362.0/cmd/mtaBuild.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "os" 10 "path" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "text/template" 15 "time" 16 17 "github.com/SAP/jenkins-library/pkg/buildsettings" 18 "github.com/SAP/jenkins-library/pkg/npm" 19 20 "github.com/SAP/jenkins-library/pkg/command" 21 piperhttp "github.com/SAP/jenkins-library/pkg/http" 22 "github.com/SAP/jenkins-library/pkg/log" 23 "github.com/SAP/jenkins-library/pkg/maven" 24 "github.com/SAP/jenkins-library/pkg/piperutils" 25 "github.com/SAP/jenkins-library/pkg/telemetry" 26 "github.com/ghodss/yaml" 27 "github.com/pkg/errors" 28 ) 29 30 const templateMtaYml = `_schema-version: "3.1" 31 ID: "{{.ID}}" 32 version: {{.Version}} 33 34 parameters: 35 hcp-deployer-version: "1.1.0" 36 37 modules: 38 - name: {{.ApplicationName}} 39 type: com.sap.hcp.html5 40 path: . 41 parameters: 42 version: {{.Version}}-${timestamp} 43 name: {{.ApplicationName}} 44 build-parameters: 45 builder: grunt 46 build-result: dist` 47 48 // MTABuildTarget ... 49 type MTABuildTarget int 50 51 const ( 52 // NEO ... 53 NEO MTABuildTarget = iota 54 // CF ... 55 CF MTABuildTarget = iota 56 //XSA ... 57 XSA MTABuildTarget = iota 58 ) 59 60 // ValueOfBuildTarget ... 61 func ValueOfBuildTarget(str string) (MTABuildTarget, error) { 62 switch str { 63 case "NEO": 64 return NEO, nil 65 case "CF": 66 return CF, nil 67 case "XSA": 68 return XSA, nil 69 default: 70 return -1, fmt.Errorf("Unknown Platform: '%s'", str) 71 } 72 } 73 74 // String ... 75 func (m MTABuildTarget) String() string { 76 return [...]string{ 77 "NEO", 78 "CF", 79 "XSA", 80 }[m] 81 } 82 83 type mtaBuildUtils interface { 84 maven.Utils 85 86 SetEnv(env []string) 87 AppendEnv(env []string) 88 89 Abs(path string) (string, error) 90 FileRead(path string) ([]byte, error) 91 FileWrite(path string, content []byte, perm os.FileMode) error 92 93 DownloadAndCopySettingsFiles(globalSettingsFile string, projectSettingsFile string) error 94 95 SetNpmRegistries(defaultNpmRegistry string) error 96 InstallAllDependencies(defaultNpmRegistry string) error 97 } 98 99 type mtaBuildUtilsBundle struct { 100 *command.Command 101 *piperutils.Files 102 *piperhttp.Client 103 } 104 105 func (bundle *mtaBuildUtilsBundle) SetNpmRegistries(defaultNpmRegistry string) error { 106 npmExecutorOptions := npm.ExecutorOptions{DefaultNpmRegistry: defaultNpmRegistry, ExecRunner: bundle} 107 npmExecutor := npm.NewExecutor(npmExecutorOptions) 108 return npmExecutor.SetNpmRegistries() 109 } 110 111 func (bundle *mtaBuildUtilsBundle) InstallAllDependencies(defaultNpmRegistry string) error { 112 npmExecutorOptions := npm.ExecutorOptions{DefaultNpmRegistry: defaultNpmRegistry, ExecRunner: bundle} 113 npmExecutor := npm.NewExecutor(npmExecutorOptions) 114 return npmExecutor.InstallAllDependencies(npmExecutor.FindPackageJSONFiles()) 115 } 116 117 func (bundle *mtaBuildUtilsBundle) DownloadAndCopySettingsFiles(globalSettingsFile string, projectSettingsFile string) error { 118 return maven.DownloadAndCopySettingsFiles(globalSettingsFile, projectSettingsFile, bundle) 119 } 120 121 func newMtaBuildUtilsBundle() mtaBuildUtils { 122 utils := mtaBuildUtilsBundle{ 123 Command: &command.Command{ 124 StepName: "mtaBuild", 125 }, 126 Files: &piperutils.Files{}, 127 Client: &piperhttp.Client{}, 128 } 129 utils.Stdout(log.Writer()) 130 utils.Stderr(log.Writer()) 131 return &utils 132 } 133 134 func mtaBuild(config mtaBuildOptions, 135 telemetryData *telemetry.CustomData, 136 commonPipelineEnvironment *mtaBuildCommonPipelineEnvironment) { 137 log.Entry().Debugf("Launching mta build") 138 utils := newMtaBuildUtilsBundle() 139 140 err := runMtaBuild(config, commonPipelineEnvironment, utils) 141 if err != nil { 142 log.Entry(). 143 WithError(err). 144 Fatal("failed to execute mta build") 145 } 146 } 147 148 func runMtaBuild(config mtaBuildOptions, 149 commonPipelineEnvironment *mtaBuildCommonPipelineEnvironment, 150 utils mtaBuildUtils) error { 151 152 var err error 153 154 err = handleSettingsFiles(config, utils) 155 if err != nil { 156 return err 157 } 158 159 err = handleActiveProfileUpdate(config, utils) 160 if err != nil { 161 return err 162 } 163 164 err = utils.SetNpmRegistries(config.DefaultNpmRegistry) 165 166 mtaYamlFile := filepath.Join(getSourcePath(config), "mta.yaml") 167 mtaYamlFileExists, err := utils.FileExists(mtaYamlFile) 168 169 if err != nil { 170 return err 171 } 172 173 if !mtaYamlFileExists { 174 175 if err = createMtaYamlFile(mtaYamlFile, config.ApplicationName, utils); err != nil { 176 return err 177 } 178 179 } else { 180 log.Entry().Infof("\"%s\" file found in project sources", mtaYamlFile) 181 } 182 183 if err = setTimeStamp(mtaYamlFile, utils); err != nil { 184 return err 185 } 186 187 mtarName, isMtarNativelySuffixed, err := getMtarName(config, mtaYamlFile, utils) 188 189 if err != nil { 190 return err 191 } 192 193 var call []string 194 195 platform, err := ValueOfBuildTarget(config.Platform) 196 if err != nil { 197 log.SetErrorCategory(log.ErrorConfiguration) 198 return err 199 } 200 201 call = append(call, "mbt", "build", "--mtar", mtarName, "--platform", platform.String()) 202 if len(config.Extensions) != 0 { 203 call = append(call, fmt.Sprintf("--extensions=%s", config.Extensions)) 204 } 205 206 call = append(call, "--source", getSourcePath(config)) 207 call = append(call, "--target", getAbsPath(getMtarFileRoot(config))) 208 209 if config.CreateBOM { 210 call = append(call, "--sbom-file-path", filepath.FromSlash("sbom-gen/bom-mta.xml")) 211 } 212 213 if config.Jobs > 0 { 214 call = append(call, "--mode=verbose") 215 call = append(call, "--jobs="+strconv.Itoa(config.Jobs)) 216 } 217 218 if err = addNpmBinToPath(utils); err != nil { 219 return err 220 } 221 222 if len(config.M2Path) > 0 { 223 absolutePath, err := utils.Abs(config.M2Path) 224 if err != nil { 225 return err 226 } 227 utils.AppendEnv([]string{"MAVEN_OPTS=-Dmaven.repo.local=" + absolutePath}) 228 } 229 230 log.Entry().Infof("Executing mta build call: \"%s\"", strings.Join(call, " ")) 231 232 if err := utils.RunExecutable(call[0], call[1:]...); err != nil { 233 log.SetErrorCategory(log.ErrorBuild) 234 return err 235 } 236 237 log.Entry().Debugf("creating build settings information...") 238 stepName := "mtaBuild" 239 dockerImage, err := GetDockerImageValue(stepName) 240 if err != nil { 241 return err 242 } 243 244 mtaConfig := buildsettings.BuildOptions{ 245 Profiles: config.Profiles, 246 GlobalSettingsFile: config.GlobalSettingsFile, 247 Publish: config.Publish, 248 BuildSettingsInfo: config.BuildSettingsInfo, 249 DefaultNpmRegistry: config.DefaultNpmRegistry, 250 DockerImage: dockerImage, 251 } 252 buildSettingsInfo, err := buildsettings.CreateBuildSettingsInfo(&mtaConfig, stepName) 253 if err != nil { 254 log.Entry().Warnf("failed to create build settings info: %v", err) 255 } 256 commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo 257 258 commonPipelineEnvironment.mtarFilePath = filepath.ToSlash(getMtarFilePath(config, mtarName)) 259 commonPipelineEnvironment.custom.mtaBuildToolDesc = filepath.ToSlash(mtaYamlFile) 260 261 if config.InstallArtifacts { 262 // install maven artifacts in local maven repo because `mbt build` executes `mvn package -B` 263 err = installMavenArtifacts(utils, config) 264 if err != nil { 265 return err 266 } 267 // mta-builder executes 'npm install --production', therefore we need 'npm ci/install' to install the dev-dependencies 268 err = utils.InstallAllDependencies(config.DefaultNpmRegistry) 269 if err != nil { 270 return err 271 } 272 } 273 274 if config.Publish { 275 log.Entry().Infof("publish detected") 276 if (len(config.MtaDeploymentRepositoryPassword) > 0) && (len(config.MtaDeploymentRepositoryUser) > 0) && 277 (len(config.MtaDeploymentRepositoryURL) > 0) { 278 if (len(config.MtarGroup) > 0) && (len(config.Version) > 0) { 279 httpClient := &piperhttp.Client{} 280 281 credentialsEncoded := "Basic " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", config.MtaDeploymentRepositoryUser, config.MtaDeploymentRepositoryPassword))) 282 headers := http.Header{} 283 headers.Add("Authorization", credentialsEncoded) 284 285 config.MtarGroup = strings.ReplaceAll(config.MtarGroup, ".", "/") 286 287 mtarArtifactName := mtarName 288 289 // only trim the .mtar suffix from the mtarName 290 if !isMtarNativelySuffixed { 291 mtarArtifactName = strings.TrimSuffix(mtarArtifactName, ".mtar") 292 } 293 294 config.MtaDeploymentRepositoryURL += config.MtarGroup + "/" + mtarArtifactName + "/" + config.Version + "/" + fmt.Sprintf("%v-%v.%v", mtarArtifactName, config.Version, "mtar") 295 296 commonPipelineEnvironment.custom.mtarPublishedURL = config.MtaDeploymentRepositoryURL 297 298 log.Entry().Infof("pushing mtar artifact to repository : %s", config.MtaDeploymentRepositoryURL) 299 300 data, err := os.Open(getMtarFilePath(config, mtarName)) 301 if err != nil { 302 return errors.Wrap(err, "failed to open mtar archive for upload") 303 } 304 _, httpErr := httpClient.SendRequest("PUT", config.MtaDeploymentRepositoryURL, data, headers, nil) 305 306 if httpErr != nil { 307 return errors.Wrap(err, "failed to upload mtar to repository") 308 } 309 } else { 310 return errors.New("mtarGroup, version not found and must be present") 311 312 } 313 314 } else { 315 return errors.New("mtaDeploymentRepositoryUser, mtaDeploymentRepositoryPassword and mtaDeploymentRepositoryURL not found , must be present") 316 } 317 } else { 318 log.Entry().Infof("no publish detected, skipping upload of mtar artifact") 319 } 320 return err 321 } 322 323 func handleActiveProfileUpdate(config mtaBuildOptions, utils mtaBuildUtils) error { 324 if len(config.Profiles) > 0 { 325 return maven.UpdateActiveProfileInSettingsXML(config.Profiles, utils) 326 } 327 return nil 328 } 329 330 func installMavenArtifacts(utils mtaBuildUtils, config mtaBuildOptions) error { 331 pomXMLExists, err := utils.FileExists("pom.xml") 332 if err != nil { 333 return err 334 } 335 if pomXMLExists { 336 err = maven.InstallMavenArtifacts(&maven.EvaluateOptions{M2Path: config.M2Path}, utils) 337 if err != nil { 338 return err 339 } 340 } 341 return nil 342 } 343 344 func addNpmBinToPath(utils mtaBuildUtils) error { 345 dir, _ := os.Getwd() 346 newPath := path.Join(dir, "node_modules", ".bin") 347 oldPath := os.Getenv("PATH") 348 if len(oldPath) > 0 { 349 newPath = newPath + ":" + oldPath 350 } 351 utils.SetEnv([]string{"PATH=" + newPath}) 352 return nil 353 } 354 355 func getMtarName(config mtaBuildOptions, mtaYamlFile string, utils mtaBuildUtils) (string, bool, error) { 356 357 mtarName := config.MtarName 358 isMtarNativelySuffixed := false 359 if len(mtarName) == 0 { 360 361 log.Entry().Debugf("mtar name not provided via config. Extracting from file \"%s\"", mtaYamlFile) 362 363 mtaID, err := getMtaID(mtaYamlFile, utils) 364 365 if err != nil { 366 log.SetErrorCategory(log.ErrorConfiguration) 367 return "", isMtarNativelySuffixed, err 368 } 369 370 if len(mtaID) == 0 { 371 log.SetErrorCategory(log.ErrorConfiguration) 372 return "", isMtarNativelySuffixed, fmt.Errorf("Invalid mtar ID. Was empty") 373 } 374 375 log.Entry().Debugf("mtar name extracted from file \"%s\": \"%s\"", mtaYamlFile, mtaID) 376 377 // there can be cases where the mtaId itself has the value com.myComapany.mtar , adding an extra .mtar causes .mtar.mtar 378 if !strings.HasSuffix(mtaID, ".mtar") { 379 mtarName = mtaID + ".mtar" 380 } else { 381 isMtarNativelySuffixed = true 382 mtarName = mtaID 383 } 384 385 } 386 387 return mtarName, isMtarNativelySuffixed, nil 388 389 } 390 391 func setTimeStamp(mtaYamlFile string, utils mtaBuildUtils) error { 392 393 mtaYaml, err := utils.FileRead(mtaYamlFile) 394 if err != nil { 395 return err 396 } 397 398 mtaYamlStr := string(mtaYaml) 399 400 timestampVar := "${timestamp}" 401 if strings.Contains(mtaYamlStr, timestampVar) { 402 403 if err := utils.FileWrite(mtaYamlFile, []byte(strings.ReplaceAll(mtaYamlStr, timestampVar, getTimestamp())), 0644); err != nil { 404 log.SetErrorCategory(log.ErrorConfiguration) 405 return err 406 } 407 log.Entry().Infof("Timestamp replaced in \"%s\"", mtaYamlFile) 408 } else { 409 log.Entry().Infof("No timestamp contained in \"%s\". File has not been modified.", mtaYamlFile) 410 } 411 412 return nil 413 } 414 415 func getTimestamp() string { 416 t := time.Now() 417 return fmt.Sprintf("%d%02d%02d%02d%02d%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) 418 } 419 420 func createMtaYamlFile(mtaYamlFile, applicationName string, utils mtaBuildUtils) error { 421 422 log.Entry().Infof("\"%s\" file not found in project sources", mtaYamlFile) 423 424 if len(applicationName) == 0 { 425 return fmt.Errorf("'%[1]s' not found in project sources and 'applicationName' not provided as parameter - cannot generate '%[1]s' file", mtaYamlFile) 426 } 427 428 packageFileExists, err := utils.FileExists("package.json") 429 if !packageFileExists { 430 return fmt.Errorf("package.json file does not exist") 431 } 432 433 var result map[string]interface{} 434 pContent, err := utils.FileRead("package.json") 435 if err != nil { 436 return err 437 } 438 json.Unmarshal(pContent, &result) 439 440 version, ok := result["version"].(string) 441 if !ok { 442 return fmt.Errorf("Version not found in \"package.json\" (or wrong type)") 443 } 444 445 name, ok := result["name"].(string) 446 if !ok { 447 return fmt.Errorf("Name not found in \"package.json\" (or wrong type)") 448 } 449 450 mtaConfig, err := generateMta(name, applicationName, version) 451 if err != nil { 452 return err 453 } 454 455 if err := utils.FileWrite(mtaYamlFile, []byte(mtaConfig), 0644); err != nil { 456 return fmt.Errorf("failed to write %v: %w", mtaYamlFile, err) 457 } 458 log.Entry().Infof("\"%s\" created.", mtaYamlFile) 459 460 return nil 461 } 462 463 func handleSettingsFiles(config mtaBuildOptions, utils mtaBuildUtils) error { 464 return utils.DownloadAndCopySettingsFiles(config.GlobalSettingsFile, config.ProjectSettingsFile) 465 } 466 467 func generateMta(id, applicationName, version string) (string, error) { 468 469 if len(id) == 0 { 470 return "", fmt.Errorf("Generating mta file: ID not provided") 471 } 472 if len(applicationName) == 0 { 473 return "", fmt.Errorf("Generating mta file: ApplicationName not provided") 474 } 475 if len(version) == 0 { 476 return "", fmt.Errorf("Generating mta file: Version not provided") 477 } 478 479 tmpl, e := template.New("mta.yaml").Parse(templateMtaYml) 480 if e != nil { 481 return "", e 482 } 483 484 type properties struct { 485 ID string 486 ApplicationName string 487 Version string 488 } 489 490 props := properties{ID: id, ApplicationName: applicationName, Version: version} 491 492 var script bytes.Buffer 493 if err := tmpl.Execute(&script, props); err != nil { 494 log.Entry().Warningf("failed to execute template: %v", err) 495 } 496 return script.String(), nil 497 } 498 499 func getMtaID(mtaYamlFile string, utils mtaBuildUtils) (string, error) { 500 501 var result map[string]interface{} 502 p, err := utils.FileRead(mtaYamlFile) 503 if err != nil { 504 return "", err 505 } 506 err = yaml.Unmarshal(p, &result) 507 if err != nil { 508 return "", err 509 } 510 511 id, ok := result["ID"].(string) 512 if !ok || len(id) == 0 { 513 return "", fmt.Errorf("Id not found in mta yaml file (or wrong type)") 514 } 515 516 return id, nil 517 } 518 519 // the "source" path locates the project's root 520 func getSourcePath(config mtaBuildOptions) string { 521 path := config.Source 522 if path == "" { 523 path = "./" 524 } 525 return filepath.FromSlash(path) 526 } 527 528 // target defines a subfolder of the project's root 529 func getTargetPath(config mtaBuildOptions) string { 530 path := config.Target 531 if path == "" { 532 path = "./" 533 } 534 return filepath.FromSlash(path) 535 } 536 537 // the "mtar" path resides below the project's root 538 // path=<config.source>/<config.target>/<mtarname> 539 func getMtarFileRoot(config mtaBuildOptions) string { 540 sourcePath := getSourcePath(config) 541 targetPath := getTargetPath(config) 542 543 return filepath.FromSlash(filepath.Join(sourcePath, targetPath)) 544 } 545 546 func getMtarFilePath(config mtaBuildOptions, mtarName string) string { 547 root := getMtarFileRoot(config) 548 549 if root == "" || root == filepath.FromSlash("./") { 550 return mtarName 551 } 552 553 return filepath.FromSlash(filepath.Join(root, mtarName)) 554 } 555 556 func getAbsPath(path string) string { 557 abspath, err := filepath.Abs(path) 558 // ignore error, pass customers path value in case of trouble 559 if err != nil { 560 abspath = path 561 } 562 return filepath.FromSlash(abspath) 563 }