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