1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

Merge remote-tracking branch 'upstream/master' into harmonize-docker-arguments

This commit is contained in:
Stengel 2019-11-06 10:02:58 +01:00
commit 1dbe7352c7
45 changed files with 2460 additions and 129 deletions

View File

@ -1,3 +1,7 @@
version: "2"
checks:
return-statements:
enabled: false
plugins:
codenarc:
enabled: true

View File

@ -22,6 +22,6 @@ indent_size = none
[cfg/id_rsa.enc]
indent_style = none
indent_size = none
[{go.mod,go.sum,*.go}]
[{go.mod,go.sum,*.go,*.golden}]
indent_style = tab
indent_size = 8

View File

@ -1,5 +1,13 @@
# Guidance on how to contribute
**Table of contents:**
1. [Using the issue tracker](#using-the-issue-tracker)
1. [Changing the code-base](#changing-the-code-base)
1. [Jenkins credential handling](#jenkins-credentials)
1. [Code Style](#code-style)
1. [References](#references)
There are two primary ways to help:
* Using the issue tracker, and
@ -36,14 +44,6 @@ Implementation of a functionality and its documentation shall happen within the
Pipeline steps must not make use of return values. The pattern for sharing parameters between pipeline steps or between a pipeline step and a pipeline script is sharing values via the [`commonPipelineEnvironment`](../vars/commonPipelineEnvironment.groovy). Since there is no return value from a pipeline step the return value of a pipeline step is already `void` rather than `def`.
### Code Style
The code should follow any stylistic and architectural guidelines prescribed by the project. In the absence of guidelines, mimic the styles and patterns in the existing code-base.
Variables, methods, types and so on shall have meaningful self describing names. Doing so makes understanding code easier and requires less commenting. It helps people who did not write the code to understand it better.
Code shall contain comments to explain the intention of the code when it is unclear what the intention of the author was. In such cases, comments should describe the "why" and not the "what" (that is in the code already).
#### EditorConfig
To ensure a common file format, there is a `.editorConfig` file [in place](../.editorconfig). To respect this file, [check](http://editorconfig.org/#download) if your editor does support it natively or you need to download a plugin.
@ -54,8 +54,25 @@ Write [meaningful commit messages](http://who-t.blogspot.de/2009/12/on-commit-me
Good commit messages speed up the review process and help to keep this project maintainable in the long term.
## Jenkins credential handling
References to Jenkins credentials should have meaningful names.
We are using the following approach for naming Jenkins credentials:
For username/password credentials:
`<tool>CredentialsId` like e.g. `neoCredentialsId`
For other cases we add further information to the name like:
* `gitSshCredentialsId` for ssh credentials
* `githubTokenCredentialsId`for token/string credentials
* `gcpFileCredentialsId` for file credentials
## Code Style
Generally, the code should follow any stylistic and architectural guidelines prescribed by the project. In the absence of guidelines, mimic the styles and patterns in the existing code-base.
The intention of this section is to describe the code style for this project. As reference document, the [Groovy's style guide](http://groovy-lang.org/style-guide.html) was taken. For further reading about Groovy's syntax and examples, please refer to this guide.
This project is intended to run in Jenkins [[2]](https://jenkins.io/doc/book/getting-started/) as part of a Jenkins Pipeline [[3]](https://jenkins.io/doc/book/pipeline/). It is composed by Jenkins Pipeline's syntax, Groovy's syntax and Java's syntax.
@ -64,6 +81,12 @@ Some Groovy's syntax is not yet supported by Jenkins. It is also the intention o
As Groovy supports 99% of Java’s syntax [[1]](http://groovy-lang.org/style-guide.html), many Java developers tend to write Groovy code using Java's syntax. Such a developer should also consider the following code style for this project.
### General remarks
Variables, methods, types and so on shall have meaningful self describing names. Doing so makes understanding code easier and requires less commenting. It helps people who did not write the code to understand it better.
Code shall contain comments to explain the intention of the code when it is unclear what the intention of the author was. In such cases, comments should describe the "why" and not the "what" (that is in the code already).
### Omit semicolons
### Use the return keyword
@ -177,7 +200,7 @@ If the type of the exception thrown inside a try block is not important, catch a
To check parameters, return values, and more, use the assert statement.
## Reference
## References
[1] Groovy's syntax: [http://groovy-lang.org/style-guide.html](http://groovy-lang.org/style-guide.html)

2
.gitignore vendored
View File

@ -19,3 +19,5 @@ documentation/docs-gen
consumer-test/**/workspace
*.code-workspace
piper
piper.exe

View File

@ -8,6 +8,8 @@ RUN go test ./... -cover
## ONLY tests so far, building to be added later
# execute build
# RUN go build -o piper
RUN export GIT_COMMIT=$(git rev-parse HEAD) && \
go build -ldflags "-X github.com/SAP/jenkins-library/cmd.GitCommit=${GIT_COMMIT}" -o piper
# FROM gcr.io/distroless/base:latest
# COPY --from=build-env /build/piper /piper

120
cmd/getConfig.go Normal file
View File

@ -0,0 +1,120 @@
package cmd
import (
"fmt"
"io"
"os"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type configCommandOptions struct {
output string //output format, so far only JSON
parametersJSON string //parameters to be considered in JSON format
stepMetadata string //metadata to be considered, can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
stepName string
contextConfig bool
openFile func(s string) (io.ReadCloser, error)
}
var configOptions configCommandOptions
// ConfigCommand is the entry command for loading the configuration of a pipeline step
func ConfigCommand() *cobra.Command {
configOptions.openFile = openPiperFile
var createConfigCmd = &cobra.Command{
Use: "getConfig",
Short: "Loads the project 'Piper' configuration respecting defaults and parameters.",
RunE: func(cmd *cobra.Command, _ []string) error {
return generateConfig()
},
}
addConfigFlags(createConfigCmd)
return createConfigCmd
}
func generateConfig() error {
var myConfig config.Config
var stepConfig config.StepConfig
var metadata config.StepData
metadataFile, err := configOptions.openFile(configOptions.stepMetadata)
if err != nil {
return errors.Wrap(err, "metadata: open failed")
}
err = metadata.ReadPipelineStepData(metadataFile)
if err != nil {
return errors.Wrap(err, "metadata: read failed")
}
customConfig, err := configOptions.openFile(generalConfig.customConfig)
if err != nil {
return errors.Wrap(err, "config: open failed")
}
defaultConfig, paramFilter, err := defaultsAndFilters(&metadata)
if err != nil {
return errors.Wrap(err, "defaults: retrieving step defaults failed")
}
for _, f := range generalConfig.defaultConfig {
fc, err := configOptions.openFile(f)
if err != nil {
return errors.Wrapf(err, "config: getting defaults failed: '%v'", f)
}
defaultConfig = append(defaultConfig, fc)
}
var flags map[string]interface{}
params := []config.StepParameters{}
if !configOptions.contextConfig {
params = metadata.Spec.Inputs.Parameters
}
stepConfig, err = myConfig.GetStepConfig(flags, generalConfig.parametersJSON, customConfig, defaultConfig, paramFilter, params, generalConfig.stageName, configOptions.stepName)
if err != nil {
return errors.Wrap(err, "getting step config failed")
}
//ToDo: Check for mandatory parameters
myConfigJSON, _ := config.GetJSON(stepConfig.Config)
fmt.Println(myConfigJSON)
return nil
}
func addConfigFlags(cmd *cobra.Command) {
//ToDo: support more output options, like https://kubernetes.io/docs/reference/kubectl/overview/#formatting-output
cmd.Flags().StringVar(&configOptions.output, "output", "json", "Defines the output format")
cmd.Flags().StringVar(&configOptions.parametersJSON, "parametersJSON", os.Getenv("PIPER_parametersJSON"), "Parameters to be considered in JSON format")
cmd.Flags().StringVar(&configOptions.stepMetadata, "stepMetadata", "", "Step metadata, passed as path to yaml")
cmd.Flags().StringVar(&configOptions.stepName, "stepName", "", "Name of the step for which configuration should be included")
cmd.Flags().BoolVar(&configOptions.contextConfig, "contextConfig", false, "Defines if step context configuration should be loaded instead of step config")
cmd.MarkFlagRequired("stepMetadata")
cmd.MarkFlagRequired("stepName")
}
func defaultsAndFilters(metadata *config.StepData) ([]io.ReadCloser, config.StepFilters, error) {
if configOptions.contextConfig {
defaults, err := metadata.GetContextDefaults(configOptions.stepName)
if err != nil {
return nil, config.StepFilters{}, errors.Wrap(err, "metadata: getting context defaults failed")
}
return []io.ReadCloser{defaults}, metadata.GetContextParameterFilters(), nil
}
//ToDo: retrieve default values from metadata
return nil, metadata.GetParameterFilters(), nil
}

90
cmd/getConfig_test.go Normal file
View File

@ -0,0 +1,90 @@
package cmd
import (
"io"
"io/ioutil"
"strings"
"testing"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
)
func configOpenFileMock(name string) (io.ReadCloser, error) {
var r string
switch name {
case "TestAddCustomDefaults_default1":
r = "default1"
case "TestAddCustomDefaults_default2":
r = "default3"
default:
r = ""
}
return ioutil.NopCloser(strings.NewReader(r)), nil
}
func TestConfigCommand(t *testing.T) {
cmd := ConfigCommand()
gotReq := []string{}
gotOpt := []string{}
cmd.Flags().VisitAll(func(pflag *flag.Flag) {
annotations, found := pflag.Annotations[cobra.BashCompOneRequiredFlag]
if found && annotations[0] == "true" {
gotReq = append(gotReq, pflag.Name)
} else {
gotOpt = append(gotOpt, pflag.Name)
}
})
t.Run("Required flags", func(t *testing.T) {
exp := []string{"stepMetadata", "stepName"}
assert.Equal(t, exp, gotReq, "required flags incorrect")
})
t.Run("Optional flags", func(t *testing.T) {
exp := []string{"contextConfig", "output", "parametersJSON"}
assert.Equal(t, exp, gotOpt, "optional flags incorrect")
})
t.Run("Run", func(t *testing.T) {
t.Run("Success case", func(t *testing.T) {
configOptions.openFile = configOpenFileMock
err := cmd.RunE(cmd, []string{})
assert.NoError(t, err, "error occured but none expected")
})
})
}
func TestDefaultsAndFilters(t *testing.T) {
metadata := config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "paramOne", Scope: []string{"GENERAL", "STEPS", "STAGES", "PARAMETERS", "ENV"}},
},
},
},
}
t.Run("Context config", func(t *testing.T) {
configOptions.contextConfig = true
defer func() { configOptions.contextConfig = false }()
defaults, filters, err := defaultsAndFilters(&metadata)
assert.Equal(t, 1, len(defaults), "getting defaults failed")
assert.Equal(t, 0, len(filters.All), "wrong number of filter values")
assert.NoError(t, err, "error occured but none expected")
})
t.Run("Step config", func(t *testing.T) {
defaults, filters, err := defaultsAndFilters(&metadata)
assert.Equal(t, 0, len(defaults), "getting defaults failed")
assert.Equal(t, 1, len(filters.All), "wrong number of filter values")
assert.NoError(t, err, "error occured but none expected")
})
}

11
cmd/interfaces.go Normal file
View File

@ -0,0 +1,11 @@
package cmd
type execRunner interface {
RunExecutable(e string, p ...string) error
Dir(d string)
}
type shellRunner interface {
RunShell(s string, c string) error
Dir(d string)
}

44
cmd/karmaExecuteTests.go Normal file
View File

@ -0,0 +1,44 @@
package cmd
import (
"strings"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
)
func karmaExecuteTests(myKarmaExecuteTestsOptions karmaExecuteTestsOptions) error {
c := command.Command{}
// reroute command output to loging framework
// also log stdout as Karma reports into it
c.Stdout = log.Entry().Writer()
c.Stderr = log.Entry().Writer()
runKarma(myKarmaExecuteTestsOptions, &c)
return nil
}
func runKarma(myKarmaExecuteTestsOptions karmaExecuteTestsOptions, command execRunner) {
installCommandTokens := tokenize(myKarmaExecuteTestsOptions.InstallCommand)
command.Dir(myKarmaExecuteTestsOptions.ModulePath)
err := command.RunExecutable(installCommandTokens[0], installCommandTokens[1:]...)
if err != nil {
log.Entry().
WithError(err).
WithField("command", myKarmaExecuteTestsOptions.InstallCommand).
Fatal("failed to execute install command")
}
runCommandTokens := tokenize(myKarmaExecuteTestsOptions.RunCommand)
command.Dir(myKarmaExecuteTestsOptions.ModulePath)
err = command.RunExecutable(runCommandTokens[0], runCommandTokens[1:]...)
if err != nil {
log.Entry().
WithError(err).
WithField("command", myKarmaExecuteTestsOptions.RunCommand).
Fatal("failed to execute run command")
}
}
func tokenize(command string) []string {
return strings.Split(command, " ")
}

View File

@ -0,0 +1,90 @@
package cmd
import (
//"os"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/spf13/cobra"
)
type karmaExecuteTestsOptions struct {
InstallCommand string `json:"installCommand,omitempty"`
ModulePath string `json:"modulePath,omitempty"`
RunCommand string `json:"runCommand,omitempty"`
}
var myKarmaExecuteTestsOptions karmaExecuteTestsOptions
var karmaExecuteTestsStepConfigJSON string
// KarmaExecuteTestsCommand Executes the Karma test runner
func KarmaExecuteTestsCommand() *cobra.Command {
metadata := karmaExecuteTestsMetadata()
var createKarmaExecuteTestsCmd = &cobra.Command{
Use: "karmaExecuteTests",
Short: "Executes the Karma test runner",
Long: `In this step the ([Karma test runner](http://karma-runner.github.io)) is executed.
The step is using the ` + "`" + `seleniumExecuteTest` + "`" + ` step to spin up two containers in a Docker network:
* a Selenium/Chrome container (` + "`" + `selenium/standalone-chrome` + "`" + `)
* a NodeJS container (` + "`" + `node:8-stretch` + "`" + `)
In the Docker network, the containers can be referenced by the values provided in ` + "`" + `dockerName` + "`" + ` and ` + "`" + `sidecarName` + "`" + `, the default values are ` + "`" + `karma` + "`" + ` and ` + "`" + `selenium` + "`" + `. These values must be used in the ` + "`" + `hostname` + "`" + ` properties of the test configuration ([Karma](https://karma-runner.github.io/1.0/config/configuration-file.html) and [WebDriver](https://github.com/karma-runner/karma-webdriver-launcher#usage)).
!!! note
In a Kubernetes environment, the containers both need to be referenced with ` + "`" + `localhost` + "`" + `.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
log.SetStepName("karmaExecuteTests")
log.SetVerbose(generalConfig.verbose)
return PrepareConfig(cmd, &metadata, "karmaExecuteTests", &myKarmaExecuteTestsOptions, openPiperFile)
},
RunE: func(cmd *cobra.Command, args []string) error {
return karmaExecuteTests(myKarmaExecuteTestsOptions)
},
}
addKarmaExecuteTestsFlags(createKarmaExecuteTestsCmd)
return createKarmaExecuteTestsCmd
}
func addKarmaExecuteTestsFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&myKarmaExecuteTestsOptions.InstallCommand, "installCommand", "npm install --quiet", "The command that is executed to install the test tool.")
cmd.Flags().StringVar(&myKarmaExecuteTestsOptions.ModulePath, "modulePath", ".", "Define the path of the module to execute tests on.")
cmd.Flags().StringVar(&myKarmaExecuteTestsOptions.RunCommand, "runCommand", "npm run karma", "The command that is executed to start the tests.")
cmd.MarkFlagRequired("installCommand")
cmd.MarkFlagRequired("modulePath")
cmd.MarkFlagRequired("runCommand")
}
// retrieve step metadata
func karmaExecuteTestsMetadata() config.StepData {
var theMetaData = config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{
Name: "installCommand",
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
},
{
Name: "modulePath",
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
},
{
Name: "runCommand",
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
},
},
},
},
}
return theMetaData
}

View File

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

View File

@ -0,0 +1,46 @@
package cmd
import (
"testing"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/stretchr/testify/assert"
)
func TestRunKarma(t *testing.T) {
t.Run("success case", func(t *testing.T) {
opts := karmaExecuteTestsOptions{ModulePath: "./test", InstallCommand: "npm install test", RunCommand: "npm run test"}
e := execMockRunner{}
runKarma(opts, &e)
assert.Equal(t, e.dir[0], "./test", "install command dir incorrect")
assert.Equal(t, e.calls[0], execCall{exec: "npm", params: []string{"install", "test"}}, "install command/params incorrect")
assert.Equal(t, e.dir[1], "./test", "run command dir incorrect")
assert.Equal(t, e.calls[1], execCall{exec: "npm", params: []string{"run", "test"}}, "run command/params incorrect")
})
t.Run("error case install command", func(t *testing.T) {
var hasFailed bool
log.Entry().Logger.ExitFunc = func(int) { hasFailed = true }
opts := karmaExecuteTestsOptions{ModulePath: "./test", InstallCommand: "fail install test", RunCommand: "npm run test"}
e := execMockRunner{}
runKarma(opts, &e)
assert.True(t, hasFailed, "expected command to exit with fatal")
})
t.Run("error case run command", func(t *testing.T) {
var hasFailed bool
log.Entry().Logger.ExitFunc = func(int) { hasFailed = true }
opts := karmaExecuteTestsOptions{ModulePath: "./test", InstallCommand: "npm install test", RunCommand: "fail run test"}
e := execMockRunner{}
runKarma(opts, &e)
assert.True(t, hasFailed, "expected command to exit with fatal")
})
}

109
cmd/piper.go Normal file
View File

@ -0,0 +1,109 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type generalConfigOptions struct {
customConfig string
defaultConfig []string //ordered list of Piper default configurations. Can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
parametersJSON string
stageName string
stepConfigJSON string
stepMetadata string //metadata to be considered, can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
stepName string
verbose bool
}
var rootCmd = &cobra.Command{
Use: "piper",
Short: "Executes CI/CD steps from project 'Piper' ",
Long: `
This project 'Piper' binary provides a CI/CD step libary.
It contains many steps which can be used within CI/CD systems as well as directly on e.g. a developer's machine.
`,
//ToDo: respect stageName to also come from parametersJSON -> first env.STAGE_NAME, second: parametersJSON, third: flag
}
var generalConfig generalConfigOptions
// Execute is the starting point of the piper command line tool
func Execute() {
rootCmd.AddCommand(ConfigCommand())
rootCmd.AddCommand(VersionCommand())
rootCmd.AddCommand(KarmaExecuteTestsCommand())
addRootFlags(rootCmd)
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func addRootFlags(rootCmd *cobra.Command) {
rootCmd.PersistentFlags().StringVar(&generalConfig.customConfig, "customConfig", ".pipeline/config.yml", "Path to the pipeline configuration file")
rootCmd.PersistentFlags().StringSliceVar(&generalConfig.defaultConfig, "defaultConfig", nil, "Default configurations, passed as path to yaml file")
rootCmd.PersistentFlags().StringVar(&generalConfig.parametersJSON, "parametersJSON", os.Getenv("PIPER_parametersJSON"), "Parameters to be considered in JSON format")
rootCmd.PersistentFlags().StringVar(&generalConfig.stageName, "stageName", os.Getenv("STAGE_NAME"), "Name of the stage for which configuration should be included")
rootCmd.PersistentFlags().StringVar(&generalConfig.stepConfigJSON, "stepConfigJSON", os.Getenv("PIPER_stepConfigJSON"), "Step configuration in JSON format")
rootCmd.PersistentFlags().BoolVarP(&generalConfig.verbose, "verbose", "v", false, "verbose output")
}
// PrepareConfig reads step configuration from various sources and merges it (defaults, config file, flags, ...)
func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName string, options interface{}, openFile func(s string) (io.ReadCloser, error)) error {
filters := metadata.GetParameterFilters()
flagValues := config.AvailableFlagValues(cmd, &filters)
var myConfig config.Config
var stepConfig config.StepConfig
if len(generalConfig.stepConfigJSON) != 0 {
// ignore config & defaults in favor of passed stepConfigJSON
stepConfig = config.GetStepConfigWithJSON(flagValues, generalConfig.stepConfigJSON, filters)
} else {
// use config & defaults
//accept that config file and defaults cannot be loaded since both are not mandatory here
customConfig, _ := openFile(generalConfig.customConfig)
var defaultConfig []io.ReadCloser
for _, f := range generalConfig.defaultConfig {
//ToDo: support also https as source
fc, _ := openFile(f)
defaultConfig = append(defaultConfig, fc)
}
var err error
stepConfig, err = myConfig.GetStepConfig(flagValues, generalConfig.parametersJSON, customConfig, defaultConfig, filters, metadata.Spec.Inputs.Parameters, generalConfig.stageName, stepName)
if err != nil {
return errors.Wrap(err, "retrieving step configuration failed")
}
}
confJSON, _ := json.Marshal(stepConfig.Config)
json.Unmarshal(confJSON, &options)
config.MarkFlagsWithValue(cmd, stepConfig)
return nil
}
func openPiperFile(name string) (io.ReadCloser, error) {
//ToDo: support also https as source
if !strings.HasPrefix(name, "http") {
return os.Open(name)
}
return nil, fmt.Errorf("file location not yet supported for '%v'", name)
}

148
cmd/piper_test.go Normal file
View File

@ -0,0 +1,148 @@
package cmd
import (
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
)
type execMockRunner struct {
dir []string
calls []execCall
}
type execCall struct {
exec string
params []string
}
type shellMockRunner struct {
dir string
calls []string
}
func (m *execMockRunner) Dir(d string) {
m.dir = append(m.dir, d)
}
func (m *execMockRunner) RunExecutable(e string, p ...string) error {
if e == "fail" {
return fmt.Errorf("error case")
}
exec := execCall{exec: e, params: p}
m.calls = append(m.calls, exec)
return nil
}
func (m *shellMockRunner) Dir(d string) {
m.dir = d
}
func (m *shellMockRunner) RunShell(s string, c string) error {
m.calls = append(m.calls, c)
return nil
}
type stepOptions struct {
TestParam string `json:"testParam,omitempty"`
}
func openFileMock(name string) (io.ReadCloser, error) {
var r string
switch name {
case "testDefaults.yml":
r = "general:\n testParam: testValue"
case "testDefaultsInvalid.yml":
r = "invalid yaml"
default:
r = ""
}
return ioutil.NopCloser(strings.NewReader(r)), nil
}
func TestAddRootFlags(t *testing.T) {
var testRootCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
addRootFlags(testRootCmd)
assert.NotNil(t, testRootCmd.Flag("customConfig"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("defaultConfig"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("parametersJSON"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("stageName"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("stepConfigJSON"), "expected flag not available")
assert.NotNil(t, testRootCmd.Flag("verbose"), "expected flag not available")
}
func TestPrepareConfig(t *testing.T) {
defaultsBak := generalConfig.defaultConfig
generalConfig.defaultConfig = []string{"testDefaults.yml"}
defer func() { generalConfig.defaultConfig = defaultsBak }()
t.Run("using stepConfigJSON", func(t *testing.T) {
stepConfigJSONBak := generalConfig.stepConfigJSON
generalConfig.stepConfigJSON = `{"testParam": "testValueJSON"}`
defer func() { generalConfig.stepConfigJSON = stepConfigJSONBak }()
testOptions := stepOptions{}
var testCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
testCmd.Flags().StringVar(&testOptions.TestParam, "testParam", "", "test usage")
metadata := config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "testParam", Scope: []string{"GENERAL"}},
},
},
},
}
PrepareConfig(testCmd, &metadata, "testStep", &testOptions, openFileMock)
assert.Equal(t, "testValueJSON", testOptions.TestParam, "wrong value retrieved from config")
})
t.Run("using config files", func(t *testing.T) {
t.Run("success case", func(t *testing.T) {
testOptions := stepOptions{}
var testCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
testCmd.Flags().StringVar(&testOptions.TestParam, "testParam", "", "test usage")
metadata := config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "testParam", Scope: []string{"GENERAL"}},
},
},
},
}
err := PrepareConfig(testCmd, &metadata, "testStep", &testOptions, openFileMock)
assert.NoError(t, err, "no error expected but error occured")
//assert config
assert.Equal(t, "testValue", testOptions.TestParam, "wrong value retrieved from config")
//assert that flag has been marked as changed
testCmd.Flags().VisitAll(func(pflag *flag.Flag) {
if pflag.Name == "testParam" {
assert.True(t, pflag.Changed, "flag should be marked as changed")
}
})
})
t.Run("error case", func(t *testing.T) {
generalConfig.defaultConfig = []string{"testDefaultsInvalid.yml"}
testOptions := stepOptions{}
var testCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
metadata := config.StepData{}
err := PrepareConfig(testCmd, &metadata, "testStep", &testOptions, openFileMock)
assert.Error(t, err, "error expected but none occured")
})
})
}

28
cmd/version.go Normal file
View File

@ -0,0 +1,28 @@
package cmd
import (
"fmt"
)
// GitCommit ...
var GitCommit string
// GitTag ...
var GitTag string
func version(myVersionOptions versionOptions) error {
gitCommit, gitTag := "<n/a>", "<n/a>"
if len(GitCommit) > 0 {
gitCommit = GitCommit
}
if len(GitTag) > 0 {
gitTag = GitTag
}
_, err := fmt.Printf("piper-version:\n commit: \"%s\"\n tag: \"%s\"\n", gitCommit, gitTag)
return err
}

52
cmd/version_generated.go Normal file
View File

@ -0,0 +1,52 @@
package cmd
import (
//"os"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/spf13/cobra"
)
type versionOptions struct {
}
var myVersionOptions versionOptions
var versionStepConfigJSON string
// VersionCommand Returns the version of the piper binary
func VersionCommand() *cobra.Command {
metadata := versionMetadata()
var createVersionCmd = &cobra.Command{
Use: "version",
Short: "Returns the version of the piper binary",
Long: `Writes the commit hash and the tag (if any) to stdout and exits with 0.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
log.SetStepName("version")
log.SetVerbose(generalConfig.verbose)
return PrepareConfig(cmd, &metadata, "version", &myVersionOptions, openPiperFile)
},
RunE: func(cmd *cobra.Command, args []string) error {
return version(myVersionOptions)
},
}
addVersionFlags(createVersionCmd)
return createVersionCmd
}
func addVersionFlags(cmd *cobra.Command) {
}
// retrieve step metadata
func versionMetadata() config.StepData {
var theMetaData = config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{},
},
},
}
return theMetaData
}

View File

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

64
cmd/version_test.go Normal file
View File

@ -0,0 +1,64 @@
package cmd
import (
"bytes"
"github.com/stretchr/testify/assert"
"io"
"os"
"testing"
)
func TestVersion(t *testing.T) {
t.Run("versionAndTagInitialValues", func(t *testing.T) {
result := runVersionCommand(t, "", "")
assert.Contains(t, result, "commit: \"<n/a>\"")
assert.Contains(t, result, "tag: \"<n/a>\"")
})
t.Run("versionAndTagSet", func(t *testing.T) {
result := runVersionCommand(t, "16bafe", "v1.2.3")
assert.Contains(t, result, "commit: \"16bafe\"")
assert.Contains(t, result, "tag: \"v1.2.3\"")
})
}
func runVersionCommand(t *testing.T, commitID, tag string) string {
orig := os.Stdout
defer func() { os.Stdout = orig }()
r, w, e := os.Pipe()
if e != nil {
t.Error("Cannot setup pipes.")
}
os.Stdout = w
//
// needs to be set in the free wild by the build process:
// go build -ldflags "-X github.com/SAP/jenkins-library/cmd.GitCommit=${GIT_COMMIT} -X github.com/SAP/jenkins-library/cmd.GitTag=${GIT_TAG}"
if len(commitID) > 0 {
GitCommit = commitID
}
if len(tag) > 0 {
GitTag = tag
}
defer func() { GitCommit = ""; GitTag = "" }()
//
//
var myVersionOptions versionOptions
e = version(myVersionOptions)
if e != nil {
t.Error("Version command failed.")
}
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
return buf.String()
}

View File

@ -46,7 +46,7 @@ export ON_K8S=true"
```
```groovy
dockerExecuteOnKubernetes(script: script, containerMap: ['maven:3.5-jdk-8-alpine': 'maven', 's4sdk/docker-cf-cli': 'cfcli']){
dockerExecuteOnKubernetes(script: script, containerMap: ['maven:3.5-jdk-8-alpine': 'maven', 'ppiper/cf-cli': 'cfcli']){
container('maven'){
sh "mvn clean install"
}

View File

@ -4,7 +4,7 @@
## Prerequisites
* **SAP CP account** - the account to where the application is deployed.
* **SAP CP account** - the account to where the application is deployed. To deploy MTA (`deployMode: mta`) an over existing _Java_ application, free _Java Quota_ of at least 1 is required, which means that this will not work on trial accounts.
* **SAP CP user for deployment** - a user with deployment permissions in the given account.
* **Jenkins credentials for deployment** - must be configured in Jenkins credentials with a dedicated Id.

View File

@ -21,7 +21,7 @@ xsDeploy
credentialsId: 'my-credentials-id',
apiUrl: 'https://example.org/xs',
space: 'mySpace',
org:: 'myOrg'
org: 'myOrg'
```
Example configuration:
@ -30,12 +30,11 @@ Example configuration:
steps:
<...>
xsDeploy:
script: this,
mtaPath: path/to/archiveFile.mtar
credentialsId: my-credentials-id
apiUrl: https://example.org/xs
space: mySpace
org:: myOrg
org: myOrg
```
[dockerExecute]: ../dockerExecute

View File

@ -78,28 +78,6 @@ nav:
- uiVeri5ExecuteTests: steps/uiVeri5ExecuteTests.md
- whitesourceExecuteScan: steps/whitesourceExecuteScan.md
- xsDeploy: steps/xsDeploy.md
- 'Pipelines':
- 'General purpose pipeline':
- 'Introduction': stages/introduction.md
- 'Examples': stages/examples.md
- 'Stages':
- 'Init Stage': stages/init.md
- 'Pull-Request Voting Stage': stages/prvoting.md
- 'Build Stage': stages/build.md
- 'Additional Unit Test Stage': stages/additionalunittests.md
- 'Integration Stage': stages/integration.md
- 'Acceptance Stage': stages/acceptance.md
- 'Security Stage': stages/security.md
- 'Performance Stage': stages/performance.md
- 'Compliance': stages/compliance.md
- 'Confirm Stage': stages/confirm.md
- 'Promote Stage': stages/promote.md
- 'Release Stage': stages/release.md
- 'Scenarios':
- 'Build and Deploy Hybrid Applications with Jenkins and SAP Solution Manager': scenarios/changeManagement.md
- 'Build and Deploy SAP UI5 or SAP Fiori Applications on SAP Cloud Platform with Jenkins': scenarios/ui5-sap-cp/Readme.md
- 'Build and Deploy Applications with Jenkins and the SAP Cloud Application Programming Model': scenarios/CAP_Scenario.md
- 'Build and Deploy SAP Fiori Applications for SAP HANA XS Advanced ': scenarios/xsa-deploy/Readme.md
- Resources:
- 'Required Plugins': jenkins/requiredPlugins.md

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.13
require (
github.com/ghodss/yaml v1.0.0
github.com/pkg/errors v0.8.1
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.2.2

6
go.sum
View File

@ -12,6 +12,7 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -21,6 +22,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
@ -30,12 +33,15 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=

9
main.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"github.com/SAP/jenkins-library/cmd"
)
func main() {
cmd.Execute()
}

134
pkg/command/command.go Normal file
View File

@ -0,0 +1,134 @@
package command
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"sync"
"github.com/pkg/errors"
)
// Command defines the information required for executing a call to any executable
type Command struct {
dir string
Stdout io.Writer
Stderr io.Writer
}
// Dir sets the working directory for the execution
func (c *Command) Dir(d string) {
c.dir = d
}
// ExecCommand defines how to execute os commands
var ExecCommand = exec.Command
// RunShell runs the specified command on the shell
func (c *Command) RunShell(shell, script string) error {
_out, _err := prepareOut(c.Stdout, c.Stderr)
cmd := ExecCommand(shell)
cmd.Dir = c.dir
in := bytes.Buffer{}
in.Write([]byte(script))
cmd.Stdin = &in
if err := runCmd(cmd, _out, _err); err != nil {
return errors.Wrapf(err, "running shell script failed with %v", shell)
}
return nil
}
// RunExecutable runs the specified executable with parameters
func (c *Command) RunExecutable(executable string, params ...string) error {
_out, _err := prepareOut(c.Stdout, c.Stderr)
cmd := ExecCommand(executable, params...)
if len(c.dir) > 0 {
cmd.Dir = c.dir
}
if err := runCmd(cmd, _out, _err); err != nil {
return errors.Wrapf(err, "running command '%v' failed", executable)
}
return nil
}
func runCmd(cmd *exec.Cmd, _out, _err io.Writer) error {
stdout, stderr, err := cmdPipes(cmd)
if err != nil {
return errors.Wrap(err, "getting commmand pipes failed")
}
err = cmd.Start()
if err != nil {
return errors.Wrap(err, "starting command failed")
}
var wg sync.WaitGroup
wg.Add(2)
var errStdout, errStderr error
go func() {
_, errStdout = io.Copy(_out, stdout)
wg.Done()
}()
go func() {
_, errStderr = io.Copy(_err, stderr)
wg.Done()
}()
wg.Wait()
err = cmd.Wait()
if err != nil {
return errors.Wrap(err, "cmd.Run() failed")
}
if errStdout != nil || errStderr != nil {
return fmt.Errorf("failed to capture stdout/stderr: '%v'/'%v'", errStdout, errStderr)
}
return nil
}
func prepareOut(stdout, stderr io.Writer) (io.Writer, io.Writer) {
//ToDo: check use of multiwriter instead to always write into os.Stdout and os.Stdin?
//stdout := io.MultiWriter(os.Stdout, &stdoutBuf)
//stderr := io.MultiWriter(os.Stderr, &stderrBuf)
if stdout == nil {
stdout = os.Stdout
}
if stderr == nil {
stderr = os.Stderr
}
return stdout, stderr
}
func cmdPipes(cmd *exec.Cmd) (io.ReadCloser, io.ReadCloser, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, errors.Wrap(err, "getting Stdout pipe failed")
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, nil, errors.Wrap(err, "getting Stderr pipe failed")
}
return stdout, stderr, nil
}

181
pkg/command/command_test.go Normal file
View File

@ -0,0 +1,181 @@
package command
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"testing"
)
//based on https://golang.org/src/os/exec/exec_test.go
func helperCommand(command string, s ...string) (cmd *exec.Cmd) {
cs := []string{"-test.run=TestHelperProcess", "--", command}
cs = append(cs, s...)
cmd = exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}
func TestShellRun(t *testing.T) {
t.Run("test shell", func(t *testing.T) {
ExecCommand = helperCommand
defer func() { ExecCommand = exec.Command }()
o := new(bytes.Buffer)
e := new(bytes.Buffer)
s := Command{Stdout: o, Stderr: e}
s.RunShell("/bin/bash", "myScript")
t.Run("success case", func(t *testing.T) {
t.Run("stdin-stdout", func(t *testing.T) {
expectedOut := "Stdout: command /bin/bash - Stdin: myScript\n"
if oStr := o.String(); oStr != expectedOut {
t.Errorf("expected: %v got: %v", expectedOut, oStr)
}
})
t.Run("stderr", func(t *testing.T) {
expectedErr := "Stderr: command /bin/bash\n"
if eStr := e.String(); eStr != expectedErr {
t.Errorf("expected: %v got: %v", expectedErr, eStr)
}
})
})
})
}
func TestExecutableRun(t *testing.T) {
t.Run("test shell", func(t *testing.T) {
ExecCommand = helperCommand
defer func() { ExecCommand = exec.Command }()
o := new(bytes.Buffer)
e := new(bytes.Buffer)
ex := Command{Stdout: o, Stderr: e}
ex.RunExecutable("echo", []string{"foo bar", "baz"}...)
t.Run("success case", func(t *testing.T) {
t.Run("stdin", func(t *testing.T) {
expectedOut := "foo bar baz\n"
if oStr := o.String(); oStr != expectedOut {
t.Errorf("expected: %v got: %v", expectedOut, oStr)
}
})
t.Run("stderr", func(t *testing.T) {
expectedErr := "Stderr: command echo\n"
if eStr := e.String(); eStr != expectedErr {
t.Errorf("expected: %v got: %v", expectedErr, eStr)
}
})
})
})
}
func TestPrepareOut(t *testing.T) {
t.Run("os", func(t *testing.T) {
s := Command{}
_out, _err := prepareOut(s.Stdout, s.Stderr)
if _out != os.Stdout {
t.Errorf("expected out to be os.Stdout")
}
if _err != os.Stderr {
t.Errorf("expected err to be os.Stderr")
}
})
t.Run("custom", func(t *testing.T) {
o := bytes.NewBufferString("")
e := bytes.NewBufferString("")
s := Command{Stdout: o, Stderr: e}
_out, _err := prepareOut(s.Stdout, s.Stderr)
expectOut := "Test out"
expectErr := "Test err"
_out.Write([]byte(expectOut))
_err.Write([]byte(expectErr))
t.Run("out", func(t *testing.T) {
if o.String() != expectOut {
t.Errorf("expected: %v got: %v", expectOut, o.String())
}
})
t.Run("err", func(t *testing.T) {
if e.String() != expectErr {
t.Errorf("expected: %v got: %v", expectErr, e.String())
}
})
})
}
func TestCmdPipes(t *testing.T) {
cmd := helperCommand("echo", "foo bar", "baz")
defer func() { ExecCommand = exec.Command }()
t.Run("success case", func(t *testing.T) {
o, e, err := cmdPipes(cmd)
t.Run("no error", func(t *testing.T) {
if err != nil {
t.Errorf("error occured but no error expected")
}
})
t.Run("out pipe", func(t *testing.T) {
if o == nil {
t.Errorf("no pipe received")
}
})
t.Run("err pipe", func(t *testing.T) {
if e == nil {
t.Errorf("no pipe received")
}
})
})
}
//based on https://golang.org/src/os/exec/exec_test.go
//this is not directly executed
func TestHelperProcess(*testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "No command\n")
os.Exit(2)
}
cmd, args := args[0], args[1:]
switch cmd {
case "/bin/bash":
o, _ := ioutil.ReadAll(os.Stdin)
fmt.Fprintf(os.Stdout, "Stdout: command %v - Stdin: %v\n", cmd, string(o))
fmt.Fprintf(os.Stderr, "Stderr: command %v\n", cmd)
case "echo":
iargs := []interface{}{}
for _, s := range args {
iargs = append(iargs, s)
}
fmt.Println(iargs...)
fmt.Fprintf(os.Stderr, "Stderr: command %v\n", cmd)
default:
fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd)
os.Exit(2)
}
}

