1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-10-30 23:57:50 +02:00

go: mta build (#1128)

move mtaBuild step from groovy to go.

Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>
This commit is contained in:
Marcus Holl
2020-02-25 14:33:34 +01:00
committed by GitHub
parent d1e5d9c2e7
commit 190a4708a2
9 changed files with 1110 additions and 44 deletions

390
cmd/mtaBuild.go Normal file
View File

@@ -0,0 +1,390 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"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"
"gopkg.in/yaml.v2"
"os"
"strings"
"text/template"
"time"
)
const templateMtaYml = `_schema-version: "3.1"
ID: "{{.ID}}"
version: {{.Version}}
parameters:
hcp-deployer-version: "1.1.0"
modules:
- name: {{.ApplicationName}}
type: html5
path: .
parameters:
version: {{.Version}}-${timestamp}
name: {{.ApplicationName}}
build-parameters:
builder: grunt
build-result: dist`
// for mocking
var getSettingsFile = maven.GetSettingsFile
// 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 BuildTarget/Platform: '%s'", str)
}
}
// String ...
func (m MTABuildTarget) String() string {
return [...]string{
"NEO",
"CF",
"XSA",
}[m]
}
func mtaBuild(config mtaBuildOptions,
telemetryData *telemetry.CustomData,
commonPipelineEnvironment *mtaBuildCommonPipelineEnvironment) {
log.Entry().Debugf("Launching mta build")
files := piperutils.Files{}
httpClient := piperhttp.Client{}
err := runMtaBuild(config, commonPipelineEnvironment, &command.Command{}, &files, &httpClient)
if err != nil {
log.Entry().
WithError(err).
Fatal("failed to execute mta build")
}
}
func runMtaBuild(config mtaBuildOptions,
commonPipelineEnvironment *mtaBuildCommonPipelineEnvironment,
e envExecRunner,
p piperutils.FileUtils,
httpClient piperhttp.Downloader) error {
e.Stdout(log.Entry().Writer()) // not sure if using the logging framework here is a suitable approach. We handover already log formatted
e.Stderr(log.Entry().Writer()) // entries to a logging framwork again. But this is considered to be some kind of project standard.
var err error
handleSettingsFiles(config, p, httpClient)
handleDefaultNpmRegistry(config, e)
mtaYamlFile := "mta.yaml"
mtaYamlFileExists, err := p.FileExists(mtaYamlFile)
if err != nil {
return err
}
if !mtaYamlFileExists {
if err = createMtaYamlFile(mtaYamlFile, config.ApplicationName, p); err != nil {
return err
}
} else {
log.Entry().Infof("\"%s\" file found in project sources", mtaYamlFile)
}
if err = setTimeStamp(mtaYamlFile, p); err != nil {
return err
}
mtarName, err := getMtarName(config, mtaYamlFile, p)
if err != nil {
return err
}
var call []string
switch config.MtaBuildTool {
case "classic":
mtaJar := getMarJarName(config)
buildTarget, err := ValueOfBuildTarget(config.BuildTarget)
if err != nil {
return err
}
call = append(call, "java", "-jar", mtaJar, "--mtar", mtarName, fmt.Sprintf("--build-target=%s", buildTarget), "build")
if len(config.Extensions) != 0 {
call = append(call, fmt.Sprintf("--extension=%s", config.Extensions))
}
case "cloudMbt":
platform, err := ValueOfBuildTarget(config.Platform)
if err != nil {
return err
}
call = append(call, "mbt", "build", "--mtar", mtarName, "--platform", platform.String())
if len(config.Extensions) != 0 {
call = append(call, fmt.Sprintf("--extensions=%s", config.Extensions))
}
call = append(call, "--target", "./")
default:
return fmt.Errorf("Unknown mta build tool: \"%s\"", config.MtaBuildTool)
}
if err = addNpmBinToPath(e); err != nil {
return err
}
log.Entry().Infof("Executing mta build call: \"%s\"", strings.Join(call, " "))
if err := e.RunExecutable(call[0], call[1:]...); err != nil {
return err
}
commonPipelineEnvironment.mtarFilePath = mtarName
return nil
}
func getMarJarName(config mtaBuildOptions) string {
mtaJar := "mta.jar"
if len(config.MtaJarLocation) > 0 {
mtaJar = config.MtaJarLocation
}
return mtaJar
}
func addNpmBinToPath(e envExecRunner) error {
path := "./node_modules/.bin"
oldPath := os.Getenv("PATH")
if len(oldPath) > 0 {
path = path + ":" + oldPath
}
e.Env(append(os.Environ(), "PATH="+path))
return nil
}
func getMtarName(config mtaBuildOptions, mtaYamlFile string, p piperutils.FileUtils) (string, error) {
mtarName := config.MtarName
if len(mtarName) == 0 {
log.Entry().Debugf("mtar name not provided via config. Extracting from file \"%s\"", mtaYamlFile)
mtaID, err := getMtaID(mtaYamlFile, p)
if err != nil {
return "", err
}
if len(mtaID) == 0 {
return "", fmt.Errorf("Invalid mtar name. Was empty")
}
log.Entry().Debugf("mtar name extracted from file \"%s\": \"%s\"", mtaYamlFile, mtaID)
mtarName = mtaID
}
return mtarName + ".mtar", nil
}
func setTimeStamp(mtaYamlFile string, p piperutils.FileUtils) error {
mtaYaml, err := p.FileRead(mtaYamlFile)
if err != nil {
return err
}
mtaYamlStr := string(mtaYaml)
timestampVar := "${timestamp}"
if strings.Contains(mtaYamlStr, timestampVar) {
if err := p.FileWrite(mtaYamlFile, []byte(strings.ReplaceAll(mtaYamlStr, timestampVar, getTimestamp())), 0644); err != nil {
return err
}
log.Entry().Infof("Timestamp replaced in \"%s\"", mtaYamlFile)
} else {
log.Entry().Infof("No timestap 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\n", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
}
func createMtaYamlFile(mtaYamlFile, applicationName string, p piperutils.FileUtils) error {
log.Entry().Debugf("mta yaml file not found in project sources.")
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 := p.FileExists("package.json")
if !packageFileExists {
return fmt.Errorf("package.json file does not exist")
}
var result map[string]interface{}
pContent, err := p.FileRead("package.json")
if err != nil {
return err
}
json.Unmarshal(pContent, &result)
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
}
p.FileWrite(mtaYamlFile, []byte(mtaConfig), 0644)
log.Entry().Infof("\"%s\" created.", mtaYamlFile)
return nil
}
func handleDefaultNpmRegistry(config mtaBuildOptions, e envExecRunner) error {
if len(config.DefaultNpmRegistry) > 0 {
log.Entry().Debugf("Setting default npm registry to \"%s\"", config.DefaultNpmRegistry)
if err := e.RunExecutable("npm", "config", "set", "registry", config.DefaultNpmRegistry); err != nil {
return err
}
} else {
log.Entry().Debugf("No default npm registry provided via configuration. Leaving npm config untouched.")
}
return nil
}
func handleSettingsFiles(config mtaBuildOptions,
p piperutils.FileUtils,
httpClient piperhttp.Downloader) error {
if len(config.ProjectSettingsFile) > 0 {
if err := getSettingsFile(maven.ProjectSettingsFile, config.ProjectSettingsFile, p, httpClient); err != nil {
return err
}
} else {
log.Entry().Debugf("Project settings file not provided via configuation.")
}
if len(config.GlobalSettingsFile) > 0 {
if err := getSettingsFile(maven.GlobalSettingsFile, config.GlobalSettingsFile, p, httpClient); err != nil {
return err
}
} else {
log.Entry().Debugf("Global settings file not provided via configuation.")
}
return nil
}
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
tmpl.Execute(&script, props)
return script.String(), nil
}
func getMtaID(mtaYamlFile string, fileUtils piperutils.FileUtils) (string, error) {
var result map[string]interface{}
p, err := fileUtils.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 {
fmt.Errorf("Id not found in mta yaml file (or wrong type)")
}
return id, nil
}

