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  }