View File

@ -6,6 +6,7 @@ import (
"io"
"io/ioutil"
"os"
"strings"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
@ -39,8 +40,44 @@ func (c *Config) ReadConfig(configuration io.ReadCloser) error {
return nil
}
// ApplyAliasConfig adds configuration values available on aliases to primary configuration parameters
func (c *Config) ApplyAliasConfig(parameters []StepParameters, filters StepFilters, stageName, stepName string) {
for _, p := range parameters {
c.General = setParamValueFromAlias(c.General, filters.General, p)
if c.Stages[stageName] != nil {
c.Stages[stageName] = setParamValueFromAlias(c.Stages[stageName], filters.Stages, p)
}
if c.Steps[stepName] != nil {
c.Steps[stepName] = setParamValueFromAlias(c.Steps[stepName], filters.Steps, p)
}
}
}
func setParamValueFromAlias(configMap map[string]interface{}, filter []string, p StepParameters) map[string]interface{} {
if configMap[p.Name] == nil && sliceContains(filter, p.Name) {
for _, a := range p.Aliases {
configMap[p.Name] = getDeepAliasValue(configMap, a.Name)
if configMap[p.Name] != nil {
return configMap
}
}
}
return configMap
}
func getDeepAliasValue(configMap map[string]interface{}, key string) interface{} {
parts := strings.Split(key, "/")
if len(parts) > 1 {
if configMap[parts[0]] == nil {
return nil
}
return getDeepAliasValue(configMap[parts[0]].(map[string]interface{}), strings.Join(parts[1:], "/"))
}
return configMap[key]
}
// GetStepConfig provides merged step configuration using defaults, config, if available
func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON string, configuration io.ReadCloser, defaults []io.ReadCloser, filters StepFilters, stageName, stepName string) (StepConfig, error) {
func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON string, configuration io.ReadCloser, defaults []io.ReadCloser, filters StepFilters, parameters []StepParameters, stageName, stepName string) (StepConfig, error) {
var stepConfig StepConfig
var d PipelineDefaults
@ -52,6 +89,7 @@ func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON stri
//ignoring unavailability of config file since considered optional
}
}
c.ApplyAliasConfig(parameters, filters, stageName, stepName)
if err := d.ReadPipelineDefaults(defaults); err != nil {
switch err.(type) {
@ -64,6 +102,7 @@ func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON stri
// first: read defaults & merge general -> steps (-> general -> steps ...)
for _, def := range d.Defaults {
def.ApplyAliasConfig(parameters, filters, stageName, stepName)
stepConfig.mixIn(def.General, filters.General)
stepConfig.mixIn(def.Steps[stepName], filters.Steps)
}
@ -80,6 +119,12 @@ func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON stri
if len(paramJSON) != 0 {
var params map[string]interface{}
json.Unmarshal([]byte(paramJSON), &params)
//apply aliases
for _, p := range parameters {
params = setParamValueFromAlias(params, filters.Parameters, p)
}
stepConfig.mixIn(params, filters.Parameters)
}

View File

@ -112,7 +112,7 @@ steps:
defaults := []io.ReadCloser{ioutil.NopCloser(strings.NewReader(defaults1)), ioutil.NopCloser(strings.NewReader(defaults2))}
myConfig := ioutil.NopCloser(strings.NewReader(testConfig))
stepConfig, err := c.GetStepConfig(flags, paramJSON, myConfig, defaults, filters, "stage1", "step1")
stepConfig, err := c.GetStepConfig(flags, paramJSON, myConfig, defaults, filters, []StepParameters{}, "stage1", "step1")
assert.Equal(t, nil, err, "error occured but none expected")
@ -151,7 +151,7 @@ steps:
t.Run("Failure case config", func(t *testing.T) {
var c Config
myConfig := ioutil.NopCloser(strings.NewReader("invalid config"))
_, err := c.GetStepConfig(nil, "", myConfig, nil, StepFilters{}, "stage1", "step1")
_, err := c.GetStepConfig(nil, "", myConfig, nil, StepFilters{}, []StepParameters{}, "stage1", "step1")
assert.EqualError(t, err, "failed to parse custom pipeline configuration: error unmarshalling \"invalid config\": error unmarshaling JSON: json: cannot unmarshal string into Go value of type config.Config", "default error expected")
})
@ -159,7 +159,7 @@ steps:
var c Config
myConfig := ioutil.NopCloser(strings.NewReader(""))
myDefaults := []io.ReadCloser{ioutil.NopCloser(strings.NewReader("invalid defaults"))}
_, err := c.GetStepConfig(nil, "", myConfig, myDefaults, StepFilters{}, "stage1", "step1")
_, err := c.GetStepConfig(nil, "", myConfig, myDefaults, StepFilters{}, []StepParameters{}, "stage1", "step1")
assert.EqualError(t, err, "failed to parse pipeline default configuration: error unmarshalling \"invalid defaults\": error unmarshaling JSON: json: cannot unmarshal string into Go value of type config.Config", "default error expected")
})
@ -187,6 +187,130 @@ func TestGetStepConfigWithJSON(t *testing.T) {
})
}
func TestApplyAliasConfig(t *testing.T) {
p := []StepParameters{
{
Name: "p0",
Aliases: []Alias{
{Name: "p0_notused"},
},
},
{
Name: "p1",
Aliases: []Alias{
{Name: "p1_alias"},
},
},
{
Name: "p2",
Aliases: []Alias{
{Name: "p2_alias/deep/test"},
},
},
{
Name: "p3",
Aliases: []Alias{
{Name: "p3_notused"},
},
},
{
Name: "p4",
Aliases: []Alias{
{Name: "p4_alias"},
{Name: "p4_2nd_alias"},
},
},
{
Name: "p5",
Aliases: []Alias{
{Name: "p5_notused"},
},
},
{
Name: "p6",
Aliases: []Alias{
{Name: "p6_1st_alias"},
{Name: "p6_alias"},
},
},
}
filters := StepFilters{
General: []string{"p1", "p2"},
Stages: []string{"p4"},
Steps: []string{"p6"},
}
c := Config{
General: map[string]interface{}{
"p0_notused": "p0_general",
"p1_alias": "p1_general",
"p2_alias": map[string]interface{}{
"deep": map[string]interface{}{
"test": "p2_general",
},
},
},
Stages: map[string]map[string]interface{}{
"stage1": map[string]interface{}{
"p3_notused": "p3_stage",
"p4_alias": "p4_stage",
},
},
Steps: map[string]map[string]interface{}{
"step1": map[string]interface{}{
"p5_notused": "p5_step",
"p6_alias": "p6_step",
},
},
}
c.ApplyAliasConfig(p, filters, "stage1", "step1")
t.Run("Global", func(t *testing.T) {
assert.Nil(t, c.General["p0"])
assert.Equal(t, "p1_general", c.General["p1"])
assert.Equal(t, "p2_general", c.General["p2"])
})
t.Run("Stage", func(t *testing.T) {
assert.Nil(t, c.General["p3"])
assert.Equal(t, "p4_stage", c.Stages["stage1"]["p4"])
})
t.Run("Stage", func(t *testing.T) {
assert.Nil(t, c.General["p5"])
assert.Equal(t, "p6_step", c.Steps["step1"]["p6"])
})
}
func TestGetDeepAliasValue(t *testing.T) {
c := map[string]interface{}{
"p0": "p0_val",
"p1": 11,
"p2": map[string]interface{}{
"p2_0": "p2_0_val",
"p2_1": map[string]interface{}{
"p2_1_0": "p2_1_0_val",
},
},
}
tt := []struct {
key string
expected interface{}
}{
{key: "p0", expected: "p0_val"},
{key: "p1", expected: 11},
{key: "p2/p2_0", expected: "p2_0_val"},
{key: "p2/p2_1/p2_1_0", expected: "p2_1_0_val"},
}
for k, v := range tt {
assert.Equal(t, v.expected, getDeepAliasValue(c, v.key), fmt.Sprintf("wrong return value for run %v", k+1))
}
}
func TestGetJSON(t *testing.T) {
t.Run("Success case", func(t *testing.T) {

View File

@ -1,6 +1,8 @@
package config
import (
"bytes"
"fmt"
"io"
"io/ioutil"
@ -25,8 +27,8 @@ type StepMetadata struct {
type StepSpec struct {
Inputs StepInputs `json:"inputs"`
// Outputs string `json:"description,omitempty"`
Containers []StepContainers `json:"containers,omitempty"`
Sidecars []StepSidecars `json:"sidecars,omitempty"`
Containers []Container `json:"containers,omitempty"`
Sidecars []Container `json:"sidecars,omitempty"`
}
// StepInputs defines the spec details for a step, like step inputs, containers, sidecars, ...
@ -45,6 +47,13 @@ type StepParameters struct {
Type string `json:"type"`
Mandatory bool `json:"mandatory,omitempty"`
Default interface{} `json:"default,omitempty"`
Aliases []Alias `json:"aliases,omitempty"`
}
// Alias defines a step input parameter alias
type Alias struct {
Name string `json:"name,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
}
// StepResources defines the resources to be provided by the step context, e.g. Jenkins pipeline
@ -66,14 +75,23 @@ type StepSecrets struct {
// Name string `json:"name"`
//}
// StepContainers defines the containers required for a step
type StepContainers struct {
Containers map[string]interface{} `json:"containers"`
// Container defines an execution container
type Container struct {
//ToDo: check dockerOptions, dockerVolumeBind, containerPortMappings, sidecarOptions, sidecarVolumeBind
Command []string `json:"command"`
EnvVars []EnvVar `json:"env"`
Image string `json:"image"`
ImagePullPolicy string `json:"imagePullPolicy"`
Name string `json:"name"`
ReadyCommand string `json:"readyCommand"`
Shell string `json:"shell"`
WorkingDir string `json:"workingDir"`
}
// StepSidecars defines any sidears required for a step
type StepSidecars struct {
Sidecars map[string]interface{} `json:"sidecars"`
// EnvVar defines an environment variable
type EnvVar struct {
Name string `json:"name"`
Value string `json:"value"`
}
// StepFilters defines the filter parameters for the different sections
@ -135,5 +153,97 @@ func (m *StepData) GetContextParameterFilters() StepFilters {
filters.Parameters = append(filters.Parameters, secret.Name)
filters.Env = append(filters.Env, secret.Name)
}
containerFilters := []string{}
if len(m.Spec.Containers) > 0 {
containerFilters = append(containerFilters, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace"}...)
}
if len(m.Spec.Sidecars) > 0 {
//ToDo: support fallback for "dockerName" configuration property -> via aliasing?
containerFilters = append(containerFilters, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}...)
}
if len(containerFilters) > 0 {
filters.All = append(filters.All, containerFilters...)
filters.Steps = append(filters.Steps, containerFilters...)
filters.Stages = append(filters.Stages, containerFilters...)
filters.Parameters = append(filters.Parameters, containerFilters...)
}
return filters
}
// GetContextDefaults retrieves context defaults like container image, name, env vars, resources, ...
// It only supports scenarios with one container and optionally one sidecar
func (m *StepData) GetContextDefaults(stepName string) (io.ReadCloser, error) {
p := map[string]interface{}{}
//ToDo error handling empty Containers/Sidecars
//ToDo handle empty Command
if len(m.Spec.Containers) > 0 {
if len(m.Spec.Containers[0].Command) > 0 {
p["containerCommand"] = m.Spec.Containers[0].Command[0]
}
p["containerName"] = m.Spec.Containers[0].Name
p["containerShell"] = m.Spec.Containers[0].Shell
p["dockerEnvVars"] = envVarsAsStringSlice(m.Spec.Containers[0].EnvVars)
p["dockerImage"] = m.Spec.Containers[0].Image
p["dockerName"] = m.Spec.Containers[0].Name
p["dockerPullImage"] = m.Spec.Containers[0].ImagePullPolicy != "Never"
p["dockerWorkspace"] = m.Spec.Containers[0].WorkingDir
// Ready command not relevant for main runtime container so far
//p[] = m.Spec.Containers[0].ReadyCommand
}
if len(m.Spec.Sidecars) > 0 {
if len(m.Spec.Sidecars[0].Command) > 0 {
p["sidecarCommand"] = m.Spec.Sidecars[0].Command[0]
}
p["sidecarEnvVars"] = envVarsAsStringSlice(m.Spec.Sidecars[0].EnvVars)
p["sidecarImage"] = m.Spec.Sidecars[0].Image
p["sidecarName"] = m.Spec.Sidecars[0].Name
p["sidecarPullImage"] = m.Spec.Sidecars[0].ImagePullPolicy != "Never"
p["sidecarReadyCommand"] = m.Spec.Sidecars[0].ReadyCommand
p["sidecarWorkspace"] = m.Spec.Sidecars[0].WorkingDir
}
// not filled for now since this is not relevant in Kubernetes case
//p["dockerOptions"] = m.Spec.Containers[0].
//p["dockerVolumeBind"] = m.Spec.Containers[0].
//p["containerPortMappings"] = m.Spec.Sidecars[0].
//p["sidecarOptions"] = m.Spec.Sidecars[0].
//p["sidecarVolumeBind"] = m.Spec.Sidecars[0].
if len(m.Spec.Inputs.Resources) > 0 {
var resources []string
for _, resource := range m.Spec.Inputs.Resources {
if resource.Type == "stash" {
resources = append(resources, resource.Name)
}
}
p["stashContent"] = resources
}
c := Config{
Steps: map[string]map[string]interface{}{
stepName: p,
},
}
JSON, err := yaml.Marshal(c)
if err != nil {
return nil, errors.Wrap(err, "failed to create context defaults")
}
r := ioutil.NopCloser(bytes.NewReader(JSON))
return r, nil
}
func envVarsAsStringSlice(envVars []EnvVar) []string {
e := []string{}
for _, v := range envVars {
e = append(e, fmt.Sprintf("%v=%v", v.Name, v.Value))
}
return e
}

View File

@ -2,9 +2,12 @@ package config
import (
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestReadPipelineStepData(t *testing.T) {
@ -227,40 +230,164 @@ func TestGetContextParameterFilters(t *testing.T) {
},
}
filters := metadata1.GetContextParameterFilters()
metadata2 := StepData{
Spec: StepSpec{
Containers: []Container{
{Name: "testcontainer"},
},
},
}
metadata3 := StepData{
Spec: StepSpec{
Sidecars: []Container{
{Name: "testsidecar"},
},
},
}
t.Run("Secrets", func(t *testing.T) {
for _, s := range metadata1.Spec.Inputs.Secrets {
t.Run("All", func(t *testing.T) {
if !sliceContains(filters.All, s.Name) {
t.Errorf("Creation of context filter failed, expected: %v to be contained", s.Name)
}
})
t.Run("General", func(t *testing.T) {
if !sliceContains(filters.General, s.Name) {
t.Errorf("Creation of context filter failed, expected: %v to be contained", s.Name)
}
})
t.Run("Step", func(t *testing.T) {
if !sliceContains(filters.Steps, s.Name) {
t.Errorf("Creation of context filter failed, expected: %v to be contained", s.Name)
}
})
t.Run("Stages", func(t *testing.T) {
if !sliceContains(filters.Steps, s.Name) {
t.Errorf("Creation of context filter failed, expected: %v to be contained", s.Name)
}
})
t.Run("Parameters", func(t *testing.T) {
if !sliceContains(filters.Parameters, s.Name) {
t.Errorf("Creation of context filter failed, expected: %v to be contained", s.Name)
}
})
t.Run("Env", func(t *testing.T) {
if !sliceContains(filters.Env, s.Name) {
t.Errorf("Creation of context filter failed, expected: %v to be contained", s.Name)
}
})
}
filters := metadata1.GetContextParameterFilters()
assert.Equal(t, []string{"testSecret1", "testSecret2"}, filters.All, "incorrect filter All")
assert.Equal(t, []string{"testSecret1", "testSecret2"}, filters.General, "incorrect filter General")
assert.Equal(t, []string{"testSecret1", "testSecret2"}, filters.Steps, "incorrect filter Steps")
assert.Equal(t, []string{"testSecret1", "testSecret2"}, filters.Stages, "incorrect filter Stages")
assert.Equal(t, []string{"testSecret1", "testSecret2"}, filters.Parameters, "incorrect filter Parameters")
assert.Equal(t, []string{"testSecret1", "testSecret2"}, filters.Env, "incorrect filter Env")
})
t.Run("Containers", func(t *testing.T) {
filters := metadata2.GetContextParameterFilters()
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace"}, filters.All, "incorrect filter All")
assert.NotEqual(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace"}, filters.General, "incorrect filter General")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace"}, filters.Steps, "incorrect filter Steps")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace"}, filters.Stages, "incorrect filter Stages")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace"}, filters.Parameters, "incorrect filter Parameters")
assert.NotEqual(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace"}, filters.Env, "incorrect filter Env")
})
t.Run("Sidecars", func(t *testing.T) {
filters := metadata3.GetContextParameterFilters()
assert.Equal(t, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}, filters.All, "incorrect filter All")
assert.NotEqual(t, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}, filters.General, "incorrect filter General")
assert.Equal(t, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}, filters.Steps, "incorrect filter Steps")
assert.Equal(t, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}, filters.Stages, "incorrect filter Stages")
assert.Equal(t, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}, filters.Parameters, "incorrect filter Parameters")
assert.NotEqual(t, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}, filters.Env, "incorrect filter Env")
})
}
func TestGetContextDefaults(t *testing.T) {
t.Run("Positive case", func(t *testing.T) {
metadata := StepData{
Spec: StepSpec{
Inputs: StepInputs{
Resources: []StepResources{
{
Name: "buildDescriptor",
Type: "stash",
},
{
Name: "source",
Type: "stash",
},
{
Name: "test",
Type: "nonce",
},
},
},
Containers: []Container{
{
Command: []string{"test/command"},
EnvVars: []EnvVar{
{Name: "env1", Value: "val1"},
{Name: "env2", Value: "val2"},
},
Name: "testcontainer",
Image: "testImage:tag",
Shell: "/bin/bash",
WorkingDir: "/test/dir",
},
},
Sidecars: []Container{
{
Command: []string{"/sidecar/command"},
EnvVars: []EnvVar{
{Name: "env3", Value: "val3"},
{Name: "env4", Value: "val4"},
},
Name: "testsidecar",
Image: "testSidecarImage:tag",
ImagePullPolicy: "Never",
ReadyCommand: "/sidecar/command",
WorkingDir: "/sidecar/dir",
},
},
},
}
cd, err := metadata.GetContextDefaults("testStep")
t.Run("No error", func(t *testing.T) {
if err != nil {
t.Errorf("No error expected but got error '%v'", err)
}
})
var d PipelineDefaults
d.ReadPipelineDefaults([]io.ReadCloser{cd})
assert.Equal(t, []interface{}{"buildDescriptor", "source"}, d.Defaults[0].Steps["testStep"]["stashContent"], "stashContent default not available")
assert.Equal(t, "test/command", d.Defaults[0].Steps["testStep"]["containerCommand"], "containerCommand default not available")
assert.Equal(t, "testcontainer", d.Defaults[0].Steps["testStep"]["containerName"], "containerName default not available")
assert.Equal(t, "/bin/bash", d.Defaults[0].Steps["testStep"]["containerShell"], "containerShell default not available")
assert.Equal(t, []interface{}{"env1=val1", "env2=val2"}, d.Defaults[0].Steps["testStep"]["dockerEnvVars"], "dockerEnvVars default not available")
assert.Equal(t, "testImage:tag", d.Defaults[0].Steps["testStep"]["dockerImage"], "dockerImage default not available")
assert.Equal(t, "testcontainer", d.Defaults[0].Steps["testStep"]["dockerName"], "dockerName default not available")
assert.Equal(t, true, d.Defaults[0].Steps["testStep"]["dockerPullImage"], "dockerPullImage default not available")
assert.Equal(t, "/test/dir", d.Defaults[0].Steps["testStep"]["dockerWorkspace"], "dockerWorkspace default not available")
assert.Equal(t, "/sidecar/command", d.Defaults[0].Steps["testStep"]["sidecarCommand"], "sidecarCommand default not available")
assert.Equal(t, []interface{}{"env3=val3", "env4=val4"}, d.Defaults[0].Steps["testStep"]["sidecarEnvVars"], "sidecarEnvVars default not available")
assert.Equal(t, "testSidecarImage:tag", d.Defaults[0].Steps["testStep"]["sidecarImage"], "sidecarImage default not available")
assert.Equal(t, "testsidecar", d.Defaults[0].Steps["testStep"]["sidecarName"], "sidecarName default not available")
assert.Equal(t, false, d.Defaults[0].Steps["testStep"]["sidecarPullImage"], "sidecarPullImage default not available")
assert.Equal(t, "/sidecar/command", d.Defaults[0].Steps["testStep"]["sidecarReadyCommand"], "sidecarReadyCommand default not available")
assert.Equal(t, "/sidecar/dir", d.Defaults[0].Steps["testStep"]["sidecarWorkspace"], "sidecarWorkspace default not available")
})
t.Run("Negative case", func(t *testing.T) {
metadataErr := []StepData{
StepData{},
StepData{
Spec: StepSpec{},
},
StepData{
Spec: StepSpec{
Containers: []Container{},
Sidecars: []Container{},
},
},
}
t.Run("No containers/sidecars", func(t *testing.T) {
cd, _ := metadataErr[0].GetContextDefaults("testStep")
var d PipelineDefaults
d.ReadPipelineDefaults([]io.ReadCloser{cd})
//no assert since we just want to make sure that no panic occurs
})
t.Run("No command", func(t *testing.T) {
cd, _ := metadataErr[1].GetContextDefaults("testStep")
var d PipelineDefaults
d.ReadPipelineDefaults([]io.ReadCloser{cd})
//no assert since we just want to make sure that no panic occurs
})
})
}

View File

@ -0,0 +1,315 @@
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
"github.com/SAP/jenkins-library/pkg/config"
)
type stepInfo struct {
CobraCmdFuncName string
CreateCmdVar string
FlagsFunc string
Long string
Metadata []config.StepParameters
OSImport bool
Short string
StepFunc string
StepName string
}
//StepGoTemplate ...
const stepGoTemplate = `package cmd
import (
//"os"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/spf13/cobra"
)
type {{ .StepName }}Options struct {
{{- range $key, $value := .Metadata }}
{{ $value.Name | golangName }} {{ $value.Type }} ` + "`json:\"{{$value.Name}},omitempty\"`" + `{{end}}
}
var my{{ .StepName | title}}Options {{.StepName}}Options
var {{ .StepName }}StepConfigJSON string
// {{.CobraCmdFuncName}} {{.Short}}
func {{.CobraCmdFuncName}}() *cobra.Command {
metadata := {{ .StepName }}Metadata()
var {{.CreateCmdVar}} = &cobra.Command{
Use: "{{.StepName}}",
Short: "{{.Short}}",
Long: {{ $tick := "` + "`" + `" }}{{ $tick }}{{.Long | longName }}{{ $tick }},
PreRunE: func(cmd *cobra.Command, args []string) error {
log.SetStepName("{{ .StepName }}")
log.SetVerbose(generalConfig.verbose)
return PrepareConfig(cmd, &metadata, "{{ .StepName }}", &my{{ .StepName | title}}Options, openPiperFile)
},
RunE: func(cmd *cobra.Command, args []string) error {
return {{.StepName}}(my{{ .StepName | title }}Options)
},
}
{{.FlagsFunc}}({{.CreateCmdVar}})
return {{.CreateCmdVar}}
}
func {{.FlagsFunc}}(cmd *cobra.Command) {
{{- range $key, $value := .Metadata }}
cmd.Flags().{{ $value.Type | flagType }}(&my{{ $.StepName | title }}Options.{{ $value.Name | golangName }}, "{{ $value.Name }}", {{ $value.Default }}, "{{ $value.Description }}"){{ end }}
{{- printf "\n" }}
{{- range $key, $value := .Metadata }}{{ if $value.Mandatory }}
cmd.MarkFlagRequired("{{ $value.Name }}"){{ end }}{{ end }}
}
// retrieve step metadata
func {{ .StepName }}Metadata() config.StepData {
var theMetaData = config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{{- range $key, $value := .Metadata }}
{
Name: "{{ $value.Name }}",
Scope: []string{{ "{" }}{{ range $notused, $scope := $value.Scope }}"{{ $scope }}",{{ end }}{{ "}" }},
Type: "{{ $value.Type }}",
Mandatory: {{ $value.Mandatory }},
},{{ end }}
},
},
},
}
return theMetaData
}
`
//StepTestGoTemplate ...
const stepTestGoTemplate = `package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test{{.CobraCmdFuncName}}(t *testing.T) {
testCmd := {{.CobraCmdFuncName}}()
// only high level testing performed - details are tested in step generation procudure
assert.Equal(t, "{{.StepName}}", testCmd.Use, "command name incorrect")
}
`
func main() {
metadataPath := "./resources/metadata"
metadataFiles, err := metadataFiles(metadataPath)
checkError(err)
err = processMetaFiles(metadataFiles, openMetaFile, fileWriter)
checkError(err)
cmd := exec.Command("go", "fmt", "./cmd")
err = cmd.Run()
checkError(err)
}
func processMetaFiles(metadataFiles []string, openFile func(s string) (io.ReadCloser, error), writeFile func(filename string, data []byte, perm os.FileMode) error) error {
for key := range metadataFiles {
var stepData config.StepData
configFilePath := metadataFiles[key]
metadataFile, err := openFile(configFilePath)
checkError(err)
defer metadataFile.Close()
fmt.Printf("Reading file %v\n", configFilePath)
err = stepData.ReadPipelineStepData(metadataFile)
checkError(err)
fmt.Printf("Step name: %v\n", stepData.Metadata.Name)
err = setDefaultParameters(&stepData)
checkError(err)
myStepInfo := getStepInfo(&stepData)
step := stepTemplate(myStepInfo)
err = writeFile(fmt.Sprintf("cmd/%v_generated.go", stepData.Metadata.Name), step, 0644)
checkError(err)
test := stepTestTemplate(myStepInfo)
err = writeFile(fmt.Sprintf("cmd/%v_generated_test.go", stepData.Metadata.Name), test, 0644)
checkError(err)
}
return nil
}
func openMetaFile(name string) (io.ReadCloser, error) {
return os.Open(name)
}
func fileWriter(filename string, data []byte, perm os.FileMode) error {
return ioutil.WriteFile(filename, data, perm)
}
func setDefaultParameters(stepData *config.StepData) error {
//ToDo: custom function for default handling, support all relevant parameter types
for k, param := range stepData.Spec.Inputs.Parameters {
if param.Default == nil {
switch param.Type {
case "string":
param.Default = fmt.Sprintf("os.Getenv(\"PIPER_%v\")", param.Name)
case "bool":
// ToDo: Check if default should be read from env
param.Default = "false"
case "[]string":
// ToDo: Check if default should be read from env
param.Default = "[]string{}"
default:
return fmt.Errorf("Meta data type not set or not known: '%v'", param.Type)
}
} else {
switch param.Type {
case "string":
param.Default = fmt.Sprintf("\"%v\"", param.Default)
case "bool":
boolVal := "false"
if param.Default.(bool) == true {
boolVal = "true"
}
param.Default = boolVal
case "[]string":
param.Default = fmt.Sprintf("[]string{\"%v\"}", strings.Join(param.Default.([]string), "\", \""))
default:
return fmt.Errorf("Meta data type not set or not known: '%v'", param.Type)
}
}
stepData.Spec.Inputs.Parameters[k] = param
}
return nil
}
func getStepInfo(stepData *config.StepData) stepInfo {
return stepInfo{
StepName: stepData.Metadata.Name,
CobraCmdFuncName: fmt.Sprintf("%vCommand", strings.Title(stepData.Metadata.Name)),
CreateCmdVar: fmt.Sprintf("create%vCmd", strings.Title(stepData.Metadata.Name)),
Short: stepData.Metadata.Description,
Long: stepData.Metadata.LongDescription,
Metadata: stepData.Spec.Inputs.Parameters,
FlagsFunc: fmt.Sprintf("add%vFlags", strings.Title(stepData.Metadata.Name)),
}
}
func checkError(err error) {
if err != nil {
fmt.Printf("Error occured: %v\n", err)
os.Exit(1)
}
}
func metadataFiles(sourceDirectory string) ([]string, error) {
var metadataFiles []string
err := filepath.Walk(sourceDirectory, func(path string, info os.FileInfo, err error) error {
if filepath.Ext(path) == ".yaml" {
metadataFiles = append(metadataFiles, path)
}
return nil
})
if err != nil {
return metadataFiles, nil
}
return metadataFiles, nil
}
func stepTemplate(myStepInfo stepInfo) []byte {
funcMap := template.FuncMap{
"flagType": flagType,
"golangName": golangName,
"title": strings.Title,
"longName": longName,
}
tmpl, err := template.New("step").Funcs(funcMap).Parse(stepGoTemplate)
checkError(err)
var generatedCode bytes.Buffer
err = tmpl.Execute(&generatedCode, myStepInfo)
checkError(err)
return generatedCode.Bytes()
}
func stepTestTemplate(myStepInfo stepInfo) []byte {
funcMap := template.FuncMap{
"flagType": flagType,
"golangName": golangName,
"title": strings.Title,
}
tmpl, err := template.New("stepTest").Funcs(funcMap).Parse(stepTestGoTemplate)
checkError(err)
var generatedCode bytes.Buffer
err = tmpl.Execute(&generatedCode, myStepInfo)
checkError(err)
return generatedCode.Bytes()
}
func longName(long string) string {
l := strings.ReplaceAll(long, "`", "` + \"`\" + `")
l = strings.TrimSpace(l)
return l
}
func golangName(name string) string {
properName := strings.Replace(name, "Api", "API", -1)
properName = strings.Replace(properName, "Url", "URL", -1)
properName = strings.Replace(properName, "Id", "ID", -1)
properName = strings.Replace(properName, "Json", "JSON", -1)
properName = strings.Replace(properName, "json", "JSON", -1)
return strings.Title(properName)
}
func flagType(paramType string) string {
var theFlagType string
switch paramType {
case "bool":
theFlagType = "BoolVar"
case "string":
theFlagType = "StringVar"
case "[]string":
theFlagType = "StringSliceVar"
default:
fmt.Printf("Meta data type not set or not known: '%v'\n", paramType)
os.Exit(1)
}
return theFlagType
}

View File

@ -0,0 +1,227 @@
package main
import (
//"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/stretchr/testify/assert"
)
func configOpenFileMock(name string) (io.ReadCloser, error) {
meta1 := `metadata:
name: testStep
description: Test description
longDescription: |
Long Test description
spec:
inputs:
params:
- name: param0
type: string
description: param0 description
default: val0
scope:
- GENERAL
- PARAMETERS
mandatory: true
- name: param1
type: string
description: param1 description
scope:
- PARAMETERS
- name: param2
type: string
description: param1 description
scope:
- PARAMETERS
mandatory: true
`
var r string
switch name {
case "test.yaml":
r = meta1
default:
r = ""
}
return ioutil.NopCloser(strings.NewReader(r)), nil
}
var files map[string][]byte
func writeFileMock(filename string, data []byte, perm os.FileMode) error {
if files == nil {
files = make(map[string][]byte)
}
files[filename] = data
return nil
}
func TestProcessMetaFiles(t *testing.T) {
processMetaFiles([]string{"test.yaml"}, configOpenFileMock, writeFileMock)
t.Run("step code", func(t *testing.T) {
goldenFilePath := filepath.Join("testdata", t.Name()+"_generated.golden")
expected, err := ioutil.ReadFile(goldenFilePath)
if err != nil {
t.Fatalf("failed reading %v", goldenFilePath)
}
assert.Equal(t, expected, files["cmd/testStep_generated.go"])
})
t.Run("test code", func(t *testing.T) {
goldenFilePath := filepath.Join("testdata", t.Name()+"_generated.golden")
expected, err := ioutil.ReadFile(goldenFilePath)
if err != nil {
t.Fatalf("failed reading %v", goldenFilePath)
}
assert.Equal(t, expected, files["cmd/testStep_generated_test.go"])
})
}
func TestSetDefaultParameters(t *testing.T) {
t.Run("success case", func(t *testing.T) {
stepData := config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param0", Scope: []string{"GENERAL"}, Type: "string", Default: "val0"},
{Name: "param1", Scope: []string{"STEPS"}, Type: "string"},
{Name: "param2", Scope: []string{"STAGES"}, Type: "bool", Default: true},
{Name: "param3", Scope: []string{"PARAMETERS"}, Type: "bool"},
{Name: "param4", Scope: []string{"ENV"}, Type: "[]string", Default: []string{"val4_1", "val4_2"}},
{Name: "param5", Scope: []string{"ENV"}, Type: "[]string"},
},
},
},
}
expected := []string{
"\"val0\"",
"os.Getenv(\"PIPER_param1\")",
"true",
"false",
"[]string{\"val4_1\", \"val4_2\"}",
"[]string{}",
}
err := setDefaultParameters(&stepData)
assert.NoError(t, err, "error occured but none expected")
for k, v := range expected {
assert.Equal(t, v, stepData.Spec.Inputs.Parameters[k].Default, fmt.Sprintf("default not correct for parameter %v", k))
}
})
t.Run("error case", func(t *testing.T) {
stepData := []config.StepData{
{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param0", Scope: []string{"GENERAL"}, Type: "int", Default: 10},
{Name: "param1", Scope: []string{"GENERAL"}, Type: "int"},
},
},
},
},
{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param1", Scope: []string{"GENERAL"}, Type: "int"},
},
},
},
},
}
for k, v := range stepData {
err := setDefaultParameters(&v)
assert.Error(t, err, fmt.Sprintf("error expected but none occured for parameter %v", k))
}
})
}
func TestGetStepInfo(t *testing.T) {
stepData := config.StepData{
Metadata: config.StepMetadata{
Name: "testStep",
Description: "Test description",
LongDescription: "Long Test description",
},
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param0", Scope: []string{"GENERAL"}, Type: "string", Default: "test"},
},
},
},
}
myStepInfo := getStepInfo(&stepData)
assert.Equal(t, "testStep", myStepInfo.StepName, "StepName incorrect")
assert.Equal(t, "TestStepCommand", myStepInfo.CobraCmdFuncName, "CobraCmdFuncName incorrect")
assert.Equal(t, "createTestStepCmd", myStepInfo.CreateCmdVar, "CreateCmdVar incorrect")
assert.Equal(t, "Test description", myStepInfo.Short, "Short incorrect")
assert.Equal(t, "Long Test description", myStepInfo.Long, "Long incorrect")
assert.Equal(t, stepData.Spec.Inputs.Parameters, myStepInfo.Metadata, "Metadata incorrect")
assert.Equal(t, "addTestStepFlags", myStepInfo.FlagsFunc, "FlagsFunc incorrect")
}
func TestLongName(t *testing.T) {
tt := []struct {
input string
expected string
}{
{input: "my long name with no ticks", expected: "my long name with no ticks"},
{input: "my long name with `ticks`", expected: "my long name with ` + \"`\" + `ticks` + \"`\" + `"},
}
for k, v := range tt {
assert.Equal(t, v.expected, longName(v.input), fmt.Sprintf("wrong long name for run %v", k))
}
}
func TestGolangName(t *testing.T) {
tt := []struct {
input string
expected string
}{
{input: "testApi", expected: "TestAPI"},
{input: "testUrl", expected: "TestURL"},
{input: "testId", expected: "TestID"},
{input: "testJson", expected: "TestJSON"},
{input: "jsonTest", expected: "JSONTest"},
}
for k, v := range tt {
assert.Equal(t, v.expected, golangName(v.input), fmt.Sprintf("wrong golang name for run %v", k))
}
}
func TestFlagType(t *testing.T) {
tt := []struct {
input string
expected string
}{
{input: "bool", expected: "BoolVar"},
{input: "string", expected: "StringVar"},
{input: "[]string", expected: "StringSliceVar"},
}
for k, v := range tt {
assert.Equal(t, v.expected, flagType(v.input), fmt.Sprintf("wrong flag type for run %v", k))
}
}