199
cmd/mtaBuild_generated.go Normal file
View File

@@ -0,0 +1,199 @@
// Code generated by piper's step-generator. DO NOT EDIT.
package cmd
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperenv"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/spf13/cobra"
)
type mtaBuildOptions struct {
BuildTarget string `json:"buildTarget,omitempty"`
MtaBuildTool string `json:"mtaBuildTool,omitempty"`
MtarName string `json:"mtarName,omitempty"`
MtaJarLocation string `json:"mtaJarLocation,omitempty"`
Extensions string `json:"extensions,omitempty"`
Platform string `json:"platform,omitempty"`
ApplicationName string `json:"applicationName,omitempty"`
DefaultNpmRegistry string `json:"defaultNpmRegistry,omitempty"`
ProjectSettingsFile string `json:"projectSettingsFile,omitempty"`
GlobalSettingsFile string `json:"globalSettingsFile,omitempty"`
}
type mtaBuildCommonPipelineEnvironment struct {
mtarFilePath string
}
func (p *mtaBuildCommonPipelineEnvironment) persist(path, resourceName string) {
content := []struct {
category string
name string
value string
}{
{category: "", name: "mtarFilePath", value: p.mtarFilePath},
}
errCount := 0
for _, param := range content {
err := piperenv.SetResourceParameter(path, resourceName, filepath.Join(param.category, param.name), param.value)
if err != nil {
log.Entry().WithError(err).Error("Error persisting piper environment.")
errCount++
}
}
if errCount > 0 {
os.Exit(1)
}
}
// MtaBuildCommand Performs an mta build
func MtaBuildCommand() *cobra.Command {
metadata := mtaBuildMetadata()
var stepConfig mtaBuildOptions
var startTime time.Time
var commonPipelineEnvironment mtaBuildCommonPipelineEnvironment
var createMtaBuildCmd = &cobra.Command{
Use: "mtaBuild",
Short: "Performs an mta build",
Long: `Executes the SAP Multitarget Application Archive Builder to create an mtar archive of the application.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
startTime = time.Now()
log.SetStepName("mtaBuild")
log.SetVerbose(GeneralConfig.Verbose)
return PrepareConfig(cmd, &metadata, "mtaBuild", &stepConfig, config.OpenPiperFile)
},
Run: func(cmd *cobra.Command, args []string) {
telemetryData := telemetry.CustomData{}
telemetryData.ErrorCode = "1"
handler := func() {
commonPipelineEnvironment.persist(GeneralConfig.EnvRootPath, "commonPipelineEnvironment")
telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds())
telemetry.Send(&telemetryData)
}
log.DeferExitHandler(handler)
defer handler()
telemetry.Initialize(GeneralConfig.NoTelemetry, "mtaBuild")
mtaBuild(stepConfig, &telemetryData, &commonPipelineEnvironment)
telemetryData.ErrorCode = "0"
},
}
addMtaBuildFlags(createMtaBuildCmd, &stepConfig)
return createMtaBuildCmd
}
func addMtaBuildFlags(cmd *cobra.Command, stepConfig *mtaBuildOptions) {
cmd.Flags().StringVar(&stepConfig.BuildTarget, "buildTarget", os.Getenv("PIPER_buildTarget"), "mtaBuildTool 'classic' only: The target platform to which the mtar can be deployed. Valid values: 'CF', 'NEO', 'XSA'.")
cmd.Flags().StringVar(&stepConfig.MtaBuildTool, "mtaBuildTool", "cloudMbt", "Tool to use when building the MTA. Valid values: 'classic', 'cloudMbt'.")
cmd.Flags().StringVar(&stepConfig.MtarName, "mtarName", os.Getenv("PIPER_mtarName"), "The name of the generated mtar file including its extension.")
cmd.Flags().StringVar(&stepConfig.MtaJarLocation, "mtaJarLocation", os.Getenv("PIPER_mtaJarLocation"), "mtaBuildTool 'classic' only: The location of the SAP Multitarget Application Archive Builder jar file, including file name and extension. If you run on Docker, this must match the location of the jar file in the container as well.")
cmd.Flags().StringVar(&stepConfig.Extensions, "extensions", os.Getenv("PIPER_extensions"), "The path to the extension descriptor file.")
cmd.Flags().StringVar(&stepConfig.Platform, "platform", os.Getenv("PIPER_platform"), "mtaBuildTool 'cloudMbt' only: The target platform to which the mtar can be deployed.")
cmd.Flags().StringVar(&stepConfig.ApplicationName, "applicationName", os.Getenv("PIPER_applicationName"), "The name of the application which is being built. If the parameter has been provided and no `mta.yaml` exists, the `mta.yaml` will be automatically generated using this parameter and the information (`name` and `version`) from 'package.json` before the actual build starts.")
cmd.Flags().StringVar(&stepConfig.DefaultNpmRegistry, "defaultNpmRegistry", os.Getenv("PIPER_defaultNpmRegistry"), "Url to the npm registry that should be used for installing npm dependencies.")
cmd.Flags().StringVar(&stepConfig.ProjectSettingsFile, "projectSettingsFile", os.Getenv("PIPER_projectSettingsFile"), "Path or url to the mvn settings file that should be used as project settings file.")
cmd.Flags().StringVar(&stepConfig.GlobalSettingsFile, "globalSettingsFile", os.Getenv("PIPER_globalSettingsFile"), "Path or url to the mvn settings file that should be used as global settings file")
}
// retrieve step metadata
func mtaBuildMetadata() config.StepData {
var theMetaData = config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{
Name: "buildTarget",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "mtaBuildTool",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "mtarName",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "mtaJarLocation",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "extensions",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "platform",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "applicationName",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "defaultNpmRegistry",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "projectSettingsFile",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "globalSettingsFile",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
},
},
},
}
return theMetaData
}

