package cmd import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "os" "path" "path/filepath" "strconv" "strings" "text/template" "time" "github.com/SAP/jenkins-library/pkg/build" "github.com/SAP/jenkins-library/pkg/buildsettings" "github.com/SAP/jenkins-library/pkg/npm" "github.com/SAP/jenkins-library/pkg/versioning" "github.com/SAP/jenkins-library/pkg/command" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/maven" "github.com/SAP/jenkins-library/pkg/piperutils" "github.com/SAP/jenkins-library/pkg/telemetry" "github.com/ghodss/yaml" "github.com/pkg/errors" ) const templateMtaYml = `_schema-version: "3.1" ID: "{{.ID}}" version: {{.Version}} parameters: hcp-deployer-version: "1.1.0" modules: - name: {{.ApplicationName}} type: com.sap.hcp.html5 path: . parameters: version: {{.Version}}-${timestamp} name: {{.ApplicationName}} build-parameters: builder: grunt build-result: dist` // MTABuildTarget ... type MTABuildTarget int const ( // NEO ... NEO MTABuildTarget = iota // CF ... CF MTABuildTarget = iota // XSA ... XSA MTABuildTarget = iota ) // ValueOfBuildTarget ... func ValueOfBuildTarget(str string) (MTABuildTarget, error) { switch str { case "NEO": return NEO, nil case "CF": return CF, nil case "XSA": return XSA, nil default: return -1, fmt.Errorf("unknown platform: '%s'", str) } } // String ... func (m MTABuildTarget) String() string { return [...]string{ "NEO", "CF", "XSA", }[m] } type mtaBuildUtils interface { maven.Utils SetEnv(env []string) AppendEnv(env []string) Abs(path string) (string, error) FileRead(path string) ([]byte, error) FileWrite(path string, content []byte, perm os.FileMode) error DownloadAndCopySettingsFiles(globalSettingsFile string, projectSettingsFile string) error SetNpmRegistries(defaultNpmRegistry string) error InstallAllDependencies(defaultNpmRegistry string) error Open(name string) (io.ReadWriteCloser, error) SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) } type mtaBuildUtilsBundle struct { *command.Command *piperutils.Files *piperhttp.Client } func (bundle *mtaBuildUtilsBundle) SetNpmRegistries(defaultNpmRegistry string) error { npmExecutorOptions := npm.ExecutorOptions{DefaultNpmRegistry: defaultNpmRegistry, ExecRunner: bundle} npmExecutor := npm.NewExecutor(npmExecutorOptions) return npmExecutor.SetNpmRegistries() } func (bundle *mtaBuildUtilsBundle) InstallAllDependencies(defaultNpmRegistry string) error { npmExecutorOptions := npm.ExecutorOptions{DefaultNpmRegistry: defaultNpmRegistry, ExecRunner: bundle} npmExecutor := npm.NewExecutor(npmExecutorOptions) return npmExecutor.InstallAllDependencies(npmExecutor.FindPackageJSONFiles()) } func (bundle *mtaBuildUtilsBundle) DownloadAndCopySettingsFiles(globalSettingsFile string, projectSettingsFile string) error { return maven.DownloadAndCopySettingsFiles(globalSettingsFile, projectSettingsFile, bundle) } func newMtaBuildUtilsBundle() mtaBuildUtils { utils := mtaBuildUtilsBundle{ Command: &command.Command{ StepName: "mtaBuild", }, Files: &piperutils.Files{}, Client: &piperhttp.Client{}, } utils.Stdout(log.Writer()) utils.Stderr(log.Writer()) return &utils } func mtaBuild(config mtaBuildOptions, _ *telemetry.CustomData, commonPipelineEnvironment *mtaBuildCommonPipelineEnvironment) { log.Entry().Debugf("Launching mta build") utils := newMtaBuildUtilsBundle() err := runMtaBuild(config, commonPipelineEnvironment, utils) if err != nil { log.Entry(). WithError(err). Fatal("failed to execute mta build") } } func runMtaBuild(config mtaBuildOptions, commonPipelineEnvironment *mtaBuildCommonPipelineEnvironment, utils mtaBuildUtils) error { if err := handleSettingsFiles(config, utils); err != nil { return err } if err := handleActiveProfileUpdate(config, utils); err != nil { return err } if err := utils.SetNpmRegistries(config.DefaultNpmRegistry); err != nil { return err } mtaYamlFile := filepath.Join(getSourcePath(config), "mta.yaml") mtaYamlFileExists, err := utils.FileExists(mtaYamlFile) if err != nil { return err } if !mtaYamlFileExists { if err = createMtaYamlFile(mtaYamlFile, config.ApplicationName, utils); err != nil { return err } } else { log.Entry().Infof(`"%s" file found in project sources`, mtaYamlFile) } if config.EnableSetTimestamp { if err = setTimeStamp(mtaYamlFile, utils); err != nil { return err } } mtarName, isMtarNativelySuffixed, err := getMtarName(config, mtaYamlFile, utils) if err != nil { return err } platform, err := ValueOfBuildTarget(config.Platform) if err != nil { log.SetErrorCategory(log.ErrorConfiguration) return err } call := []string{"mbt", "build", "--mtar", mtarName, "--platform", platform.String()} if len(config.Extensions) != 0 { call = append(call, fmt.Sprintf("--extensions=%s", config.Extensions)) } call = append(call, "--source", getSourcePath(config)) call = append(call, "--target", getAbsPath(getMtarFileRoot(config))) if config.CreateBOM { call = append(call, "--sbom-file-path", filepath.FromSlash("sbom-gen/bom-mta.xml")) } if config.Jobs > 0 { call = append(call, "--mode=verbose") call = append(call, "--jobs="+strconv.Itoa(config.Jobs)) } if err = addNpmBinToPath(utils); err != nil { return err } if len(config.M2Path) > 0 { absolutePath, err := utils.Abs(config.M2Path) if err != nil { return err } utils.AppendEnv([]string{"MAVEN_OPTS=-Dmaven.repo.local=" + absolutePath}) } log.Entry().Infof(`Executing mta build call: "%s"`, strings.Join(call, " ")) if err := utils.RunExecutable(call[0], call[1:]...); err != nil { log.SetErrorCategory(log.ErrorBuild) return err } log.Entry().Debugf("creating build settings information...") stepName := "mtaBuild" dockerImage, err := GetDockerImageValue(stepName) if err != nil { return err } mtaConfig := buildsettings.BuildOptions{ Profiles: config.Profiles, GlobalSettingsFile: config.GlobalSettingsFile, Publish: config.Publish, BuildSettingsInfo: config.BuildSettingsInfo, DefaultNpmRegistry: config.DefaultNpmRegistry, DockerImage: dockerImage, } buildSettingsInfo, err := buildsettings.CreateBuildSettingsInfo(&mtaConfig, stepName) if err != nil { log.Entry().Warnf("failed to create build settings info: %v", err) } commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo commonPipelineEnvironment.mtarFilePath = filepath.ToSlash(getMtarFilePath(config, mtarName)) commonPipelineEnvironment.custom.mtaBuildToolDesc = filepath.ToSlash(mtaYamlFile) if config.InstallArtifacts { if err = installMavenArtifacts(utils, config); err != nil { return err } if err = utils.InstallAllDependencies(config.DefaultNpmRegistry); err != nil { return err } } if config.Publish { if err = handlePublish(config, commonPipelineEnvironment, utils, mtarName, isMtarNativelySuffixed); err != nil { return err } } else { log.Entry().Infof("no publish detected, skipping upload of mtar artifact") } return nil } func handlePublish(config mtaBuildOptions, commonPipelineEnvironment *mtaBuildCommonPipelineEnvironment, utils mtaBuildUtils, mtarName string, isMtarNativelySuffixed bool) error { log.Entry().Infof("publish detected") if len(config.MtaDeploymentRepositoryPassword) == 0 || len(config.MtaDeploymentRepositoryUser) == 0 || len(config.MtaDeploymentRepositoryURL) == 0 { return errors.New("mtaDeploymentRepositoryUser, mtaDeploymentRepositoryPassword and mtaDeploymentRepositoryURL not found, must be present") } if len(config.MtarGroup) == 0 || len(config.Version) == 0 { return errors.New("mtarGroup, version not found and must be present") } credentialsEncoded := "Basic " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", config.MtaDeploymentRepositoryUser, config.MtaDeploymentRepositoryPassword))) headers := http.Header{} headers.Add("Authorization", credentialsEncoded) config.MtarGroup = strings.ReplaceAll(config.MtarGroup, ".", "/") mtarArtifactName := mtarName if !isMtarNativelySuffixed { mtarArtifactName = strings.TrimSuffix(mtarArtifactName, ".mtar") } config.MtaDeploymentRepositoryURL += config.MtarGroup + "/" + mtarArtifactName + "/" + config.Version + "/" + fmt.Sprintf("%v-%v.%v", mtarArtifactName, config.Version, "mtar") commonPipelineEnvironment.custom.mtarPublishedURL = config.MtaDeploymentRepositoryURL log.Entry().Infof("pushing mtar artifact to repository : %s", config.MtaDeploymentRepositoryURL) mtarPath := getMtarFilePath(config, mtarName) data, err := utils.Open(mtarPath) if err != nil { return errors.Wrap(err, "failed to open mtar archive for upload") } defer data.Close() if _, httpErr := utils.SendRequest("PUT", config.MtaDeploymentRepositoryURL, data, headers, nil); httpErr != nil { return errors.Wrap(httpErr, "failed to upload mtar to repository") } if config.CreateBuildArtifactsMetadata { if err := buildArtifactsMetadata(config, commonPipelineEnvironment, mtarPath); err != nil { log.Entry().Warnf("unable to create build artifacts metadata: %v", err) return nil } } return nil } func buildArtifactsMetadata(config mtaBuildOptions, commonPipelineEnvironment *mtaBuildCommonPipelineEnvironment, mtarPath string) error { mtarDir := filepath.Dir(mtarPath) buildArtifacts := build.BuildArtifacts{ Coordinates: []versioning.Coordinates{ { GroupID: config.MtarGroup, ArtifactID: config.MtarName, Version: config.Version, Packaging: "mtar", BuildPath: getSourcePath(config), URL: config.MtaDeploymentRepositoryURL, PURL: piperutils.GetPurl(filepath.Join(mtarDir, "sbom-gen/bom-mta.xml")), }, }, } jsonResult, err := json.Marshal(buildArtifacts) if err != nil { return fmt.Errorf("failed to marshal build artifacts: %v", err) } commonPipelineEnvironment.custom.mtaBuildArtifacts = string(jsonResult) return nil } func handleActiveProfileUpdate(config mtaBuildOptions, utils mtaBuildUtils) error { if len(config.Profiles) > 0 { return maven.UpdateActiveProfileInSettingsXML(config.Profiles, utils) } return nil } func installMavenArtifacts(utils mtaBuildUtils, config mtaBuildOptions) error { pomXMLExists, err := utils.FileExists("pom.xml") if err != nil { return err } if pomXMLExists { err = maven.InstallMavenArtifacts(&maven.EvaluateOptions{M2Path: config.M2Path}, utils) if err != nil { return err } } return nil } func addNpmBinToPath(utils mtaBuildUtils) error { dir, _ := os.Getwd() newPath := path.Join(dir, "node_modules", ".bin") oldPath := os.Getenv("PATH") if len(oldPath) > 0 { newPath = newPath + ":" + oldPath } utils.SetEnv([]string{"PATH=" + newPath}) return nil } func getMtarName(config mtaBuildOptions, mtaYamlFile string, utils mtaBuildUtils) (string, bool, error) { mtarName := config.MtarName isMtarNativelySuffixed := false if len(mtarName) == 0 { log.Entry().Debugf(`mtar name not provided via config. Extracting from file "%s"`, mtaYamlFile) mtaID, err := getMtaID(mtaYamlFile, utils) if err != nil { log.SetErrorCategory(log.ErrorConfiguration) return "", isMtarNativelySuffixed, err } if len(mtaID) == 0 { log.SetErrorCategory(log.ErrorConfiguration) return "", isMtarNativelySuffixed, fmt.Errorf("invalid mtar ID. Was empty") } log.Entry().Debugf(`mtar name extracted from file "%s": "%s"`, mtaYamlFile, mtaID) // there can be cases where the mtaId itself has the value com.myComapany.mtar , adding an extra .mtar causes .mtar.mtar if !strings.HasSuffix(mtaID, ".mtar") { mtarName = mtaID + ".mtar" } else { isMtarNativelySuffixed = true mtarName = mtaID } } return mtarName, isMtarNativelySuffixed, nil } func setTimeStamp(mtaYamlFile string, utils mtaBuildUtils) error { mtaYaml, err := utils.FileRead(mtaYamlFile) if err != nil { return err } mtaYamlStr := string(mtaYaml) timestampVar := "${timestamp}" if strings.Contains(mtaYamlStr, timestampVar) { if err := utils.FileWrite(mtaYamlFile, []byte(strings.ReplaceAll(mtaYamlStr, timestampVar, getTimestamp())), 0644); err != nil { log.SetErrorCategory(log.ErrorConfiguration) return err } log.Entry().Infof(`Timestamp replaced in "%s"`, mtaYamlFile) } else { log.Entry().Infof(`No timestamp contained in "%s". File has not been modified.`, mtaYamlFile) } return nil } func getTimestamp() string { t := time.Now() return fmt.Sprintf("%d%02d%02d%02d%02d%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) } func createMtaYamlFile(mtaYamlFile, applicationName string, utils mtaBuildUtils) error { log.Entry().Infof(`"%s" file not found in project sources`, mtaYamlFile) if len(applicationName) == 0 { return fmt.Errorf("'%[1]s' not found in project sources and 'applicationName' not provided as parameter - cannot generate '%[1]s' file", mtaYamlFile) } packageFileExists, err := utils.FileExists("package.json") if err != nil { return err } if !packageFileExists { return fmt.Errorf("package.json file does not exist") } var result map[string]interface{} pContent, err := utils.FileRead("package.json") if err != nil { return err } if err := json.Unmarshal(pContent, &result); err != nil { return fmt.Errorf("failed to unmarshal package.json: %w", err) } version, ok := result["version"].(string) if !ok { return fmt.Errorf(`version not found in "package.json" (or wrong type)`) } name, ok := result["name"].(string) if !ok { return fmt.Errorf(`name not found in "package.json" (or wrong type)`) } mtaConfig, err := generateMta(name, applicationName, version) if err != nil { return err } if err := utils.FileWrite(mtaYamlFile, []byte(mtaConfig), 0644); err != nil { return fmt.Errorf("failed to write %v: %w", mtaYamlFile, err) } log.Entry().Infof(`"%s" created.`, mtaYamlFile) return nil } func handleSettingsFiles(config mtaBuildOptions, utils mtaBuildUtils) error { return utils.DownloadAndCopySettingsFiles(config.GlobalSettingsFile, config.ProjectSettingsFile) } func generateMta(id, applicationName, version string) (string, error) { if len(id) == 0 { return "", fmt.Errorf("generating mta file: ID not provided") } if len(applicationName) == 0 { return "", fmt.Errorf("generating mta file: ApplicationName not provided") } if len(version) == 0 { return "", fmt.Errorf("generating mta file: Version not provided") } tmpl, e := template.New("mta.yaml").Parse(templateMtaYml) if e != nil { return "", e } type properties struct { ID string ApplicationName string Version string } props := properties{ID: id, ApplicationName: applicationName, Version: version} var script bytes.Buffer if err := tmpl.Execute(&script, props); err != nil { log.Entry().Warningf("failed to execute template: %v", err) } return script.String(), nil } func getMtaID(mtaYamlFile string, utils mtaBuildUtils) (string, error) { var result map[string]interface{} p, err := utils.FileRead(mtaYamlFile) if err != nil { return "", err } err = yaml.Unmarshal(p, &result) if err != nil { return "", err } id, ok := result["ID"].(string) if !ok || len(id) == 0 { return "", fmt.Errorf("id not found in mta yaml file (or wrong type)") } return id, nil } // the "source" path locates the project's root func getSourcePath(config mtaBuildOptions) string { path := config.Source if path == "" { path = "./" } return filepath.FromSlash(path) } // target defines a subfolder of the project's root func getTargetPath(config mtaBuildOptions) string { path := config.Target if path == "" { path = "./" } return filepath.FromSlash(path) } // the "mtar" path resides below the project's root // path=// func getMtarFileRoot(config mtaBuildOptions) string { sourcePath := getSourcePath(config) targetPath := getTargetPath(config) return filepath.FromSlash(filepath.Join(sourcePath, targetPath)) } func getMtarFilePath(config mtaBuildOptions, mtarName string) string { root := getMtarFileRoot(config) if root == "" || root == filepath.FromSlash("./") { return mtarName } return filepath.FromSlash(filepath.Join(root, mtarName)) } func getAbsPath(path string) string { abspath, err := filepath.Abs(path) // ignore error, pass customers path value in case of trouble if err != nil { abspath = path } return filepath.FromSlash(abspath) }