github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/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.Jobs > 0 {
   210  		call = append(call, "--mode=verbose")
   211  		call = append(call, "--jobs="+strconv.Itoa(config.Jobs))
   212  	}
   213  
   214  	if err = addNpmBinToPath(utils); err != nil {
   215  		return err
   216  	}
   217  
   218  	if len(config.M2Path) > 0 {
   219  		absolutePath, err := utils.Abs(config.M2Path)
   220  		if err != nil {
   221  			return err
   222  		}
   223  		utils.AppendEnv([]string{"MAVEN_OPTS=-Dmaven.repo.local=" + absolutePath})
   224  	}
   225  
   226  	log.Entry().Infof("Executing mta build call: \"%s\"", strings.Join(call, " "))
   227  
   228  	if err := utils.RunExecutable(call[0], call[1:]...); err != nil {
   229  		log.SetErrorCategory(log.ErrorBuild)
   230  		return err
   231  	}
   232  
   233  	log.Entry().Debugf("creating build settings information...")
   234  	stepName := "mtaBuild"
   235  	dockerImage, err := GetDockerImageValue(stepName)
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	mtaConfig := buildsettings.BuildOptions{
   241  		Profiles:           config.Profiles,
   242  		GlobalSettingsFile: config.GlobalSettingsFile,
   243  		Publish:            config.Publish,
   244  		BuildSettingsInfo:  config.BuildSettingsInfo,
   245  		DefaultNpmRegistry: config.DefaultNpmRegistry,
   246  		DockerImage:        dockerImage,
   247  	}
   248  	buildSettingsInfo, err := buildsettings.CreateBuildSettingsInfo(&mtaConfig, stepName)
   249  	if err != nil {
   250  		log.Entry().Warnf("failed to create build settings info: %v", err)
   251  	}
   252  	commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo
   253  
   254  	commonPipelineEnvironment.mtarFilePath = filepath.ToSlash(getMtarFilePath(config, mtarName))
   255  	commonPipelineEnvironment.custom.mtaBuildToolDesc = filepath.ToSlash(mtaYamlFile)
   256  
   257  	if config.InstallArtifacts {
   258  		// install maven artifacts in local maven repo because `mbt build` executes `mvn package -B`
   259  		err = installMavenArtifacts(utils, config)
   260  		if err != nil {
   261  			return err
   262  		}
   263  		// mta-builder executes 'npm install --production', therefore we need 'npm ci/install' to install the dev-dependencies
   264  		err = utils.InstallAllDependencies(config.DefaultNpmRegistry)
   265  		if err != nil {
   266  			return err
   267  		}
   268  	}
   269  
   270  	if config.Publish {
   271  		log.Entry().Infof("publish detected")
   272  		if (len(config.MtaDeploymentRepositoryPassword) > 0) && (len(config.MtaDeploymentRepositoryUser) > 0) &&
   273  			(len(config.MtaDeploymentRepositoryURL) > 0) {
   274  			if (len(config.MtarGroup) > 0) && (len(config.Version) > 0) {
   275  				httpClient := &piperhttp.Client{}
   276  
   277  				credentialsEncoded := "Basic " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", config.MtaDeploymentRepositoryUser, config.MtaDeploymentRepositoryPassword)))
   278  				headers := http.Header{}
   279  				headers.Add("Authorization", credentialsEncoded)
   280  
   281  				config.MtarGroup = strings.ReplaceAll(config.MtarGroup, ".", "/")
   282  
   283  				mtarArtifactName := mtarName
   284  
   285  				// only trim the .mtar suffix from the mtarName
   286  				if !isMtarNativelySuffixed {
   287  					mtarArtifactName = strings.TrimSuffix(mtarArtifactName, ".mtar")
   288  				}
   289  
   290  				config.MtaDeploymentRepositoryURL += config.MtarGroup + "/" + mtarArtifactName + "/" + config.Version + "/" + fmt.Sprintf("%v-%v.%v", mtarArtifactName, config.Version, "mtar")
   291  
   292  				commonPipelineEnvironment.custom.mtarPublishedURL = config.MtaDeploymentRepositoryURL
   293  
   294  				log.Entry().Infof("pushing mtar artifact to repository : %s", config.MtaDeploymentRepositoryURL)
   295  
   296  				data, err := os.Open(getMtarFilePath(config, mtarName))
   297  				if err != nil {
   298  					return errors.Wrap(err, "failed to open mtar archive for upload")
   299  				}
   300  				_, httpErr := httpClient.SendRequest("PUT", config.MtaDeploymentRepositoryURL, data, headers, nil)
   301  
   302  				if httpErr != nil {
   303  					return errors.Wrap(err, "failed to upload mtar to repository")
   304  				}
   305  			} else {
   306  				return errors.New("mtarGroup, version not found and must be present")
   307  
   308  			}
   309  
   310  		} else {
   311  			return errors.New("mtaDeploymentRepositoryUser, mtaDeploymentRepositoryPassword and mtaDeploymentRepositoryURL not found , must be present")
   312  		}
   313  	} else {
   314  		log.Entry().Infof("no publish detected, skipping upload of mtar artifact")
   315  	}
   316  	return err
   317  }
   318  
   319  func handleActiveProfileUpdate(config mtaBuildOptions, utils mtaBuildUtils) error {
   320  	if len(config.Profiles) > 0 {
   321  		return maven.UpdateActiveProfileInSettingsXML(config.Profiles, utils)
   322  	}
   323  	return nil
   324  }
   325  
   326  func installMavenArtifacts(utils mtaBuildUtils, config mtaBuildOptions) error {
   327  	pomXMLExists, err := utils.FileExists("pom.xml")
   328  	if err != nil {
   329  		return err
   330  	}
   331  	if pomXMLExists {
   332  		err = maven.InstallMavenArtifacts(&maven.EvaluateOptions{M2Path: config.M2Path}, utils)
   333  		if err != nil {
   334  			return err
   335  		}
   336  	}
   337  	return nil
   338  }
   339  
   340  func addNpmBinToPath(utils mtaBuildUtils) error {
   341  	dir, _ := os.Getwd()
   342  	newPath := path.Join(dir, "node_modules", ".bin")
   343  	oldPath := os.Getenv("PATH")
   344  	if len(oldPath) > 0 {
   345  		newPath = newPath + ":" + oldPath
   346  	}
   347  	utils.SetEnv([]string{"PATH=" + newPath})
   348  	return nil
   349  }
   350  
   351  func getMtarName(config mtaBuildOptions, mtaYamlFile string, utils mtaBuildUtils) (string, bool, error) {
   352  
   353  	mtarName := config.MtarName
   354  	isMtarNativelySuffixed := false
   355  	if len(mtarName) == 0 {
   356  
   357  		log.Entry().Debugf("mtar name not provided via config. Extracting from file \"%s\"", mtaYamlFile)
   358  
   359  		mtaID, err := getMtaID(mtaYamlFile, utils)
   360  
   361  		if err != nil {
   362  			log.SetErrorCategory(log.ErrorConfiguration)
   363  			return "", isMtarNativelySuffixed, err
   364  		}
   365  
   366  		if len(mtaID) == 0 {
   367  			log.SetErrorCategory(log.ErrorConfiguration)
   368  			return "", isMtarNativelySuffixed, fmt.Errorf("Invalid mtar ID. Was empty")
   369  		}
   370  
   371  		log.Entry().Debugf("mtar name extracted from file \"%s\": \"%s\"", mtaYamlFile, mtaID)
   372  
   373  		// there can be cases where the mtaId itself has the value com.myComapany.mtar , adding an extra .mtar causes .mtar.mtar
   374  		if !strings.HasSuffix(mtaID, ".mtar") {
   375  			mtarName = mtaID + ".mtar"
   376  		} else {
   377  			isMtarNativelySuffixed = true
   378  			mtarName = mtaID
   379  		}
   380  
   381  	}
   382  
   383  	return mtarName, isMtarNativelySuffixed, nil
   384  
   385  }
   386  
   387  func setTimeStamp(mtaYamlFile string, utils mtaBuildUtils) error {
   388  
   389  	mtaYaml, err := utils.FileRead(mtaYamlFile)
   390  	if err != nil {
   391  		return err
   392  	}
   393  
   394  	mtaYamlStr := string(mtaYaml)
   395  
   396  	timestampVar := "${timestamp}"
   397  	if strings.Contains(mtaYamlStr, timestampVar) {
   398  
   399  		if err := utils.FileWrite(mtaYamlFile, []byte(strings.ReplaceAll(mtaYamlStr, timestampVar, getTimestamp())), 0644); err != nil {
   400  			log.SetErrorCategory(log.ErrorConfiguration)
   401  			return err
   402  		}
   403  		log.Entry().Infof("Timestamp replaced in \"%s\"", mtaYamlFile)
   404  	} else {
   405  		log.Entry().Infof("No timestamp contained in \"%s\". File has not been modified.", mtaYamlFile)
   406  	}
   407  
   408  	return nil
   409  }
   410  
   411  func getTimestamp() string {
   412  	t := time.Now()
   413  	return fmt.Sprintf("%d%02d%02d%02d%02d%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
   414  }
   415  
   416  func createMtaYamlFile(mtaYamlFile, applicationName string, utils mtaBuildUtils) error {
   417  
   418  	log.Entry().Infof("\"%s\" file not found in project sources", mtaYamlFile)
   419  
   420  	if len(applicationName) == 0 {
   421  		return fmt.Errorf("'%[1]s' not found in project sources and 'applicationName' not provided as parameter - cannot generate '%[1]s' file", mtaYamlFile)
   422  	}
   423  
   424  	packageFileExists, err := utils.FileExists("package.json")
   425  	if !packageFileExists {
   426  		return fmt.Errorf("package.json file does not exist")
   427  	}
   428  
   429  	var result map[string]interface{}
   430  	pContent, err := utils.FileRead("package.json")
   431  	if err != nil {
   432  		return err
   433  	}
   434  	json.Unmarshal(pContent, &result)
   435  
   436  	version, ok := result["version"].(string)
   437  	if !ok {
   438  		return fmt.Errorf("Version not found in \"package.json\" (or wrong type)")
   439  	}
   440  
   441  	name, ok := result["name"].(string)
   442  	if !ok {
   443  		return fmt.Errorf("Name not found in \"package.json\" (or wrong type)")
   444  	}
   445  
   446  	mtaConfig, err := generateMta(name, applicationName, version)
   447  	if err != nil {
   448  		return err
   449  	}
   450  
   451  	if err := utils.FileWrite(mtaYamlFile, []byte(mtaConfig), 0644); err != nil {
   452  		return fmt.Errorf("failed to write %v: %w", mtaYamlFile, err)
   453  	}
   454  	log.Entry().Infof("\"%s\" created.", mtaYamlFile)
   455  
   456  	return nil
   457  }
   458  
   459  func handleSettingsFiles(config mtaBuildOptions, utils mtaBuildUtils) error {
   460  	return utils.DownloadAndCopySettingsFiles(config.GlobalSettingsFile, config.ProjectSettingsFile)
   461  }
   462  
   463  func generateMta(id, applicationName, version string) (string, error) {
   464  
   465  	if len(id) == 0 {
   466  		return "", fmt.Errorf("Generating mta file: ID not provided")
   467  	}
   468  	if len(applicationName) == 0 {
   469  		return "", fmt.Errorf("Generating mta file: ApplicationName not provided")
   470  	}
   471  	if len(version) == 0 {
   472  		return "", fmt.Errorf("Generating mta file: Version not provided")
   473  	}
   474  
   475  	tmpl, e := template.New("mta.yaml").Parse(templateMtaYml)
   476  	if e != nil {
   477  		return "", e
   478  	}
   479  
   480  	type properties struct {
   481  		ID              string
   482  		ApplicationName string
   483  		Version         string
   484  	}
   485  
   486  	props := properties{ID: id, ApplicationName: applicationName, Version: version}
   487  
   488  	var script bytes.Buffer
   489  	if err := tmpl.Execute(&script, props); err != nil {
   490  		log.Entry().Warningf("failed to execute template: %v", err)
   491  	}
   492  	return script.String(), nil
   493  }
   494  
   495  func getMtaID(mtaYamlFile string, utils mtaBuildUtils) (string, error) {
   496  
   497  	var result map[string]interface{}
   498  	p, err := utils.FileRead(mtaYamlFile)
   499  	if err != nil {
   500  		return "", err
   501  	}
   502  	err = yaml.Unmarshal(p, &result)
   503  	if err != nil {
   504  		return "", err
   505  	}
   506  
   507  	id, ok := result["ID"].(string)
   508  	if !ok || len(id) == 0 {
   509  		return "", fmt.Errorf("Id not found in mta yaml file (or wrong type)")
   510  	}
   511  
   512  	return id, nil
   513  }
   514  
   515  // the "source" path locates the project's root
   516  func getSourcePath(config mtaBuildOptions) string {
   517  	path := config.Source
   518  	if path == "" {
   519  		path = "./"
   520  	}
   521  	return filepath.FromSlash(path)
   522  }
   523  
   524  // target defines a subfolder of the project's root
   525  func getTargetPath(config mtaBuildOptions) string {
   526  	path := config.Target
   527  	if path == "" {
   528  		path = "./"
   529  	}
   530  	return filepath.FromSlash(path)
   531  }
   532  
   533  // the "mtar" path resides below the project's root
   534  // path=<config.source>/<config.target>/<mtarname>
   535  func getMtarFileRoot(config mtaBuildOptions) string {
   536  	sourcePath := getSourcePath(config)
   537  	targetPath := getTargetPath(config)
   538  
   539  	return filepath.FromSlash(filepath.Join(sourcePath, targetPath))
   540  }
   541  
   542  func getMtarFilePath(config mtaBuildOptions, mtarName string) string {
   543  	root := getMtarFileRoot(config)
   544  
   545  	if root == "" || root == filepath.FromSlash("./") {
   546  		return mtarName
   547  	}
   548  
   549  	return filepath.FromSlash(filepath.Join(root, mtarName))
   550  }
   551  
   552  func getAbsPath(path string) string {
   553  	abspath, err := filepath.Abs(path)
   554  	// ignore error, pass customers path value in case of trouble
   555  	if err != nil {
   556  		abspath = path
   557  	}
   558  	return filepath.FromSlash(abspath)
   559  }