View File

@ -0,0 +1,79 @@
package cmd
import (
//"os"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/spf13/cobra"
)
type testStepOptions struct {
Param0 string `json:"param0,omitempty"`
Param1 string `json:"param1,omitempty"`
Param2 string `json:"param2,omitempty"`
}
var myTestStepOptions testStepOptions
var testStepStepConfigJSON string
// TestStepCommand Test description
func TestStepCommand() *cobra.Command {
metadata := testStepMetadata()
var createTestStepCmd = &cobra.Command{
Use: "testStep",
Short: "Test description",
Long: `Long Test description`,
PreRunE: func(cmd *cobra.Command, args []string) error {
log.SetStepName("testStep")
log.SetVerbose(generalConfig.verbose)
return PrepareConfig(cmd, &metadata, "testStep", &myTestStepOptions, openPiperFile)
},
RunE: func(cmd *cobra.Command, args []string) error {
return testStep(myTestStepOptions)
},
}
addTestStepFlags(createTestStepCmd)
return createTestStepCmd
}
func addTestStepFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&myTestStepOptions.Param0, "param0", "val0", "param0 description")
cmd.Flags().StringVar(&myTestStepOptions.Param1, "param1", os.Getenv("PIPER_param1"), "param1 description")
cmd.Flags().StringVar(&myTestStepOptions.Param2, "param2", os.Getenv("PIPER_param2"), "param1 description")
cmd.MarkFlagRequired("param0")
cmd.MarkFlagRequired("param2")
}
// retrieve step metadata
func testStepMetadata() config.StepData {
var theMetaData = config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{
Name: "param0",
Scope: []string{"GENERAL","PARAMETERS",},
Type: "string",
Mandatory: true,
},
{
Name: "param1",
Scope: []string{"PARAMETERS",},
Type: "string",
Mandatory: false,
},
{
Name: "param2",
Scope: []string{"PARAMETERS",},
Type: "string",
Mandatory: true,
},
},
},
},
}
return theMetaData
}

