1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-11-24 08:32:32 +02:00

Provide first parts for golang implementation (#905)

* Provide first parts for golang implementation
This commit is contained in:
Oliver Nocon 2019-10-22 15:41:27 +02:00 committed by GitHub
parent 8e987c46e1
commit c1eb9f5c70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1293 additions and 1 deletions

View File

@ -18,6 +18,12 @@ plugins:
strings:
- TODO
- FIXME
gofmt:
enabled: true
golint:
enabled: true
govet:
enabled: true
markdownlint:
enabled: true
checks:

View File

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

2
.gitignore vendored
View File

@ -17,3 +17,5 @@ targets/
documentation/docs-gen
consumer-test/**/workspace
*.code-workspace

View File

@ -27,7 +27,9 @@ jobs:
- curl -L --output cc-test-reporter https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
script: mvn package --batch-mode
script:
- docker build -t piper:latest .
- mvn package --batch-mode
after_script:
- JACOCO_SOURCE_PATH="src vars test" ./cc-test-reporter format-coverage target/site/jacoco/jacoco.xml --input-type jacoco
- ./cc-test-reporter upload-coverage

131
DEVELOPMENT.md Normal file
View File

@ -0,0 +1,131 @@
# Development
**Table of contents:**
1. [Getting started](#getting-started)
1. [Build the project](#build-the-project_)
1. [Logging](#logging)
1. [Error handling](#error-handling)
## Getting started
1. [Ramp up your development environment](#ramp-up)
1. [Get familiar with Go language](#go-basics)
1. Create [a GitHub account](https://github.com/join)
1. Setup [GitHub access via SSH](https://help.github.com/articles/connecting-to-github-with-ssh/)
1. [Create and checkout a repo fork](#checkout-your-fork)
1. Optional: [Get Jenkins related environment](#jenkins-environment)
1. Optional: [Get familiar with Jenkins Pipelines as Code](#jenkins-pipelines)
### Ramp up
First you need to set up an appropriate development environment:
Install Go, see [GO Getting Started](https://golang.org/doc/install)
Install an IDE with Go plugins, see for example [Go in Visual Studio Code](https://code.visualstudio.com/docs/languages/go)
### Go basics
In order to get yourself started, there is a lot of useful information out there.
As a first step to take we highly recommend the [Golang documentation](https://golang.org/doc/) especially, [A Tour of Go](https://tour.golang.org/welcome/1)
We have a strong focus on high quality software and contributions without adequate tests will not be accepted.
There is an excellent resource which teaches Go using a test-driven approach: [Learn Go with Tests](https://github.com/quii/learn-go-with-tests)
### Checkout your fork
The project uses [Go modules](https://blog.golang.org/using-go-modules). Thus please make sure to **NOT** checkout the project into your [`GOPATH`](https://github.com/golang/go/wiki/SettingGOPATH).
To check out this repository:
1. Create your own
[fork of this repo](https://help.github.com/articles/fork-a-repo/)
1. Clone it to your machine, for example like:
```shell
mkdir -p ${HOME}/projects/jenkins-library
cd ${HOME}/projects
git clone git@github.com:${YOUR_GITHUB_USERNAME}/jenkins-library.git
cd jenkins-library
git remote add upstream git@github.com:sap/jenkins-library.git
git remote set-url --push upstream no_push
```
### Jenkins environment
If you want to contribute also to the Jenkins-specific parts like
* Jenkins library step
* Jenkins pipeline integration
you need to do the following in addition:
* [Install Groovy](https://groovy-lang.org/install.html)
* [Install Maven](https://maven.apache.org/install.html)
* Get a local Jenkins installed: Use for example [cx-server](toDo: add link)
### Jenkins pipelines
The Jenkins related parts depend on
* [Jenkins Pipelines as Code](https://jenkins.io/doc/book/pipeline-as-code/)
* [Jenkins Shared Libraries](https://jenkins.io/doc/book/pipeline/shared-libraries/)
You should get familiar with these concepts for contributing to the Jenkins-specific parts.
## Build the project
### Build the executable suitable for the CI/CD Linux target environments
Use Docker:
`docker build -t piper:latest .`
You can extract the binary using Docker means to your local filesystem:
```
docker create --name piper piper:latest
docker cp piper:/piper .
docker rm piper
```
## Generating step framework
The steps are generated based on the yaml files in `resources/metadata/` with the following command
`go run pkg/generator/step-metadata.go`.
The yaml format is kept pretty close to Tekton's [task format](https://github.com/tektoncd/pipeline/blob/master/docs/tasks.md).
Where the Tekton format was not sufficient some extenstions have been made.
Examples are:
* matadata - longDescription
* spec - inputs - secrets
* spec - containers
* spec - sidecars
## Logging
to be added
## Error handling
In order to better understand the root cause of errors that occur we wrap errors like
```golang
f, err := os.Open(path)
if err != nil {
return errors.Wrapf(err, "open failed for %v", path)
}
defer f.Close()
```
We use [github.com/pkg/errors](https://github.com/pkg/errors) for that.
## Testing
Unit tests are done using basic `golang` means.
Additionally we encourage you to use [github.com/stretchr/testify/assert](https://github.com/stretchr/testify/assert) in order to have slimmer assertions if you like.

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM golang:1.13 AS build-env
COPY . /build
WORKDIR /build
# execute tests
RUN go test ./... -cover
## ONLY tests so far, building to be added later
# execute build
# RUN go build -o piper
# FROM gcr.io/distroless/base:latest
# COPY --from=build-env /build/piper /piper
# ENTRYPOINT ["/piper"]

11
go.mod Normal file
View File

@ -0,0 +1,11 @@
module github.com/SAP/jenkins-library
go 1.13
require (
github.com/ghodss/yaml v1.0.0
github.com/pkg/errors v0.8.1
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.2.2
)

42
go.sum Normal file
View File

@ -0,0 +1,42 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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/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=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/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=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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/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/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=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

189
pkg/config/config.go Normal file
View File

@ -0,0 +1,189 @@
package config
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
)
// Config defines the structure of the config files
type Config struct {
General map[string]interface{} `json:"general"`
Stages map[string]map[string]interface{} `json:"stages"`
Steps map[string]map[string]interface{} `json:"steps"`
}
// StepConfig defines the structure for merged step configuration
type StepConfig struct {
Config map[string]interface{}
}
// ReadConfig loads config and returns its content
func (c *Config) ReadConfig(configuration io.ReadCloser) error {
defer configuration.Close()
content, err := ioutil.ReadAll(configuration)
if err != nil {
return errors.Wrapf(err, "error reading %v", configuration)
}
err = yaml.Unmarshal(content, &c)
if err != nil {
return NewParseError(fmt.Sprintf("error unmarshalling %q: %v", content, err))
}
return nil
}
// 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) {
var stepConfig StepConfig
var d PipelineDefaults
if err := c.ReadConfig(configuration); err != nil {
switch err.(type) {
case *ParseError:
return StepConfig{}, errors.Wrap(err, "failed to parse custom pipeline configuration")
default:
//ignoring unavailability of config file since considered optional
}
}
if err := d.ReadPipelineDefaults(defaults); err != nil {
switch err.(type) {
case *ParseError:
return StepConfig{}, errors.Wrap(err, "failed to parse pipeline default configuration")
default:
//ignoring unavailability of defaults since considered optional
}
}
// first: read defaults & merge general -> steps (-> general -> steps ...)
for _, def := range d.Defaults {
stepConfig.mixIn(def.General, filters.General)
stepConfig.mixIn(def.Steps[stepName], filters.Steps)
}
// second: read config & merge - general -> steps -> stages
stepConfig.mixIn(c.General, filters.General)
stepConfig.mixIn(c.Steps[stepName], filters.Steps)
stepConfig.mixIn(c.Stages[stageName], filters.Stages)
// third: merge parameters provided via env vars
stepConfig.mixIn(envValues(filters.All), filters.All)
// fourth: if parameters are provided in JSON format merge them
if len(paramJSON) != 0 {
var params map[string]interface{}
json.Unmarshal([]byte(paramJSON), &params)
stepConfig.mixIn(params, filters.Parameters)
}
// fifth: merge command line flags
if flagValues != nil {
stepConfig.mixIn(flagValues, filters.Parameters)
}
return stepConfig, nil
}
// GetStepConfigWithJSON provides merged step configuration using a provided stepConfigJSON with additional flags provided
func GetStepConfigWithJSON(flagValues map[string]interface{}, stepConfigJSON string, filters StepFilters) StepConfig {
var stepConfig StepConfig
stepConfigMap := map[string]interface{}{}
json.Unmarshal([]byte(stepConfigJSON), &stepConfigMap)
stepConfig.mixIn(stepConfigMap, filters.All)
// ToDo: mix in parametersJSON
if flagValues != nil {
stepConfig.mixIn(flagValues, filters.Parameters)
}
return stepConfig
}
// GetJSON returns JSON representation of an object
func GetJSON(data interface{}) (string, error) {
result, err := json.Marshal(data)
if err != nil {
return "", errors.Wrapf(err, "error marshalling json: %v", err)
}
return string(result), nil
}
func envValues(filter []string) map[string]interface{} {
vals := map[string]interface{}{}
for _, param := range filter {
if envVal := os.Getenv("PIPER_" + param); len(envVal) != 0 {
vals[param] = os.Getenv("PIPER_" + param)
}
}
return vals
}
func (s *StepConfig) mixIn(mergeData map[string]interface{}, filter []string) {
if s.Config == nil {
s.Config = map[string]interface{}{}
}
s.Config = filterMap(merge(s.Config, mergeData), filter)
}
func filterMap(data map[string]interface{}, filter []string) map[string]interface{} {
result := map[string]interface{}{}
if data == nil {
data = map[string]interface{}{}
}
for key, value := range data {
if len(filter) == 0 || sliceContains(filter, key) {
result[key] = value
}
}
return result
}
func merge(base, overlay map[string]interface{}) map[string]interface{} {
result := map[string]interface{}{}
if base == nil {
base = map[string]interface{}{}
}
for key, value := range base {
result[key] = value
}
for key, value := range overlay {
if val, ok := value.(map[string]interface{}); ok {
if valBaseKey, ok := base[key].(map[string]interface{}); !ok {
result[key] = merge(map[string]interface{}{}, val)
} else {
result[key] = merge(valBaseKey, val)
}
} else {
result[key] = value
}
}
return result
}
func sliceContains(slice []string, find string) bool {
for _, elem := range slice {
if elem == find {
return true
}
}
return false
}

253
pkg/config/config_test.go Normal file
View File

@ -0,0 +1,253 @@
package config
import (
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
type errReadCloser int
func (errReadCloser) Read(p []byte) (n int, err error) {
return 0, errors.New("read error")
}
func (errReadCloser) Close() error {
return nil
}
func TestReadConfig(t *testing.T) {
var c Config
t.Run("Success case", func(t *testing.T) {
myConfig := strings.NewReader("general:\n generalTestKey: generalTestValue\nsteps:\n testStep:\n testStepKey: testStepValue")
err := c.ReadConfig(ioutil.NopCloser(myConfig)) // NopCloser "no-ops" the closing interface since strings do not need to be closed
if err != nil {
t.Errorf("Got error although no error expected: %v", err)
}
if c.General["generalTestKey"] != "generalTestValue" {
t.Errorf("General config- got: %v, expected: %v", c.General["generalTestKey"], "generalTestValue")
}
if c.Steps["testStep"]["testStepKey"] != "testStepValue" {
t.Errorf("Step config - got: %v, expected: %v", c.Steps["testStep"]["testStepKey"], "testStepValue")
}
})
t.Run("Read failure", func(t *testing.T) {
var rc errReadCloser
err := c.ReadConfig(rc)
if err == nil {
t.Errorf("Got no error although error expected.")
}
})
t.Run("Unmarshalling failure", func(t *testing.T) {
myConfig := strings.NewReader("general:\n generalTestKey: generalTestValue\nsteps:\n testStep:\n\ttestStepKey: testStepValue")
err := c.ReadConfig(ioutil.NopCloser(myConfig))
if err == nil {
t.Errorf("Got no error although error expected.")
}
})
}
func TestGetStepConfig(t *testing.T) {
t.Run("Success case", func(t *testing.T) {
testConfig := `general:
p3: p3_general
px3: px3_general
p4: p4_general
steps:
step1:
p4: p4_step
px4: px4_step
p5: p5_step
stages:
stage1:
p5: p5_stage
px5: px5_stage
p6: p6_stage
`
filters := StepFilters{
General: []string{"p0", "p1", "p2", "p3", "p4"},
Steps: []string{"p0", "p1", "p2", "p3", "p4", "p5"},
Stages: []string{"p0", "p1", "p2", "p3", "p4", "p5", "p6"},
Parameters: []string{"p0", "p1", "p2", "p3", "p4", "p5", "p6", "p7"},
Env: []string{"p0", "p1", "p2", "p3", "p4", "p5"},
}
defaults1 := `general:
p0: p0_general_default
px0: px0_general_default
p1: p1_general_default
steps:
step1:
p1: p1_step_default
px1: px1_step_default
p2: p2_step_default
`
defaults2 := `general:
p2: p2_general_default
px2: px2_general_default
p3: p3_general_default
`
paramJSON := `{"p6":"p6_param","p7":"p7_param"}`
flags := map[string]interface{}{"p7": "p7_flag"}
var c Config
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")
assert.Equal(t, nil, err, "error occured but none expected")
t.Run("Config", func(t *testing.T) {
expected := map[string]string{
"p0": "p0_general_default",
"p1": "p1_step_default",
"p2": "p2_general_default",
"p3": "p3_general",
"p4": "p4_step",
"p5": "p5_stage",
"p6": "p6_param",
"p7": "p7_flag",
}
for k, v := range expected {
t.Run(k, func(t *testing.T) {
if stepConfig.Config[k] != v {
t.Errorf("got: %v, expected: %v", stepConfig.Config[k], v)
}
})
}
})
t.Run("Config not expected", func(t *testing.T) {
notExpectedKeys := []string{"px0", "px1", "px2", "px3", "px4", "px5"}
for _, p := range notExpectedKeys {
t.Run(p, func(t *testing.T) {
if stepConfig.Config[p] != nil {
t.Errorf("unexpected: %v", p)
}
})
}
})
})
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")
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")
})
t.Run("Failure case defaults", func(t *testing.T) {
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")
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")
})
//ToDo: test merging of env and parameters/flags
}
func TestGetStepConfigWithJSON(t *testing.T) {
filters := StepFilters{All: []string{"key1"}}
t.Run("Without flags", func(t *testing.T) {
sc := GetStepConfigWithJSON(nil, `"key1":"value1","key2":"value2"`, filters)
if sc.Config["key1"] != "value1" && sc.Config["key2"] == "value2" {
t.Errorf("got: %v, expected: %v", sc.Config, StepConfig{Config: map[string]interface{}{"key1": "value1"}})
}
})
t.Run("With flags", func(t *testing.T) {
flags := map[string]interface{}{"key1": "flagVal1"}
sc := GetStepConfigWithJSON(flags, `"key1":"value1","key2":"value2"`, filters)
if sc.Config["key1"] != "flagVal1" {
t.Errorf("got: %v, expected: %v", sc.Config["key1"], "flagVal1")
}
})
}
func TestGetJSON(t *testing.T) {
t.Run("Success case", func(t *testing.T) {
custom := map[string]interface{}{"key1": "value1"}
json, err := GetJSON(custom)
if err != nil {
t.Errorf("Got error although no error expected: %v", err)
}
if json != `{"key1":"value1"}` {
t.Errorf("got: %v, expected: %v", json, `{"key1":"value1"}`)
}
})
t.Run("Marshalling failure", func(t *testing.T) {
_, err := GetJSON(make(chan int))
if err == nil {
t.Errorf("Got no error although error expected")
}
})
}
func TestMerge(t *testing.T) {
testTable := []struct {
Source map[string]interface{}
Filter []string
MergeData map[string]interface{}
ExpectedOutput map[string]interface{}
}{
{
Source: map[string]interface{}{"key1": "baseValue"},
Filter: []string{},
MergeData: map[string]interface{}{"key1": "overwrittenValue"},
ExpectedOutput: map[string]interface{}{"key1": "overwrittenValue"},
},
{
Source: map[string]interface{}{"key1": "value1"},
Filter: []string{},
MergeData: map[string]interface{}{"key2": "value2"},
ExpectedOutput: map[string]interface{}{"key1": "value1", "key2": "value2"},
},
{
Source: map[string]interface{}{"key1": "value1"},
Filter: []string{"key1"},
MergeData: map[string]interface{}{"key2": "value2"},
ExpectedOutput: map[string]interface{}{"key1": "value1"},
},
{
Source: map[string]interface{}{"key1": map[string]interface{}{"key1_1": "value1"}},
Filter: []string{},
MergeData: map[string]interface{}{"key1": map[string]interface{}{"key1_2": "value2"}},
ExpectedOutput: map[string]interface{}{"key1": map[string]interface{}{"key1_1": "value1", "key1_2": "value2"}},
},
}
for _, row := range testTable {
t.Run(fmt.Sprintf("Merging %v into %v", row.MergeData, row.Source), func(t *testing.T) {
stepConfig := StepConfig{Config: row.Source}
stepConfig.mixIn(row.MergeData, row.Filter)
assert.Equal(t, row.ExpectedOutput, stepConfig.Config, "Mixin was incorrect")
})
}
}

40
pkg/config/defaults.go Normal file
View File

@ -0,0 +1,40 @@
package config
import (
"fmt"
"io"
"io/ioutil"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
)
// PipelineDefaults defines the structure of the pipeline defaults
type PipelineDefaults struct {
Defaults []Config `json:"defaults"`
}
// ReadPipelineDefaults loads defaults and returns its content
func (d *PipelineDefaults) ReadPipelineDefaults(defaultSources []io.ReadCloser) error {
for _, def := range defaultSources {
defer def.Close()
var c Config
var err error
content, err := ioutil.ReadAll(def)
if err != nil {
return errors.Wrapf(err, "error reading %v", def)
}
err = yaml.Unmarshal(content, &c)
if err != nil {
return NewParseError(fmt.Sprintf("error unmarshalling %q: %v", content, err))
}
d.Defaults = append(d.Defaults, c)
}
return nil
}

View File

@ -0,0 +1,53 @@
package config
import (
"io"
"io/ioutil"
"strings"
"testing"
)
func TestReadPipelineDefaults(t *testing.T) {
var d PipelineDefaults
t.Run("Success case", func(t *testing.T) {
d0 := strings.NewReader("general:\n testStepKey1: testStepValue1")
d1 := strings.NewReader("general:\n testStepKey2: testStepValue2")
err := d.ReadPipelineDefaults([]io.ReadCloser{ioutil.NopCloser(d0), ioutil.NopCloser(d1)})
if err != nil {
t.Errorf("Got error although no error expected: %v", err)
}
t.Run("Defaults 0", func(t *testing.T) {
expected := "testStepValue1"
if d.Defaults[0].General["testStepKey1"] != expected {
t.Errorf("got: %v, expected: %v", d.Defaults[0].General["testStepKey1"], expected)
}
})
t.Run("Defaults 1", func(t *testing.T) {
expected := "testStepValue2"
if d.Defaults[1].General["testStepKey2"] != expected {
t.Errorf("got: %v, expected: %v", d.Defaults[1].General["testStepKey2"], expected)
}
})
})
t.Run("Read failure", func(t *testing.T) {
var rc errReadCloser
err := d.ReadPipelineDefaults([]io.ReadCloser{rc})
if err == nil {
t.Errorf("Got no error although error expected.")
}
})
t.Run("Unmarshalling failure", func(t *testing.T) {
myConfig := strings.NewReader("general:\n\ttestStepKey: testStepValue")
err := d.ReadPipelineDefaults([]io.ReadCloser{ioutil.NopCloser(myConfig)})
if err == nil {
t.Errorf("Got no error although error expected.")
}
})
}

18
pkg/config/errors.go Normal file
View File

@ -0,0 +1,18 @@
package config
// ParseError defines an error type for configuration parsing errors
type ParseError struct {
message string
}
// NewParseError creates a new ParseError
func NewParseError(message string) *ParseError {
return &ParseError{
message: message,
}
}
// Error returns the message of the ParseError
func (e *ParseError) Error() string {
return e.message
}

13
pkg/config/errors_test.go Normal file
View File

@ -0,0 +1,13 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseError(t *testing.T) {
err := NewParseError("Parsing failed")
assert.Equal(t, "Parsing failed", err.Error())
}

43
pkg/config/flags.go Normal file
View File

@ -0,0 +1,43 @@
package config
import (
"fmt"
"os"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
)
// AvailableFlagValues returns all flags incl. values which are available to the command.
func AvailableFlagValues(cmd *cobra.Command, filters *StepFilters) map[string]interface{} {
flagValues := map[string]interface{}{}
flags := cmd.Flags()
//only check flags where value has been set
flags.Visit(func(pflag *flag.Flag) {
switch pflag.Value.Type() {
case "string":
flagValues[pflag.Name] = pflag.Value.String()
case "stringSlice":
flagValues[pflag.Name], _ = flags.GetStringSlice(pflag.Name)
case "bool":
flagValues[pflag.Name], _ = flags.GetBool(pflag.Name)
default:
fmt.Printf("Meta data type not set or not known: '%v'\n", pflag.Value.Type())
os.Exit(1)
}
filters.Parameters = append(filters.Parameters, pflag.Name)
})
return flagValues
}
// MarkFlagsWithValue marks a flag as changed if value is available for the flag through the step configuration.
func MarkFlagsWithValue(cmd *cobra.Command, stepConfig StepConfig) {
flags := cmd.Flags()
flags.VisitAll(func(pflag *flag.Flag) {
//mark as available in case default is available or config is available
if len(pflag.Value.String()) > 0 || stepConfig.Config[pflag.Name] != nil {
pflag.Changed = true
}
})
}

67
pkg/config/flags_test.go Normal file
View File

@ -0,0 +1,67 @@
package config
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
func TestAvailableFlagValues(t *testing.T) {
var f StepFilters
var test0 string
var test1 string
var test2 []string
var test3 bool
var c = &cobra.Command{
Use: "test",
Short: "..",
}
c.Flags().StringVar(&test0, "test0", "val0", "Test 0")
c.Flags().StringVar(&test1, "test1", "", "Test 1")
c.Flags().StringSliceVar(&test2, "test2", []string{}, "Test 2")
c.Flags().BoolVar(&test3, "test3", false, "Test 3")
c.Flags().Set("test1", "val1")
c.Flags().Set("test2", "val3_1")
c.Flags().Set("test3", "true")
v := AvailableFlagValues(c, &f)
if v["test0"] != nil {
t.Errorf("expected: 'test0' to be empty but was %v", v["test0"])
}
assert.Equal(t, "val1", v["test1"])
assert.Equal(t, []string{"val3_1"}, v["test2"])
assert.Equal(t, true, v["test3"])
}
func TestMarkFlagsWithValue(t *testing.T) {
var test0 string
var test1 string
var test2 string
var c = &cobra.Command{
Use: "test",
Short: "..",
}
c.Flags().StringVar(&test0, "test0", "val0", "Test 0")
c.Flags().StringVar(&test1, "test1", "", "Test 1")
c.Flags().StringVar(&test2, "test2", "", "Test 2")
s := StepConfig{
Config: map[string]interface{}{
"test2": "val2",
},
}
MarkFlagsWithValue(c, s)
assert.Equal(t, true, c.Flags().Changed("test0"), "default not considered")
assert.Equal(t, false, c.Flags().Changed("test1"), "no value: considered as set")
assert.Equal(t, true, c.Flags().Changed("test2"), "config not considered")
}

139
pkg/config/stepmeta.go Normal file
View File

@ -0,0 +1,139 @@
package config
import (
"io"
"io/ioutil"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
)
// StepData defines the metadata for a step, like step descriptions, parameters, ...
type StepData struct {
Metadata StepMetadata `json:"metadata"`
Spec StepSpec `json:"spec"`
}
// StepMetadata defines the metadata for a step, like step descriptions, parameters, ...
type StepMetadata struct {
Name string `json:"name"`
Description string `json:"description"`
LongDescription string `json:"longDescription,omitempty"`
}
// StepSpec defines the spec details for a step, like step inputs, containers, sidecars, ...
type StepSpec struct {
Inputs StepInputs `json:"inputs"`
// Outputs string `json:"description,omitempty"`
Containers []StepContainers `json:"containers,omitempty"`
Sidecars []StepSidecars `json:"sidecars,omitempty"`
}
// StepInputs defines the spec details for a step, like step inputs, containers, sidecars, ...
type StepInputs struct {
Parameters []StepParameters `json:"params"`
Resources []StepResources `json:"resources,omitempty"`
Secrets []StepSecrets `json:"secrets,omitempty"`
}
// StepParameters defines the parameters for a step
type StepParameters struct {
Name string `json:"name"`
Description string `json:"description"`
LongDescription string `json:"longDescription,omitempty"`
Scope []string `json:"scope"`
Type string `json:"type"`
Mandatory bool `json:"mandatory,omitempty"`
Default interface{} `json:"default,omitempty"`
}
// StepResources defines the resources to be provided by the step context, e.g. Jenkins pipeline
type StepResources struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Type string `json:"type,omitempty"`
}
// StepSecrets defines the secrets to be provided by the step context, e.g. Jenkins pipeline
type StepSecrets struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Type string `json:"type,omitempty"`
}
// StepOutputs defines the outputs of a step
//type StepOutputs struct {
// Name string `json:"name"`
//}
// StepContainers defines the containers required for a step
type StepContainers struct {
Containers map[string]interface{} `json:"containers"`
}
// StepSidecars defines any sidears required for a step
type StepSidecars struct {
Sidecars map[string]interface{} `json:"sidecars"`
}
// StepFilters defines the filter parameters for the different sections
type StepFilters struct {
All []string
General []string
Stages []string
Steps []string
Parameters []string
Env []string
}
// ReadPipelineStepData loads step definition in yaml format
func (m *StepData) ReadPipelineStepData(metadata io.ReadCloser) error {
defer metadata.Close()
content, err := ioutil.ReadAll(metadata)
if err != nil {
return errors.Wrapf(err, "error reading %v", metadata)
}
err = yaml.Unmarshal(content, &m)
if err != nil {
return errors.Wrapf(err, "error unmarshalling: %v", err)
}
return nil
}
// GetParameterFilters retrieves all scope dependent parameter filters
func (m *StepData) GetParameterFilters() StepFilters {
var filters StepFilters
for _, param := range m.Spec.Inputs.Parameters {
filters.All = append(filters.All, param.Name)
for _, scope := range param.Scope {
switch scope {
case "GENERAL":
filters.General = append(filters.General, param.Name)
case "STEPS":
filters.Steps = append(filters.Steps, param.Name)
case "STAGES":
filters.Stages = append(filters.Stages, param.Name)
case "PARAMETERS":
filters.Parameters = append(filters.Parameters, param.Name)
case "ENV":
filters.Env = append(filters.Env, param.Name)
}
}
}
return filters
}
// GetContextParameterFilters retrieves all scope dependent parameter filters
func (m *StepData) GetContextParameterFilters() StepFilters {
var filters StepFilters
for _, secret := range m.Spec.Inputs.Secrets {
filters.All = append(filters.All, secret.Name)
filters.General = append(filters.General, secret.Name)
filters.Steps = append(filters.Steps, secret.Name)
filters.Stages = append(filters.Stages, secret.Name)
filters.Parameters = append(filters.Parameters, secret.Name)
filters.Env = append(filters.Env, secret.Name)
}
return filters
}

266
pkg/config/stepmeta_test.go Normal file
View File

@ -0,0 +1,266 @@
package config
import (
"fmt"
"io/ioutil"
"strings"
"testing"
)
func TestReadPipelineStepData(t *testing.T) {
var s StepData
t.Run("Success case", func(t *testing.T) {
myMeta := strings.NewReader("metadata:\n name: testIt\nspec:\n inputs:\n params:\n - name: testParamName\n secrets:\n - name: testSecret")
err := s.ReadPipelineStepData(ioutil.NopCloser(myMeta)) // NopCloser "no-ops" the closing interface since strings do not need to be closed
if err != nil {
t.Errorf("Got error although no error expected: %v", err)
}
t.Run("step name", func(t *testing.T) {
if s.Metadata.Name != "testIt" {
t.Errorf("Meta name - got: %v, expected: %v", s.Metadata.Name, "testIt")
}
})
t.Run("param name", func(t *testing.T) {
if s.Spec.Inputs.Parameters[0].Name != "testParamName" {
t.Errorf("Step name - got: %v, expected: %v", s.Spec.Inputs.Parameters[0].Name, "testParamName")
}
})
t.Run("secret name", func(t *testing.T) {
if s.Spec.Inputs.Secrets[0].Name != "testSecret" {
t.Errorf("Step name - got: %v, expected: %v", s.Spec.Inputs.Secrets[0].Name, "testSecret")
}
})
})
t.Run("Read failure", func(t *testing.T) {
var rc errReadCloser
err := s.ReadPipelineStepData(rc)
if err == nil {
t.Errorf("Got no error although error expected.")
}
})
t.Run("Unmarshalling failure", func(t *testing.T) {
myMeta := strings.NewReader("metadata:\n\tname: testIt")
err := s.ReadPipelineStepData(ioutil.NopCloser(myMeta))
if err == nil {
t.Errorf("Got no error although error expected.")
}
})
}
func TestGetParameterFilters(t *testing.T) {
metadata1 := StepData{
Spec: StepSpec{
Inputs: StepInputs{
Parameters: []StepParameters{
{Name: "paramOne", Scope: []string{"GENERAL", "STEPS", "STAGES", "PARAMETERS", "ENV"}},
{Name: "paramTwo", Scope: []string{"STEPS", "STAGES", "PARAMETERS", "ENV"}},
{Name: "paramThree", Scope: []string{"STAGES", "PARAMETERS", "ENV"}},
{Name: "paramFour", Scope: []string{"PARAMETERS", "ENV"}},
{Name: "paramFive", Scope: []string{"ENV"}},
{Name: "paramSix"},
},
},
},
}
metadata2 := StepData{
Spec: StepSpec{
Inputs: StepInputs{
Parameters: []StepParameters{
{Name: "paramOne", Scope: []string{"GENERAL"}},
{Name: "paramTwo", Scope: []string{"STEPS"}},
{Name: "paramThree", Scope: []string{"STAGES"}},
{Name: "paramFour", Scope: []string{"PARAMETERS"}},
{Name: "paramFive", Scope: []string{"ENV"}},
{Name: "paramSix"},
},
},
},
}
metadata3 := StepData{
Spec: StepSpec{
Inputs: StepInputs{
Parameters: []StepParameters{},
},
},
}
testTable := []struct {
Metadata StepData
ExpectedAll []string
ExpectedGeneral []string
ExpectedStages []string
ExpectedSteps []string
ExpectedParameters []string
ExpectedEnv []string
NotExpectedAll []string
NotExpectedGeneral []string
NotExpectedStages []string
NotExpectedSteps []string
NotExpectedParameters []string
NotExpectedEnv []string
}{
{
Metadata: metadata1,
ExpectedGeneral: []string{"paramOne"},
ExpectedSteps: []string{"paramOne", "paramTwo"},
ExpectedStages: []string{"paramOne", "paramTwo", "paramThree"},
ExpectedParameters: []string{"paramOne", "paramTwo", "paramThree", "paramFour"},
ExpectedEnv: []string{"paramOne", "paramTwo", "paramThree", "paramFour", "paramFive"},
ExpectedAll: []string{"paramOne", "paramTwo", "paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedGeneral: []string{"paramTwo", "paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedSteps: []string{"paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedStages: []string{"paramFour", "paramFive", "paramSix"},
NotExpectedParameters: []string{"paramFive", "paramSix"},
NotExpectedEnv: []string{"paramSix"},
NotExpectedAll: []string{},
},
{
Metadata: metadata2,
ExpectedGeneral: []string{"paramOne"},
ExpectedSteps: []string{"paramTwo"},
ExpectedStages: []string{"paramThree"},
ExpectedParameters: []string{"paramFour"},
ExpectedEnv: []string{"paramFive"},
ExpectedAll: []string{"paramOne", "paramTwo", "paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedGeneral: []string{"paramTwo", "paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedSteps: []string{"paramOne", "paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedStages: []string{"paramOne", "paramTwo", "paramFour", "paramFive", "paramSix"},
NotExpectedParameters: []string{"paramOne", "paramTwo", "paramThree", "paramFive", "paramSix"},
NotExpectedEnv: []string{"paramOne", "paramTwo", "paramThree", "paramFour", "paramSix"},
NotExpectedAll: []string{},
},
{
Metadata: metadata3,
ExpectedGeneral: []string{},
ExpectedStages: []string{},
ExpectedSteps: []string{},
ExpectedParameters: []string{},
ExpectedEnv: []string{},
},
}
for key, row := range testTable {
t.Run(fmt.Sprintf("Metadata%v", key), func(t *testing.T) {
filters := row.Metadata.GetParameterFilters()
t.Run("General", func(t *testing.T) {
for _, val := range filters.General {
if !sliceContains(row.ExpectedGeneral, val) {
t.Errorf("Creation of parameter filter failed, expected: %v to be contained in %v", val, filters.General)
}
if sliceContains(row.NotExpectedGeneral, val) {
t.Errorf("Creation of parameter filter failed, expected: %v NOT to be contained in %v", val, filters.General)
}
}
})
t.Run("Steps", func(t *testing.T) {
for _, val := range filters.Steps {
if !sliceContains(row.ExpectedSteps, val) {
t.Errorf("Creation of parameter filter failed, expected: %v to be contained in %v", val, filters.Steps)
}
if sliceContains(row.NotExpectedSteps, val) {
t.Errorf("Creation of parameter filter failed, expected: %v NOT to be contained in %v", val, filters.Steps)
}
}
})
t.Run("Stages", func(t *testing.T) {
for _, val := range filters.Stages {
if !sliceContains(row.ExpectedStages, val) {
t.Errorf("Creation of parameter filter failed, expected: %v to be contained in %v", val, filters.Stages)
}
if sliceContains(row.NotExpectedStages, val) {
t.Errorf("Creation of parameter filter failed, expected: %v NOT to be contained in %v", val, filters.Stages)
}
}
})
t.Run("Parameters", func(t *testing.T) {
for _, val := range filters.Parameters {
if !sliceContains(row.ExpectedParameters, val) {
t.Errorf("Creation of parameter filter failed, expected: %v to be contained in %v", val, filters.Parameters)
}
if sliceContains(row.NotExpectedParameters, val) {
t.Errorf("Creation of parameter filter failed, expected: %v NOT to be contained in %v", val, filters.Parameters)
}
}
})
t.Run("Env", func(t *testing.T) {
for _, val := range filters.Env {
if !sliceContains(row.ExpectedEnv, val) {
t.Errorf("Creation of parameter filter failed, expected: %v to be contained in %v", val, filters.Env)
}
if sliceContains(row.NotExpectedEnv, val) {
t.Errorf("Creation of parameter filter failed, expected: %v NOT to be contained in %v", val, filters.Env)
}
}
})
t.Run("All", func(t *testing.T) {
for _, val := range filters.All {
if !sliceContains(row.ExpectedAll, val) {
t.Errorf("Creation of parameter filter failed, expected: %v to be contained in %v", val, filters.All)
}
if sliceContains(row.NotExpectedAll, val) {
t.Errorf("Creation of parameter filter failed, expected: %v NOT to be contained in %v", val, filters.All)
}
}
})
})
}
}
func TestGetContextParameterFilters(t *testing.T) {
metadata1 := StepData{
Spec: StepSpec{
Inputs: StepInputs{
Secrets: []StepSecrets{
{Name: "testSecret1", Type: "jenkins"},
{Name: "testSecret2", Type: "jenkins"},
},
},
},
}
filters := metadata1.GetContextParameterFilters()
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)
}
})
}
})
}