View File

@@ -0,0 +1,16 @@
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMtaBuildCommand(t *testing.T) {
testCmd := MtaBuildCommand()
// only high level testing performed - details are tested in step generation procudure
assert.Equal(t, "mtaBuild", testCmd.Use, "command name incorrect")
}

336
cmd/mtaBuild_test.go Normal file
View File

@@ -0,0 +1,336 @@
package cmd
import (
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/maven"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
"os"
"testing"
)
func TestMarBuild(t *testing.T) {
cpe := mtaBuildCommonPipelineEnvironment{}
httpClient := piperhttp.Client{}
t.Run("Application name not set", func(t *testing.T) {
e := execMockRunner{}
options := mtaBuildOptions{}
fileUtils := MtaTestFileUtilsMock{}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.NotNil(t, err)
assert.Equal(t, "'mta.yaml' not found in project sources and 'applicationName' not provided as parameter - cannot generate 'mta.yaml' file", err.Error())
})
t.Run("Provide default npm registry", func(t *testing.T) {
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp", MtaBuildTool: "classic", BuildTarget: "CF", DefaultNpmRegistry: "https://example.org/npm", MtarName: "myName"}
existingFiles := make(map[string]string)
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.Nil(t, err)
if assert.Len(t, e.calls, 2) { // the second (unchecked) entry is the mta call
assert.Equal(t, "npm", e.calls[0].exec)
assert.Equal(t, []string{"config", "set", "registry", "https://example.org/npm"}, e.calls[0].params)
}
})
t.Run("Package json does not exist", func(t *testing.T) {
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp"}
fileUtils := MtaTestFileUtilsMock{}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.NotNil(t, err)
assert.Equal(t, "package.json file does not exist", err.Error())
})
t.Run("Write yaml file", func(t *testing.T) {
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp", MtaBuildTool: "classic", BuildTarget: "CF", MtarName: "myName"}
existingFiles := make(map[string]string)
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.Nil(t, err)
type MtaResult struct {
Version string
ID string `yaml:"ID,omitempty"`
Parameters map[string]string
Modules []struct {
Name string
Type string
Parameters map[string]interface{}
}
}
assert.NotEmpty(t, fileUtils.writtenFiles["mta.yaml"])
var result MtaResult
yaml.Unmarshal([]byte(fileUtils.writtenFiles["mta.yaml"]), &result)
assert.Equal(t, "myName", result.ID)
assert.Equal(t, "1.2.3", result.Version)
assert.Equal(t, "myApp", result.Modules[0].Name)
assert.Equal(t, result.Modules[0].Parameters["version"], "1.2.3-${timestamp}")
assert.Equal(t, "myApp", result.Modules[0].Parameters["name"])
})
t.Run("Dont write mta yaml file when already present no timestamp placeholder", func(t *testing.T) {
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp", MtaBuildTool: "classic", BuildTarget: "CF"}
existingFiles := make(map[string]string)
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
existingFiles["mta.yaml"] = "already there"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.Empty(t, fileUtils.writtenFiles)
})
t.Run("Write mta yaml file when already present with timestamp placeholder", func(t *testing.T) {
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp", MtaBuildTool: "classic", BuildTarget: "CF"}
existingFiles := make(map[string]string)
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
existingFiles["mta.yaml"] = "already there with-${timestamp}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.NotEmpty(t, fileUtils.writtenFiles["mta.yaml"])
})
t.Run("Test mta build classic toolset", func(t *testing.T) {
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp", MtaBuildTool: "classic", BuildTarget: "CF", MtarName: "myName"}
cpe.mtarFilePath = ""
existingFiles := make(map[string]string)
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.Nil(t, err)
if assert.Len(t, e.calls, 1) {
assert.Equal(t, "java", e.calls[0].exec)
assert.Equal(t, []string{"-jar", "mta.jar", "--mtar", "myName.mtar", "--build-target=CF", "build"}, e.calls[0].params)
}
assert.Equal(t, "myName.mtar", cpe.mtarFilePath)
})
t.Run("Test mta build classic toolset, mtarName from already existing mta.yaml", func(t *testing.T) {
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp", MtaBuildTool: "classic", BuildTarget: "CF"}
cpe.mtarFilePath = ""
existingFiles := make(map[string]string)
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
existingFiles["mta.yaml"] = "ID: \"myNameFromMtar\""
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.Nil(t, err)
if assert.Len(t, e.calls, 1) {
assert.Equal(t, "java", e.calls[0].exec)
assert.Equal(t, []string{"-jar", "mta.jar", "--mtar", "myNameFromMtar.mtar", "--build-target=CF", "build"}, e.calls[0].params)
}
})
t.Run("Test mta build classic toolset with configured mta jar", func(t *testing.T) {
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp", MtaBuildTool: "classic", BuildTarget: "CF", MtaJarLocation: "/opt/sap/mta/lib/mta.jar", MtarName: "myName"}
existingFiles := make(map[string]string)
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.Nil(t, err)
if assert.Len(t, e.calls, 1) {
assert.Equal(t, "java", e.calls[0].exec)
assert.Equal(t, []string{"-jar", "/opt/sap/mta/lib/mta.jar", "--mtar", "myName.mtar", "--build-target=CF", "build"}, e.calls[0].params)
}
})
t.Run("Mta build mbt toolset", func(t *testing.T) {
e := execMockRunner{}
cpe.mtarFilePath = ""
options := mtaBuildOptions{ApplicationName: "myApp", MtaBuildTool: "cloudMbt", Platform: "CF", MtarName: "myName"}
existingFiles := make(map[string]string)
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.Nil(t, err)
if assert.Len(t, e.calls, 1) {
assert.Equal(t, "mbt", e.calls[0].exec)
assert.Equal(t, []string{"build", "--mtar", "myName.mtar", "--platform", "CF", "--target", "./"}, e.calls[0].params)
}
assert.Equal(t, "myName.mtar", cpe.mtarFilePath)
})
t.Run("Settings file releatd tests", func(t *testing.T) {
var settingsFile string
var settingsFileType maven.SettingsFileType
defer func() {
getSettingsFile = maven.GetSettingsFile
}()
getSettingsFile = func(
sfType maven.SettingsFileType,
src string,
fileUtilsMock piperutils.FileUtils,
httpClientMock piperhttp.Downloader) error {
settingsFile = src
settingsFileType = sfType
return nil
}
fileUtils := MtaTestFileUtilsMock{}
fileUtils.existingFiles = make(map[string]string)
fileUtils.existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
t.Run("Copy global settings file", func(t *testing.T) {
defer func() {
settingsFile = ""
settingsFileType = -1
}()
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp", GlobalSettingsFile: "/opt/maven/settings.xml", MtaBuildTool: "cloudMbt", Platform: "CF", MtarName: "myName"}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.Nil(t, err)
assert.Equal(t, settingsFile, "/opt/maven/settings.xml")
assert.Equal(t, settingsFileType, maven.GlobalSettingsFile)
})
t.Run("Copy project settings file", func(t *testing.T) {
defer func() {
settingsFile = ""
settingsFileType = -1
}()
e := execMockRunner{}
options := mtaBuildOptions{ApplicationName: "myApp", ProjectSettingsFile: "/my/project/settings.xml", MtaBuildTool: "cloudMbt", Platform: "CF", MtarName: "myName"}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
assert.Nil(t, err)
assert.Equal(t, "/my/project/settings.xml", settingsFile)
assert.Equal(t, maven.ProjectSettingsFile, settingsFileType)
})
})
}
type MtaTestFileUtilsMock struct {
existingFiles map[string]string
writtenFiles map[string]string
copiedFiles map[string]string
}
func (f *MtaTestFileUtilsMock) FileExists(path string) (bool, error) {
if _, ok := f.existingFiles[path]; ok {
return true, nil
}
return false, nil
}
func (f *MtaTestFileUtilsMock) Copy(src, dest string) (int64, error) {
if f.copiedFiles == nil {
f.copiedFiles = make(map[string]string)
}
f.copiedFiles[src] = dest
return 0, nil
}
func (f *MtaTestFileUtilsMock) FileRead(path string) ([]byte, error) {
return []byte(f.existingFiles[path]), nil
}
func (f *MtaTestFileUtilsMock) FileWrite(path string, content []byte, perm os.FileMode) error {
if f.writtenFiles == nil {
f.writtenFiles = make(map[string]string)
}
if _, ok := f.writtenFiles[path]; ok {
delete(f.writtenFiles, path)
}
f.writtenFiles[path] = string(content)
return nil
}
func (f *MtaTestFileUtilsMock) MkdirAll(path string, perm os.FileMode) error {
return nil
}