View File

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

28
pkg/log/log.go Normal file
View File

@ -0,0 +1,28 @@
package log
import (
"github.com/sirupsen/logrus"
)
var logger *logrus.Entry
// Entry returns the logger entry or creates one if none is present.
func Entry() *logrus.Entry {
if logger == nil {
logger = logrus.WithField("library", "sap/jenkins-library")
}
return logger
}
// SetVerbose sets the log level with respect to verbose flag.
func SetVerbose(verbose bool) {
if verbose {
//Logger().Debugf("logging set to level: %s", level)
logrus.SetLevel(logrus.DebugLevel)
}
}
// SetStepName sets the stepName field.
func SetStepName(stepName string) {
logger = Entry().WithField("stepName", stepName)
}

View File

@ -139,6 +139,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>fr.opensagres.js</groupId>
<artifactId>minimatch.java</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>

View File

@ -167,10 +167,10 @@ steps:
- 'deployDescriptor'
- 'pipelineConfigAndTests'
cf_native:
dockerImage: 's4sdk/docker-cf-cli'
dockerImage: 'ppiper/cf-cli'
dockerWorkspace: '/home/piper'
mtaDeployPlugin:
dockerImage: 's4sdk/docker-cf-cli'
dockerImage: 'ppiper/cf-cli'
dockerWorkspace: '/home/piper'
containerExecuteStructureTests:
containerCommand: '/busybox/tail -f /dev/null'
@ -328,7 +328,7 @@ steps:
mtaJarLocation: '/opt/sap/mta/lib/mta.jar'
dockerImage: 'ppiper/mta-archive-builder'
neoDeploy:
dockerImage: 's4sdk/docker-neo-cli'
dockerImage: 'ppiper/neo-cli'
deployMode: 'mta'
warAction: 'deploy'
extensions: []
@ -440,14 +440,14 @@ steps:
noDefaultExludes: []
pipelineStashFilesBeforeBuild:
stashIncludes:
buildDescriptor: '**/pom.xml, **/.mvn/**, **/assembly.xml, **/.swagger-codegen-ignore, **/package.json, **/requirements.txt, **/setup.py, **/mta*.y*ml, **/.npmrc, Dockerfile, .hadolint.yaml, **/VERSION, **/version.txt, **/Gopkg.*, **/dub.json, **/dub.sdl, **/build.sbt, **/sbtDescriptor.json, **/project/*'
buildDescriptor: '**/pom.xml, **/.mvn/**, **/assembly.xml, **/.swagger-codegen-ignore, **/package.json, **/requirements.txt, **/setup.py, **/mta*.y*ml, **/.npmrc, Dockerfile, .hadolint.yaml, **/VERSION, **/version.txt, **/Gopkg.*, **/dub.json, **/dub.sdl, **/build.sbt, **/sbtDescriptor.json, **/project/*, **/ui5.yaml, **/ui5.yml'
deployDescriptor: '**/manifest*.y*ml, **/*.mtaext.y*ml, **/*.mtaext, **/xs-app.json, helm/**, *.y*ml'
git: '.git/**'
opa5: '**/*.*'
opensourceConfiguration: '**/srcclr.yml, **/vulas-custom.properties, **/.nsprc, **/.retireignore, **/.retireignore.json, **/.snyk, **/wss-unified-agent.config, **/vendor/**/*'
pipelineConfigAndTests: '.pipeline/**'
securityDescriptor: '**/xs-security.json'
tests: '**/pom.xml, **/*.json, **/*.xml, **/src/**, **/node_modules/**, **/specs/**, **/env/**, **/*.js, **/tests/**'
tests: '**/pom.xml, **/*.json, **/*.xml, **/src/**, **/node_modules/**, **/specs/**, **/env/**, **/*.js, **/tests/**, **/*.html, **/*.css, **/*.properties'
stashExcludes:
buildDescriptor: '**/node_modules/**/package.json'
deployDescriptor: ''
@ -526,7 +526,7 @@ steps:
archive: false
active: false
cobertura:
pattern: '**/target/coverage/cobertura-coverage.xml'
pattern: '**/target/coverage/**/cobertura-coverage.xml'
onlyStableBuilds: true
allowEmptyResults: true
archive: false

