mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-12 10:55:20 +02:00
d7985dd1b5
This change enables the setupCommonPipelineEnvironment step to handle custom default configurations defined in customDefaults parameter of the project configuration. Previously, only the getConfig Go step was able to incorporate custom default configurations. Update documentation on custom defaults and sharing between projects. Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>
463 lines
16 KiB
Markdown
463 lines
16 KiB
Markdown
# Development
|
|
|
|
**Table of contents:**
|
|
|
|
1. [Getting started](#getting-started)
|
|
1. [Build the project](build-the-project)
|
|
1. [Generating step framework](#generating-step-framework)
|
|
1. [Best practices for writing piper-go steps](#best-practices-for-writing-piper-go-steps)
|
|
1. [Testing](#testing)
|
|
1. [Debugging](#debugging)
|
|
1. [Release](#release)
|
|
1. [Pipeline Configuration](#pipeline-configuration)
|
|
|
|
## 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:
|
|
|
|
1. Install Go, see [GO Getting Started](https://golang.org/doc/install)
|
|
1. 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](https://github.com/SAP/devops-docker-cx-server)
|
|
|
|
### 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:
|
|
|
|
```sh
|
|
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
|
|
|
|
There are certain extensions:
|
|
|
|
* **aliases** allow alternative parameter names also supporting deeper configuration structures. [Example](https://github.com/SAP/jenkins-library/blob/master/resources/metadata/kubernetesdeploy.yaml)
|
|
* **resources** allow to read for example from a shared `commonPipelineEnvironment` which contains information which has been provided by a previous step in the pipeline via an output. [Example](https://github.com/SAP/jenkins-library/blob/master/resources/metadata/githubrelease.yaml)
|
|
* **secrets** allow to specify references to Jenkins credentials which can be used in the `groovy` library. [Example](https://github.com/SAP/jenkins-library/blob/master/resources/metadata/kubernetesdeploy.yaml)
|
|
* **outputs** allow to write to dedicated outputs like
|
|
|
|
* Influx metrics. [Example](https://github.com/SAP/jenkins-library/blob/master/resources/metadata/checkmarx.yaml)
|
|
* Sharing data via `commonPipelineEnvironment` which can be used by another step as input
|
|
|
|
* **conditions** allow for example to specify in which case a certain container is used (depending on a configuration parameter). [Example](https://github.com/SAP/jenkins-library/blob/master/resources/metadata/kubernetesdeploy.yaml)
|
|
|
|
## Best practices for writing piper-go steps
|
|
|
|
1. [Logging](#logging)
|
|
1. [Error handling](#error-handling)
|
|
|
|
Implementing a new step starts by adding a new yaml file in `resources/metadata/` and running
|
|
the [step generator](#generating-step-framework). This creates most of the boiler-plate code for the
|
|
step's implementation in `cmd/`. There are four files per step based on the name given within the yaml:
|
|
|
|
1. `cmd/<step>.go` - contains the skeleton of your step implementation.
|
|
1. `cmd/<step>_test.go` - write your unit tests here.
|
|
1. `cmd/<step>_generated.go` - contains the generated boiler plate code, and a dedicated type definition for your step's options.
|
|
1. `cmd/<step>_generated_test.go` - contains a simple unit test for the generated part.
|
|
|
|
You never edit in the generated parts. If you need to make changes, you make them in the yaml and re-run the step
|
|
generator (which will of course not overwrite your implementation).
|
|
|
|
The file `cmd/<step>.go` initially contains two functions:
|
|
|
|
```golang
|
|
func step(options stepOptions, telemetryData *telemetry.CustomData) {
|
|
err := runStep(&options, telemetryData)
|
|
if err != nil {
|
|
log.Entry().WithError(err).Fatal("step execution failed")
|
|
}
|
|
}
|
|
|
|
func runStep(options *stepOptions, telemetryData *telemetry.CustomData) error {
|
|
}
|
|
```
|
|
|
|
The separation into these two functions facilitates unit tests and mocking. From your tests, you could call
|
|
`runStep()` with mocking instances of needed objects, while inside `step()`, you create runtime instances of these
|
|
objects.
|
|
|
|
### Logging
|
|
|
|
Logging is done via the [sirupsen/logrus](https://github.com/sirupsen/logrus) framework.
|
|
It can conveniently be accessed through:
|
|
|
|
```golang
|
|
import (
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
|
)
|
|
|
|
func myStep ...
|
|
...
|
|
log.Entry().Info("This is my info.")
|
|
...
|
|
}
|
|
```
|
|
|
|
If a fatal error occurs your code should act similar to:
|
|
|
|
```golang
|
|
...
|
|
if err != nil {
|
|
log.Entry().
|
|
WithError(err).
|
|
Fatal("failed to execute step ...")
|
|
}
|
|
```
|
|
|
|
Calling `Fatal` results in an `os.Exit(0)` and before exiting some cleanup actions (e.g. writing output data,
|
|
writing telemetry data if not deactivated by the user, ...) are performed.
|
|
|
|
### 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.
|
|
|
|
It has proven a good practice to bubble up errors until the runtime entry function and only
|
|
there exit via the logging framework (see also [logging](#logging)).
|
|
|
|
## Testing
|
|
|
|
1. [Mocking](#mocking)
|
|
1. [Mockable Interface](#mockable-interface)
|
|
1. [Global function pointers](global-function-pointers)
|
|
|
|
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. A good pattern to follow is this:
|
|
|
|
```golang
|
|
func TestNameOfFunctionUnderTest(t *testing.T) {
|
|
t.Run("A description of the test case", func(t *testing.T) {
|
|
// init
|
|
// test
|
|
// assert
|
|
})
|
|
t.Run("Another test case", func(t *testing.T) {
|
|
// init
|
|
// test
|
|
// assert
|
|
})
|
|
}
|
|
```
|
|
|
|
This will also structure the test output for better readability.
|
|
|
|
### Mocking
|
|
|
|
Tests should be written only for the code of your step implementation, while any
|
|
external functionality should be mocked, in order to test all code paths including
|
|
the error cases.
|
|
|
|
There are (at least) two approaches for this:
|
|
|
|
#### Mockable Interface
|
|
|
|
In this approach you declare an interface that contains every external function
|
|
used within your step that you need to be able to mock. In addition, you declare a struct
|
|
which holds the data you need during runtime, and implement the interface with the "real"
|
|
functions. Here is an example to illustrate:
|
|
|
|
```golang
|
|
import (
|
|
"github.com/SAP/jenkins-library/pkg/piperutils"
|
|
)
|
|
|
|
type myStepUtils interface {
|
|
fileExists(path string) (bool, error)
|
|
fileRead(path string) ([]byte, error)
|
|
}
|
|
|
|
type myUtilsData struct {
|
|
fileUtils piperutils.Files
|
|
}
|
|
|
|
func (u *myUtilsData) fileExists(path string) (bool, error) {
|
|
return u.fileUtils.FileExists(path)
|
|
}
|
|
|
|
func (u *myUtilsData) fileRead(path string) ([]byte, error) {
|
|
return u.fileUtils.FileRead(path)
|
|
}
|
|
```
|
|
|
|
Then you create the runtime version of the utils data in your top-level entry function and
|
|
pass it to your `run*()` function:
|
|
|
|
```golang
|
|
func step(options stepOptions, _ *telemetry.CustomData) {
|
|
utils := myUtilsData{
|
|
fileUtils: piperutils.Files{},
|
|
}
|
|
err := runStep(&options, &utils)
|
|
...
|
|
}
|
|
|
|
func runStep(options *stepOptions, utils myStepUtils) error {
|
|
...
|
|
exists, err := utils.fileExists(path)
|
|
...
|
|
}
|
|
```
|
|
|
|
In your tests, you would provide a mocking implementation of this interface and pass
|
|
instances of that to the functions under test. To better illustrate this, here is an example
|
|
for the interface above implemented in the `<step>_test.go` file:
|
|
|
|
```golang
|
|
type mockUtilsBundle struct {
|
|
files map[string][]byte
|
|
}
|
|
|
|
func newMockUtilsBundle() mockUtilsBundle {
|
|
utils := mockUtilsBundle{}
|
|
utils.files = map[string][]byte{}
|
|
return utils
|
|
}
|
|
|
|
func (m *mockUtilsBundle) fileExists(path string) (bool, error) {
|
|
content := m.files[path]
|
|
return content != nil, nil
|
|
}
|
|
|
|
func (m *mockUtilsBundle) fileRead(path string) ([]byte, error) {
|
|
content := m.files[path]
|
|
if content == nil {
|
|
return nil, fmt.Errorf("could not read '%s': %w", path, os.ErrNotExist)
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
// This is how it would be used in tests:
|
|
|
|
func TestSomeFunction() {
|
|
t.Run("Happy path", func(t *testing.T) {
|
|
// init
|
|
utils := newMockUtilsBundle()
|
|
utils.files["some/path/file.xml"] = []byte(´content of the file´)
|
|
// test
|
|
err := someFunction(&utils)
|
|
// assert
|
|
assert.NoError(t, err)
|
|
})
|
|
t.Run("Error path", func(t *testing.T) {
|
|
// init
|
|
utils := newMockUtilsBundle()
|
|
// test
|
|
err := someFunction(&utils)
|
|
// assert
|
|
assert.EqualError(t, err, "could not read 'some/path/file.xml'")
|
|
})
|
|
}
|
|
```
|
|
|
|
#### Global Function Pointers
|
|
|
|
An alternative approach are global function pointers:
|
|
|
|
```golang
|
|
import (
|
|
FileUtils "github.com/SAP/jenkins-library/pkg/piperutils"
|
|
)
|
|
|
|
var fileUtilsExists = FileUtils.FileExists
|
|
|
|
func someFunction(options *stepOptions) error {
|
|
...
|
|
exists, err := fileUtilsExists(path)
|
|
...
|
|
}
|
|
```
|
|
|
|
In your tests, you can then simply set the function pointer to a mocking implementation:
|
|
|
|
```golang
|
|
func TestSomeFunction() {
|
|
t.Run("Happy path", func(t *testing.T) {
|
|
// init
|
|
originalFileExists := fileUtilsExists
|
|
fileUtilsExists = func(filename string) (bool, error) {
|
|
return true, nil
|
|
}
|
|
defer fileUtilsExists = originalFileExists
|
|
// test
|
|
err := someFunction(...)
|
|
// assert
|
|
assert.NoError(t, err)
|
|
})
|
|
t.Run("Error path", func(t *testing.T) {
|
|
// init
|
|
originalFileExists := fileUtilsExists
|
|
fileUtilsExists = func(filename string) (bool, error) {
|
|
return false, errors.New("something happened")
|
|
}
|
|
defer fileUtilsExists = originalFileExists
|
|
// test
|
|
err := someFunction(...)
|
|
// assert
|
|
assert.EqualError(t, err, "something happened")
|
|
})
|
|
}
|
|
```
|
|
|
|
Both approaches have their own benefits. Global function pointers require less preparation
|
|
in the actual implementation and give great flexibility in the tests, while mocking interfaces
|
|
tend to result in more code re-use and slim down the tests. The mocking implementation of a
|
|
utils interface can facilitate implementations of related functions to be based on shared data.
|
|
|
|
## Debugging
|
|
|
|
Debugging can be initiated with VS code fairly easily. Compile the binary with specific compiler flags to turn off optimizations `go build -gcflags "all=-N -l" -o piper.exe`.
|
|
|
|
Modify the `launch.json` located in folder `.vscode` of your project root to point with `program` exactly to the binary that you just built with above command - must be an absolute path. Add any arguments required for the execution of the Piper step to `args`. What is separated with a blank on the command line must go into a separate string.
|
|
|
|
```javascript
|
|
{
|
|
// Use IntelliSense to learn about possible attributes.
|
|
// Hover to view descriptions of existing attributes.
|
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
"version": "0.2.0",
|
|
"configurations": [
|
|
{
|
|
"name": "Launch",
|
|
"type": "go",
|
|
"request": "launch",
|
|
"mode": "exec",
|
|
"program": "C:/CF@HCP/git/jenkins-library-public/piper.exe",
|
|
"env": {},
|
|
"args": ["checkmarxExecuteScan", "--password", "abcd", "--username", "1234", "--projectName", "testProject4711", "--serverUrl", "https://cx.server.com/"]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Finally, set your breakpoints and use the `Launch` button in the VS code UI to start debugging.
|
|
|
|
## Release
|
|
|
|
Releases are performed using [Project "Piper" Action](https://github.com/SAP/project-piper-action).
|
|
We release on schedule (once a week) and on demand.
|
|
To perform a release, the respective action must be invoked for which a convenience script is available in `contrib/perform-release.sh`.
|
|
It requires a personal access token for GitHub with `repo` scope.
|
|
Example usage `PIPER_RELEASE_TOKEN=THIS_IS_MY_TOKEN contrib/perform-release.sh`.
|
|
|
|
## Pipeline Configuration
|
|
|
|
The pipeline configuration is organized in a hierarchical manner and configuration parameters are incorporated from multiple sources.
|
|
In general, there are four sources for configurations:
|
|
|
|
1. Directly passed step parameters
|
|
1. Project specific configuration placed in `.pipeline/config.yml`
|
|
1. Custom default configuration provided in `customDefaults` parameter of the project config or passed as parameter to the step `setupCommonPipelineEnvironment`
|
|
1. Default configuration from Piper library
|
|
|
|
For more information and examples on how to configure a project, please refer to the [configuration documentation](https://sap.github.io/jenkins-library/configuration/).
|
|
|
|
### Groovy vs. Go step configuration
|
|
|
|
The configuration of a project is, as of now, resolved separately for Groovy and Go steps.
|
|
There are, however, dependencies between the steps responsible for resolving the configuration.
|
|
The following provides an overview of the central components and their dependencies.
|
|
|
|
#### setupCommonPipelineEnvironment (Groovy)
|
|
|
|
The step `setupCommonPipelineEnvironment` initializes the `commonPipelineEnvironment` and `DefaultValueCache`.
|
|
Custom default configurations can be provided as parameters to `setupCommonPipelineEnvironment` or via the `customDefaults` parameter in project configuration.
|
|
|
|
#### DefaultValueCache (Groovy)
|
|
|
|
The `DefaultValueCache` caches the resolved (custom) default pipeline configuration and the list of configurations that contributed to the result.
|
|
On initialization, it merges the provided custom default configurations with the default configuration from Piper library, as per the hierarchical order.
|
|
|
|
Note, the list of configurations cached by `DefaultValueCache` is used to pass path to the (custom) default configurations of each Go step.
|
|
It only contains the paths of configurations which are **not** provided via `customDefaults` parameter of the project configuration, since the Go layer already resolves configurations provided via `customDefaults` parameter independently.
|