View File

@@ -56,6 +56,7 @@ func Execute() {
rootCmd.AddCommand(CloudFoundryDeleteServiceCommand())
rootCmd.AddCommand(AbapEnvironmentPullGitRepoCommand())
rootCmd.AddCommand(CheckmarxExecuteScanCommand())
rootCmd.AddCommand(MtaBuildCommand())
rootCmd.AddCommand(ProtecodeExecuteScanCommand())
addRootFlags(rootCmd)

View File

@@ -110,7 +110,8 @@ xs {{.Mode.GetDeployCommand}} -i {{.OperationID}} -a {{.Action.GetAction}}
func xsDeploy(config xsDeployOptions, telemetryData *telemetry.CustomData) {
c := command.Command{}
err := runXsDeploy(config, &c, piperutils.FileExists, piperutils.Copy, os.Remove, os.Stdout)
fileUtils := piperutils.Files{}
err := runXsDeploy(config, &c, fileUtils, os.Remove, os.Stdout)
if err != nil {
log.Entry().
WithError(err).
@@ -119,8 +120,7 @@ func xsDeploy(config xsDeployOptions, telemetryData *telemetry.CustomData) {
}
func runXsDeploy(XsDeployOptions xsDeployOptions, s shellRunner,
fExists func(string) (bool, error),
fCopy func(string, string) (int64, error),
fileUtils piperutils.FileUtils,
fRemove func(string) error,
stdout io.Writer) error {
@@ -150,7 +150,7 @@ func runXsDeploy(XsDeployOptions xsDeployOptions, s shellRunner,
log.Entry().Debugf("performLogin: %t, performLogout: %t", performLogin, performLogout)
{
exists, e := fExists(XsDeployOptions.MtaPath)
exists, e := fileUtils.FileExists(XsDeployOptions.MtaPath)
if e != nil {
return e
}
@@ -200,14 +200,14 @@ func runXsDeploy(XsDeployOptions xsDeployOptions, s shellRunner,
if performLogin {
loginErr = xsLogin(XsDeployOptions, s)
if loginErr == nil {
err = copyFileFromHomeToPwd(xsSessionFile, fCopy)
err = copyFileFromHomeToPwd(xsSessionFile, fileUtils)
}
}
if loginErr == nil && err == nil {
{
exists, e := fExists(xsSessionFile)
exists, e := fileUtils.FileExists(xsSessionFile)
if e != nil {
return e
}
@@ -216,7 +216,7 @@ func runXsDeploy(XsDeployOptions xsDeployOptions, s shellRunner,
}
}
copyFileFromPwdToHome(xsSessionFile, fCopy)
copyFileFromPwdToHome(xsSessionFile, fileUtils)
switch action {
case Resume, Abort, Retry:
@@ -435,20 +435,17 @@ func executeCmd(templateID string, commandPattern string, properties interface{}
return nil
}
func copyFileFromHomeToPwd(xsSessionFile string, fCopy func(string, string) (int64, error)) error {
if fCopy == nil {
fCopy = piperutils.Copy
}
func copyFileFromHomeToPwd(xsSessionFile string, fileUtils piperutils.FileUtils) error {
src, dest := fmt.Sprintf("%s/%s", os.Getenv("HOME"), xsSessionFile), fmt.Sprintf("%s", xsSessionFile)
log.Entry().Debugf("Copying xs session file from home directory ('%s') to workspace ('%s')", src, dest)
if _, err := fCopy(src, dest); err != nil {
if _, err := fileUtils.Copy(src, dest); err != nil {
return errors.Wrapf(err, "Cannot copy xssession file from home directory ('%s') to workspace ('%s')", src, dest)
}
log.Entry().Debugf("xs session file copied from home directory ('%s') to workspace ('%s')", src, dest)
return nil
}
func copyFileFromPwdToHome(xsSessionFile string, fCopy func(string, string) (int64, error)) error {
func copyFileFromPwdToHome(xsSessionFile string, fileUtils piperutils.FileUtils) error {
//
// We rely on running inside a docker container which is discarded after a single use.
@@ -457,12 +454,9 @@ func copyFileFromPwdToHome(xsSessionFile string, fCopy func(string, string) (int
// affects also other builds.
//
if fCopy == nil {
fCopy = piperutils.Copy
}
src, dest := fmt.Sprintf("%s", xsSessionFile), fmt.Sprintf("%s/%s", os.Getenv("HOME"), xsSessionFile)
log.Entry().Debugf("Copying xs session file from workspace ('%s') to home directory ('%s')", src, dest)
if _, err := fCopy(src, dest); err != nil {
if _, err := fileUtils.Copy(src, dest); err != nil {
return errors.Wrapf(err, "Cannot copy xssession file from workspace ('%s') to home directory ('%s')", src, dest)
}
log.Entry().Debugf("xs session file copied from workspace ('%s') to home directory ('%s')", src, dest)

View File

@@ -7,11 +7,37 @@ import (
"github.com/stretchr/testify/assert"
"io"
"io/ioutil"
"os"
"strings"
"sync"
"testing"
)
type FileUtilsMock struct {
copiedFiles []string
}
func (f *FileUtilsMock) FileExists(path string) (bool, error) {
return path == "dummy.mtar" || path == ".xs_session", nil
}
func (f *FileUtilsMock) Copy(src, dest string) (int64, error) {
f.copiedFiles = append(f.copiedFiles, fmt.Sprintf("%s->%s", src, dest))
return 0, nil
}
func (f *FileUtilsMock) FileRead(path string) ([]byte, error) {
return []byte{}, nil
}
func (f *FileUtilsMock) FileWrite(path string, content []byte, perm os.FileMode) error {
return nil
}
func (f *FileUtilsMock) MkdirAll(path string, perm os.FileMode) error {
return nil
}
func TestDeploy(t *testing.T) {
myXsDeployOptions := xsDeployOptions{
APIURL: "https://example.org:12345",
@@ -30,17 +56,9 @@ func TestDeploy(t *testing.T) {
s := shellMockRunner{}
var copiedFiles []string
var removedFiles []string
fExists := func(path string) (bool, error) {
return path == "dummy.mtar" || path == ".xs_session", nil
}
fCopy := func(src, dest string) (int64, error) {
copiedFiles = append(copiedFiles, fmt.Sprintf("%s->%s", src, dest))
return 0, nil
}
fileUtilsMock := FileUtilsMock{}
fRemove := func(path string) error {
removedFiles = append(removedFiles, path)
@@ -52,7 +70,7 @@ func TestDeploy(t *testing.T) {
t.Run("Standard deploy succeeds", func(t *testing.T) {
defer func() {
copiedFiles = nil
fileUtilsMock.copiedFiles = nil
removedFiles = nil
s.calls = nil
stdout = ""
@@ -70,7 +88,7 @@ func TestDeploy(t *testing.T) {
wg.Done()
}()
e := runXsDeploy(myXsDeployOptions, &s, fExists, fCopy, fRemove, wStdout)
e := runXsDeploy(myXsDeployOptions, &s, &fileUtilsMock, fRemove, wStdout)
wStdout.Close()
wg.Wait()
@@ -88,14 +106,14 @@ func TestDeploy(t *testing.T) {
assert.Len(t, removedFiles, 1)
assert.Contains(t, removedFiles, ".xs_session")
assert.Len(t, copiedFiles, 2)
assert.Len(t, fileUtilsMock.copiedFiles, 2)
// We copy the xs session file to the workspace in order to be able to use the file later.
// This happens directly after login
// We copy the xs session file from the workspace to the home folder in order to be able to
// use that file. This is important in case we rely on a login which happend e
assert.Contains(t, copiedFiles[0], "/.xs_session->.xs_session")
assert.Contains(t, copiedFiles[1], ".xs_session->")
assert.Contains(t, copiedFiles[1], "/.xs_session")
assert.Contains(t, fileUtilsMock.copiedFiles[0], "/.xs_session->.xs_session")
assert.Contains(t, fileUtilsMock.copiedFiles[1], ".xs_session->")
assert.Contains(t, fileUtilsMock.copiedFiles[1], "/.xs_session")
})
t.Run("Password not exposed", func(t *testing.T) {
@@ -107,7 +125,7 @@ func TestDeploy(t *testing.T) {
t.Run("Standard deploy fails, deployable missing", func(t *testing.T) {
defer func() {
copiedFiles = nil
fileUtilsMock.copiedFiles = nil
removedFiles = nil
s.calls = nil
}()
@@ -121,14 +139,14 @@ func TestDeploy(t *testing.T) {
// this file is not denoted in the file exists mock
myXsDeployOptions.MtaPath = "doesNotExist"
e := runXsDeploy(myXsDeployOptions, &s, fExists, fCopy, fRemove, ioutil.Discard)
e := runXsDeploy(myXsDeployOptions, &s, &fileUtilsMock, fRemove, ioutil.Discard)
checkErr(t, e, "Deployable 'doesNotExist' does not exist")
})
t.Run("Standard deploy fails, action provided", func(t *testing.T) {
defer func() {
copiedFiles = nil
fileUtilsMock.copiedFiles = nil
removedFiles = nil
s.calls = nil
}()
@@ -138,14 +156,14 @@ func TestDeploy(t *testing.T) {
myXsDeployOptions.Action = "NONE"
}()
e := runXsDeploy(myXsDeployOptions, &s, fExists, fCopy, fRemove, ioutil.Discard)
e := runXsDeploy(myXsDeployOptions, &s, &fileUtilsMock, fRemove, ioutil.Discard)
checkErr(t, e, "Cannot perform action 'RETRY' in mode 'DEPLOY'. Only action 'NONE' is allowed.")
})
t.Run("Standard deploy fails, error from underlying process", func(t *testing.T) {
defer func() {
copiedFiles = nil
fileUtilsMock.copiedFiles = nil
removedFiles = nil
s.calls = nil
s.shouldFailOnCommand = nil
@@ -153,14 +171,14 @@ func TestDeploy(t *testing.T) {
s.shouldFailOnCommand = map[string]error{"#!/bin/bash\nxs login -a https://example.org:12345 -u me -p 'secretPassword' -o myOrg -s mySpace --skip-ssl-validation\n": errors.New("Error from underlying process")}
e := runXsDeploy(myXsDeployOptions, &s, fExists, fCopy, fRemove, ioutil.Discard)
e := runXsDeploy(myXsDeployOptions, &s, &fileUtilsMock, fRemove, ioutil.Discard)
checkErr(t, e, "Error from underlying process")
})
t.Run("BG deploy succeeds", func(t *testing.T) {
defer func() {
copiedFiles = nil
fileUtilsMock.copiedFiles = nil
removedFiles = nil
s.calls = nil
}()
@@ -173,7 +191,7 @@ func TestDeploy(t *testing.T) {
myXsDeployOptions.Mode = "BG_DEPLOY"
e := runXsDeploy(myXsDeployOptions, &s, fExists, fCopy, fRemove, ioutil.Discard)
e := runXsDeploy(myXsDeployOptions, &s, &fileUtilsMock, fRemove, ioutil.Discard)
checkErr(t, e, "")
assert.Contains(t, s.calls[0], "xs login")
@@ -184,7 +202,7 @@ func TestDeploy(t *testing.T) {
t.Run("BG deploy abort succeeds", func(t *testing.T) {
defer func() {
copiedFiles = nil
fileUtilsMock.copiedFiles = nil
removedFiles = nil
s.calls = nil
}()
@@ -202,7 +220,7 @@ func TestDeploy(t *testing.T) {
myXsDeployOptions.Action = "ABORT"
myXsDeployOptions.OperationID = "12345"
e := runXsDeploy(myXsDeployOptions, &s, fExists, fCopy, fRemove, ioutil.Discard)
e := runXsDeploy(myXsDeployOptions, &s, &fileUtilsMock, fRemove, ioutil.Discard)
checkErr(t, e, "")
assert.Contains(t, s.calls[0], "xs bg-deploy -i 12345 -a abort")
@@ -213,7 +231,7 @@ func TestDeploy(t *testing.T) {
t.Run("BG deploy abort fails due to missing operationId", func(t *testing.T) {
defer func() {
copiedFiles = nil
fileUtilsMock.copiedFiles = nil
removedFiles = nil
s.calls = nil
}()
@@ -229,7 +247,7 @@ func TestDeploy(t *testing.T) {
myXsDeployOptions.Mode = "BG_DEPLOY"
myXsDeployOptions.Action = "ABORT"
e := runXsDeploy(myXsDeployOptions, &s, fExists, fCopy, fRemove, ioutil.Discard)
e := runXsDeploy(myXsDeployOptions, &s, &fileUtilsMock, fRemove, ioutil.Discard)
checkErr(t, e, "OperationID was not provided")
})
}

1
go.mod
View File

@@ -20,4 +20,5 @@ require (
github.com/testcontainers/testcontainers-go v0.1.0
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.4
)

View File

@@ -0,0 +1,111 @@
metadata:
name: mtaBuild
description: Performs an mta build
longDescription: |
Executes the SAP Multitarget Application Archive Builder to create an mtar archive of the application.
spec:
inputs:
params:
- name: buildTarget
type: string
description: "mtaBuildTool 'classic' only: The target platform to which the mtar can be deployed. Valid values: 'CF', 'NEO', 'XSA'."
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default:
- name: mtaBuildTool
type: string
description: "Tool to use when building the MTA. Valid values: 'classic', 'cloudMbt'."
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default: cloudMbt
- name: mtarName
type: string
description: "The name of the generated mtar file including its extension."
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default:
- name: mtaJarLocation
type: string
description: "mtaBuildTool 'classic' only: The location of the SAP Multitarget Application Archive Builder jar file, including file name and extension. If you run on Docker, this must match the location of the jar file in the container as well."
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default:
- name: extensions
type: string
description: "The path to the extension descriptor file."
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default:
- name: platform
type: string
description: "mtaBuildTool 'cloudMbt' only: The target platform to which the mtar can be deployed."
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default:
- name: applicationName
type: string
description: "The name of the application which is being built. If the parameter has been provided and no `mta.yaml` exists, the `mta.yaml` will be automatically generated using this parameter and the information (`name` and `version`) from 'package.json` before the actual build starts."
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default:
- name: defaultNpmRegistry
type: string
description: "Url to the npm registry that should be used for installing npm dependencies."
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default:
- name: projectSettingsFile
type: string
description: "Path or url to the mvn settings file that should be used as project settings file."
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default:
- name: globalSettingsFile
type: string
description: "Path or url to the mvn settings file that should be used as global settings file"
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default:
outputs:
resources:
- name: commonPipelineEnvironment
type: piperEnvironment
params:
- name: mtarFilePath
fields:
- name: mtarFilePath
containers:
- name: mta
image: devxci/mbtci
imagePullPolicy: Always