View File

@ -0,0 +1,67 @@
metadata:
name: karmaExecuteTests
description: Executes the Karma test runner
longDescription: |
In this step the ([Karma test runner](http://karma-runner.github.io)) is executed.
The step is using the `seleniumExecuteTest` step to spin up two containers in a Docker network:
* a Selenium/Chrome container (`selenium/standalone-chrome`)
* a NodeJS container (`node:8-stretch`)
In the Docker network, the containers can be referenced by the values provided in `dockerName` and `sidecarName`, the default values are `karma` and `selenium`. These values must be used in the `hostname` properties of the test configuration ([Karma](https://karma-runner.github.io/1.0/config/configuration-file.html) and [WebDriver](https://github.com/karma-runner/karma-webdriver-launcher#usage)).
!!! note
In a Kubernetes environment, the containers both need to be referenced with `localhost`.
spec:
inputs:
resources:
- name: buildDescriptor
type: stash
- name: tests
type: stash
params:
- name: installCommand
type: string
description: The command that is executed to install the test tool.
default: npm install --quiet
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: modulePath
type: string
description: Define the path of the module to execute tests on.
default: '.'
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: runCommand
type: string
description: The command that is executed to start the tests.
default: npm run karma
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
mandatory: true
#outputs:
containers:
- name: maven
image: maven:3.5-jdk-8
volumeMounts:
- mountPath: /dev/shm
name: dev-shm
sidecars:
- image: selenium/standalone-chrome
name: selenium
securityContext:
privileged: true
volumeMounts:
- mountPath: /dev/shm
name: dev-shm

View File

@ -0,0 +1,5 @@
metadata:
name: version
description: Returns the version of the piper binary
longDescription: |
Writes the commit hash and the tag (if any) to stdout and exits with 0.

View File

@ -152,7 +152,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
cfManifest: 'test.yml'
])
// asserts
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry('STATUS_CODE', "${200}"))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -204,7 +204,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
]
])
// asserts
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry('STATUS_CODE', "${200}"))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -278,7 +278,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
cfManifest: 'test.yml'
])
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -306,7 +306,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
cfManifest: 'test.yml'
])
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -335,7 +335,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
cfManifest: 'test.yml'
])
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -373,7 +373,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
cfManifest: 'test.yml'
])
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -469,7 +469,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
mtaPath: 'target/test.mtar'
])
// asserts
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
assertThat(shellRule.shell, hasItem(containsString('cf deploy target/test.mtar -f')))
@ -545,7 +545,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
cfManifestVariablesFiles: ['vars.yml']
])
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry('STATUS_CODE', "${200}"))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -558,7 +558,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
@Test
void testCfPushDeploymentWithVariableSubstitutionFromNotExistingFilePrintsWarning() {
readYamlRule.registerYaml('test.yml', "applications: [[name: '((appName))']]")
fileExistsRule.registerExistingFile('test.yml')
fileExistsRule.registerExistingFile('test.yml')
stepRule.step.cloudFoundryDeploy([
script: nullScript,
@ -574,15 +574,15 @@ class CloudFoundryDeployTest extends BasePiperTest {
])
// asserts
assertThat(shellRule.shell, hasItem(containsString("cf push testAppName -f 'test.yml'")))
assertThat(shellRule.shell, hasItem(containsString("cf push testAppName -f 'test.yml'")))
assertThat(loggingRule.log, containsString("[WARNING] We skip adding not-existing file 'vars.yml' as a vars-file to the cf create-service-push call"))
}
@Test
void testCfPushDeploymentWithVariableSubstitutionFromVarsList() {
readYamlRule.registerYaml('test.yml', "applications: [[name: '((appName))']]")
List varsList = [["appName" : "testApplicationFromVarsList"]]
List varsList = [["appName" : "testApplicationFromVarsList"]]
stepRule.step.cloudFoundryDeploy([
script: nullScript,
juStabUtils: utils,
@ -597,7 +597,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
])
// asserts
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry('STATUS_CODE', "${200}"))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -609,8 +609,8 @@ class CloudFoundryDeployTest extends BasePiperTest {
@Test
void testCfPushDeploymentWithVariableSubstitutionFromVarsListNotAList() {
readYamlRule.registerYaml('test.yml', "applications: [[name: '((appName))']]")
readYamlRule.registerYaml('test.yml', "applications: [[name: '((appName))']]")
thrown.expect(hudson.AbortException)
thrown.expectMessage('[cloudFoundryDeploy] ERROR: Parameter config.cloudFoundry.manifestVariables is not a List!')
@ -626,7 +626,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
cfManifest: 'test.yml',
cfManifestVariables: 'notAList'
])
}
@Test
@ -650,7 +650,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
])
// asserts
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry('STATUS_CODE', "${200}"))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -674,8 +674,8 @@ class CloudFoundryDeployTest extends BasePiperTest {
cfManifest: 'test.yml'
])
// asserts
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
// asserts
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry('STATUS_CODE', "${200}"))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -720,7 +720,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
assertNotNull(testYamlData)
assertThat(testYamlData.get("applications").get(0).get(0).get("name"), is("testApplication"))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry('STATUS_CODE', "${200}"))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
@ -766,7 +766,7 @@ class CloudFoundryDeployTest extends BasePiperTest {
assertNotNull(testYamlData)
assertThat(testYamlData.get("applications").get(0).get(0).get("name"), is("testApplicationFromVarsList"))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 's4sdk/docker-cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerImage', 'ppiper/cf-cli'))
assertThat(dockerExecuteRule.dockerParams, hasEntry('dockerWorkspace', '/home/piper'))
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry('STATUS_CODE', "${200}"))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))

View File

@ -92,7 +92,7 @@ class MulticloudDeployTest extends BasePiperTest {
deployType: 'blue-green',
keepOldInstance: true,
cf_native: [
dockerImage: 's4sdk/docker-cf-cli',
dockerImage: 'ppiper/cf-cli',
dockerWorkspace: '/home/piper'
]
]

View File

@ -12,6 +12,7 @@ import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertTrue
import util.Rules
import minimatch.Minimatch
class TestsPublishResultsTest extends BasePiperTest {
Map publisherStepOptions
@ -90,14 +91,20 @@ class TestsPublishResultsTest extends BasePiperTest {
stepRule.step.testsPublishResults(script: nullScript, jacoco: true, cobertura: true)
assertTrue('JaCoCo options are empty', publisherStepOptions.jacoco != null)
assertTrue('Cobertura options are empty', publisherStepOptions.cobertura != null)
assertEquals('JaCoCo default pattern not set correct',
'**/target/*.exec', publisherStepOptions.jacoco.execPattern)
assertEquals('Cobertura default pattern not set correct',
'**/target/coverage/cobertura-coverage.xml', publisherStepOptions.cobertura.coberturaReportFile)
// ensure nothing else is published
assertTrue('JUnit options are not empty', publisherStepOptions.junit == null)
assertTrue('JMeter options are not empty', publisherStepOptions.jmeter == null)
assertTrue('Cobertura options are empty', publisherStepOptions.cobertura != null)
assertTrue('Cobertura default pattern is empty', publisherStepOptions.cobertura.coberturaReportFile != null)
String sampleCoberturaPathForJava = 'my/workspace/my/project/target/coverage/cobertura-coverage.xml'
assertTrue('Cobertura default pattern does not match files at target/coverage/cobertura-coverage.xml for Java projects',
Minimatch.minimatch(sampleCoberturaPathForJava, publisherStepOptions.cobertura.coberturaReportFile))
String sampleCoberturaPathForKarma = 'my/workspace/my/project/target/coverage/Chrome 78.0.3904 (Mac OS X 10.14.6)/cobertura-coverage.xml'
assertTrue('Cobertura default pattern does not match files at target/coverage/<browser>/cobertura-coverage.xml for UI5 projects',
Minimatch.minimatch(sampleCoberturaPathForKarma, publisherStepOptions.cobertura.coberturaReportFile))
}
@Test
@ -145,7 +152,7 @@ class TestsPublishResultsTest extends BasePiperTest {
}]
}]
}
stepRule.step.testsPublishResults(script: nullScript)
assertJobStatusSuccess()
}

View File

@ -39,7 +39,7 @@ import hudson.AbortException
'containerEnvVars',
/**
* A map of docker image to the name of the container. The pod will be created with all the images from this map and they are labled based on the value field of each map entry.
* Example: `['maven:3.5-jdk-8-alpine': 'mavenExecute', 'selenium/standalone-chrome': 'selenium', 'famiko/jmeter-base': 'checkJMeter', 's4sdk/docker-cf-cli': 'cloudfoundry']`
* Example: `['maven:3.5-jdk-8-alpine': 'mavenExecute', 'selenium/standalone-chrome': 'selenium', 'famiko/jmeter-base': 'checkJMeter', 'ppiper/cf-cli': 'cloudfoundry']`
*/
'containerMap',
/**

View File

@ -14,7 +14,7 @@ import java.nio.charset.StandardCharsets
@Field Set GENERAL_CONFIG_KEYS = [
/**
* Pull-Request voting only:
* The URL to the Github API. see https://docs.sonarqube.org/display/PLUG/GitHub+Plugin#GitHubPlugin-Usage
* The URL to the Github API. see [GitHub plugin docs](https://docs.sonarqube.org/display/PLUG/GitHub+Plugin#GitHubPlugin-Usage)
* deprecated: only supported in LTS / < 7.2
*/
'githubApiUrl',
@ -38,7 +38,7 @@ import java.nio.charset.StandardCharsets
*/
'githubTokenCredentialsId',
/**
* The Jenkins credentialsId for a SonarQube token. It is needed for non-anonymous analysis runs. see https://sonarcloud.io/account/security
* The Jenkins credentialsId for a SonarQube token. It is needed for non-anonymous analysis runs. see [SonarQube docs](https://docs.sonarqube.org/latest/user-guide/user-token/)
* @possibleValues Jenkins credential id
*/
'sonarTokenCredentialsId',
@ -62,7 +62,7 @@ import java.nio.charset.StandardCharsets
'disableInlineComments',
/**
* Name of the docker image that should be used. If empty, Docker is not used and the command is executed directly on the Jenkins system.
* see dockerExecute
* see [dockerExecute](dockerExecute.md)
*/
'dockerImage',
/**
@ -120,21 +120,22 @@ void call(Map parameters = [:]) {
configuration.options = [].plus(configuration.options)
def worker = { config ->
withSonarQubeEnv(config.instance) {
try{
loadSonarScanner(config)
try {
withSonarQubeEnv(config.instance) {
loadCertificates(config)
loadSonarScanner(config)
if(config.organization) config.options.add("sonar.organization=${config.organization}")
if(config.projectVersion) config.options.add("sonar.projectVersion=${config.projectVersion}")
// prefix options
config.options = config.options.collect { it.startsWith('-D') ? it : "-D${it}" }
loadCertificates(config)
sh "PATH=\$PATH:${env.WORKSPACE}/.sonar-scanner/bin sonar-scanner ${config.options.join(' ')}"
}finally{
sh 'rm -rf .sonar-scanner .certificates .scannerwork'
if(config.organization) config.options.add("sonar.organization=${config.organization}")
if(config.projectVersion) config.options.add("sonar.projectVersion=${config.projectVersion}")
// prefix options
config.options = config.options.collect { it.startsWith('-D') ? it : "-D${it}" }
sh "PATH=\$PATH:${env.WORKSPACE}/.sonar-scanner/bin sonar-scanner ${config.options.join(' ')}"
}
} finally {
sh 'rm -rf .sonar-scanner .certificates .scannerwork'
}
}