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

refactor(hadolint): implement step in GO (#1169)

* initial commit of yaml file

* initial commit for HaDoLint in GO

* add helper function to load file from url

* load config file

* write report information to disk

* comment the code

* refactor groovy code

* remove download function from FileUtils

* use http.Downloader

* rename step files

* update generated files

* update generated files

* remove duplicate commands

* add credentials for config url

* add generated test file

* reuse piperExecuteBin functions

* correct step name

* update go step

* deactivate test

* fix import

* use differing go step name

* rename step

* correct result publishing

* correct command name

* expose tls insecure flag

* hand through error

* disable tls verification

* fix tls disabling

* use credentials

* mow

* reformat

* add qgate only if set

* correct report name

* remove old defaults

* add qgate to defaults

* handle report name

* restore default

* remove unused step config

* use piperExecuteBin

* remove obsolete type

* add test cases

* remove groovy tests

* move client parameter handling to run function

* use custom interfaces and mockery

* remove commented code

* correct struct names

* rename parameter dockerfile

* add further asserts

* cleanup

* change file permission to read/write

* remove tokenize

* add further comments

* init http client only if necessary

* add todo

* Revert "rename parameter dockerfile"

This reverts commit 2a570685b8.

* add alias for dockerfile parameter

* correct test case

* Apply suggestions from code review

Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>

* add comment about mock assertions

Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>
This commit is contained in:
Christopher Fenner
2020-11-16 15:14:54 +01:00
committed by GitHub
parent e8c74a4867
commit 81c8553d6a
11 changed files with 596 additions and 184 deletions

127
cmd/hadolintExecute.go Normal file
View File

@@ -0,0 +1,127 @@
package cmd
import (
"bytes"
"io"
"net/http"
"os"
"time"
"github.com/SAP/jenkins-library/pkg/command"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/pkg/errors"
)
const hadolintCommand = "hadolint"
// HadolintPiperFileUtils abstracts piperutils.Files
// mock generated with: mockery --name HadolintPiperFileUtils --dir cmd --output pkg/hadolint/mocks
type HadolintPiperFileUtils interface {
FileExists(filename string) (bool, error)
FileWrite(filename string, data []byte, perm os.FileMode) error
}
// HadolintClient abstracts http.Client
// mock generated with: mockery --name hadolintClient --dir cmd --output pkg/hadolint/mocks
type HadolintClient interface {
SetOptions(options piperhttp.ClientOptions)
DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error
}
// hadolintRunner abstracts command.Command
type hadolintRunner interface {
RunExecutable(executable string, params ...string) error
Stdout(err io.Writer)
Stderr(err io.Writer)
}
type hadolintUtils struct {
HadolintPiperFileUtils
HadolintClient
hadolintRunner
}
func hadolintExecute(config hadolintExecuteOptions, _ *telemetry.CustomData) {
runner := command.Command{
ErrorCategoryMapping: map[string][]string{},
}
// reroute runner output to logging framework
runner.Stdout(log.Writer())
runner.Stderr(log.Writer())
utils := hadolintUtils{
HadolintPiperFileUtils: &piperutils.Files{},
HadolintClient: &piperhttp.Client{},
hadolintRunner: &runner,
}
if err := runHadolint(config, utils); err != nil {
log.Entry().WithError(err).Fatal("Execution failed")
}
}
func runHadolint(config hadolintExecuteOptions, utils hadolintUtils) error {
var outputBuffer bytes.Buffer
var errorBuffer bytes.Buffer
utils.Stdout(&outputBuffer)
utils.Stderr(&errorBuffer)
options := []string{"--format", "checkstyle"}
// load config file from URL
if !hasConfigurationFile(config.ConfigurationFile, utils) && len(config.ConfigurationURL) > 0 {
clientOptions := piperhttp.ClientOptions{
TransportTimeout: 20 * time.Second,
TransportSkipVerification: true,
}
if len(config.ConfigurationUsername) > 0 {
clientOptions.Username = config.ConfigurationUsername
clientOptions.Password = config.ConfigurationPassword
}
utils.SetOptions(clientOptions)
if err := loadConfigurationFile(config.ConfigurationURL, config.ConfigurationFile, utils); err != nil {
return errors.Wrap(err, "failed to load configuration file from URL")
}
}
// use config
if hasConfigurationFile(config.ConfigurationFile, utils) {
options = append(options, "--config", config.ConfigurationFile)
log.Entry().WithField("file", config.ConfigurationFile).Debug("Using configuration file")
} else {
log.Entry().Debug("No configuration file found.")
}
// execute scan command
err := utils.RunExecutable(hadolintCommand, append([]string{config.DockerFile}, options...)...)
//TODO: related to https://github.com/hadolint/hadolint/issues/391
// hadolint exists with 1 if there are processing issues but also if there are findings
// thus check stdout first if a report was created
if output := outputBuffer.String(); len(output) > 0 {
log.Entry().WithField("report", output).Debug("Report created")
utils.FileWrite(config.ReportFile, []byte(output), 0666)
} else if err != nil {
// if stdout is empty a processing issue occured
return errors.Wrap(err, errorBuffer.String())
}
//TODO: mock away in tests
// persist report information
piperutils.PersistReportsAndLinks("hadolintExecute", "./", []piperutils.Path{{Target: config.ReportFile}}, []piperutils.Path{})
return nil
}
// loadConfigurationFile loads a file from the provided url
func loadConfigurationFile(url, file string, utils hadolintUtils) error {
log.Entry().WithField("url", url).Debug("Loading configuration file from URL")
return utils.DownloadFile(url, file, nil, nil)
}
// hasConfigurationFile checks if the given file exists
func hasConfigurationFile(file string, utils hadolintUtils) bool {
exists, err := utils.FileExists(file)
if err != nil {
log.Entry().WithError(err).Error()
}
return exists
}

View File

@@ -0,0 +1,169 @@
// Code generated by piper's step-generator. DO NOT EDIT.
package cmd
import (
"fmt"
"os"
"time"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/spf13/cobra"
)
type hadolintExecuteOptions struct {
ConfigurationURL string `json:"configurationUrl,omitempty"`
ConfigurationUsername string `json:"configurationUsername,omitempty"`
ConfigurationPassword string `json:"configurationPassword,omitempty"`
DockerFile string `json:"dockerFile,omitempty"`
ConfigurationFile string `json:"configurationFile,omitempty"`
ReportFile string `json:"reportFile,omitempty"`
}
// HadolintExecuteCommand Executes the Haskell Dockerfile Linter which is a smarter Dockerfile linter that helps you build [best practice](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) Docker images.
func HadolintExecuteCommand() *cobra.Command {
const STEP_NAME = "hadolintExecute"
metadata := hadolintExecuteMetadata()
var stepConfig hadolintExecuteOptions
var startTime time.Time
var createHadolintExecuteCmd = &cobra.Command{
Use: STEP_NAME,
Short: "Executes the Haskell Dockerfile Linter which is a smarter Dockerfile linter that helps you build [best practice](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) Docker images.",
Long: `Executes the Haskell Dockerfile Linter which is a smarter Dockerfile linter that helps you build [best practice](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) Docker images.
The linter is parsing the Dockerfile into an abstract syntax tree (AST) and performs rules on top of the AST.`,
PreRunE: func(cmd *cobra.Command, _ []string) error {
startTime = time.Now()
log.SetStepName(STEP_NAME)
log.SetVerbose(GeneralConfig.Verbose)
path, _ := os.Getwd()
fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path}
log.RegisterHook(fatalHook)
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.ConfigurationUsername)
log.RegisterSecret(stepConfig.ConfigurationPassword)
if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID)
log.RegisterHook(&sentryHook)
}
return nil
},
Run: func(_ *cobra.Command, _ []string) {
telemetryData := telemetry.CustomData{}
telemetryData.ErrorCode = "1"
handler := func() {
config.RemoveVaultSecretFiles()
telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds())
telemetryData.ErrorCategory = log.GetErrorCategory().String()
telemetry.Send(&telemetryData)
}
log.DeferExitHandler(handler)
defer handler()
telemetry.Initialize(GeneralConfig.NoTelemetry, STEP_NAME)
hadolintExecute(stepConfig, &telemetryData)
telemetryData.ErrorCode = "0"
log.Entry().Info("SUCCESS")
},
}
addHadolintExecuteFlags(createHadolintExecuteCmd, &stepConfig)
return createHadolintExecuteCmd
}
func addHadolintExecuteFlags(cmd *cobra.Command, stepConfig *hadolintExecuteOptions) {
cmd.Flags().StringVar(&stepConfig.ConfigurationURL, "configurationUrl", os.Getenv("PIPER_configurationUrl"), "URL pointing to the .hadolint.yaml exclude configuration to be used for linting. Also have a look at `configurationFile` which could avoid central configuration download in case the file is part of your repository.")
cmd.Flags().StringVar(&stepConfig.ConfigurationUsername, "configurationUsername", os.Getenv("PIPER_configurationUsername"), "The username to authenticate")
cmd.Flags().StringVar(&stepConfig.ConfigurationPassword, "configurationPassword", os.Getenv("PIPER_configurationPassword"), "The password to authenticate")
cmd.Flags().StringVar(&stepConfig.DockerFile, "dockerFile", `./Dockerfile`, "Dockerfile to be used for the assessment.")
cmd.Flags().StringVar(&stepConfig.ConfigurationFile, "configurationFile", `.hadolint.yaml`, "Name of the configuration file used locally within the step. If a file with this name is detected as part of your repo downloading the central configuration via `configurationUrl` will be skipped. If you change the file's name make sure your stashing configuration also reflects this.")
cmd.Flags().StringVar(&stepConfig.ReportFile, "reportFile", `hadolint.xml`, "Name of the result file used locally within the step.")
}
// retrieve step metadata
func hadolintExecuteMetadata() config.StepData {
var theMetaData = config.StepData{
Metadata: config.StepMetadata{
Name: "hadolintExecute",
Aliases: []config.Alias{},
},
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{
Name: "configurationUrl",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "configurationUsername",
ResourceRef: []config.ResourceReference{
{
Name: "configurationCredentialsId",
Param: "username",
Type: "secret",
},
},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "configurationPassword",
ResourceRef: []config.ResourceReference{
{
Name: "configurationCredentialsId",
Param: "password",
Type: "secret",
},
},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "dockerFile",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "dockerfile"}},
},
{
Name: "configurationFile",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "reportFile",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
},
},
},
}
return theMetaData
}

View File

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

View File

@@ -0,0 +1,86 @@
package cmd
import (
"testing"
"github.com/SAP/jenkins-library/pkg/hadolint/mocks"
piperMocks "github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestRunHadolintExecute(t *testing.T) {
t.Run("default", func(t *testing.T) {
// init
fileMock := &mocks.HadolintPiperFileUtils{}
clientMock := &mocks.HadolintClient{}
runnerMock := &piperMocks.ExecMockRunner{}
config := hadolintExecuteOptions{
DockerFile: "./Dockerfile", // default
ConfigurationFile: ".hadolint.yaml", // default
}
fileMock.
On("FileExists", config.ConfigurationFile).Return(false, nil)
// test
err := runHadolint(config, hadolintUtils{
HadolintPiperFileUtils: fileMock,
HadolintClient: clientMock,
hadolintRunner: runnerMock,
})
// assert
assert.NoError(t, err)
if assert.Len(t, runnerMock.Calls, 1) {
assert.Equal(t, "hadolint", runnerMock.Calls[0].Exec)
assert.Contains(t, runnerMock.Calls[0].Params, config.DockerFile)
assert.Contains(t, runnerMock.Calls[0].Params, "--format")
assert.Contains(t, runnerMock.Calls[0].Params, "checkstyle")
assert.NotContains(t, runnerMock.Calls[0].Params, "--config")
assert.NotContains(t, runnerMock.Calls[0].Params, config.ConfigurationFile)
}
// assert that mocks are called as previously defined
fileMock.AssertExpectations(t)
clientMock.AssertExpectations(t)
})
t.Run("with remote config", func(t *testing.T) {
// init
fileMock := &mocks.HadolintPiperFileUtils{}
clientMock := &mocks.HadolintClient{}
runnerMock := &piperMocks.ExecMockRunner{}
config := hadolintExecuteOptions{
DockerFile: "./Dockerfile", // default
ConfigurationFile: ".hadolint.yaml", // default
ConfigurationURL: "https://myconfig",
}
clientMock.
On("SetOptions", mock.Anything).
On("DownloadFile", config.ConfigurationURL, config.ConfigurationFile, mock.Anything, mock.Anything).Return(nil)
fileMock.
// checks if config exists before downloading
On("FileExists", config.ConfigurationFile).Return(false, nil).Once().
// checks again but config is now downloaded
On("FileExists", config.ConfigurationFile).Return(true, nil)
// test
err := runHadolint(config, hadolintUtils{
HadolintPiperFileUtils: fileMock,
HadolintClient: clientMock,
hadolintRunner: runnerMock,
})
// assert
assert.NoError(t, err)
if assert.Len(t, runnerMock.Calls, 1) {
assert.Equal(t, "hadolint", runnerMock.Calls[0].Exec)
assert.Contains(t, runnerMock.Calls[0].Params, config.DockerFile)
assert.Contains(t, runnerMock.Calls[0].Params, "--format")
assert.Contains(t, runnerMock.Calls[0].Params, "checkstyle")
assert.Contains(t, runnerMock.Calls[0].Params, "--config")
assert.Contains(t, runnerMock.Calls[0].Params, config.ConfigurationFile)
}
// assert that mocks are called as previously defined
fileMock.AssertExpectations(t)
clientMock.AssertExpectations(t)
})
}

View File

@@ -68,6 +68,7 @@ func Execute() {
rootCmd.AddCommand(CommandLineCompletionCommand())
rootCmd.AddCommand(VersionCommand())
rootCmd.AddCommand(DetectExecuteScanCommand())
rootCmd.AddCommand(HadolintExecuteCommand())
rootCmd.AddCommand(KarmaExecuteTestsCommand())
rootCmd.AddCommand(SonarExecuteScanCommand())
rootCmd.AddCommand(KubernetesDeployCommand())

View File

@@ -0,0 +1,34 @@
// Code generated by mockery v2.0.0-alpha.13. DO NOT EDIT.
package mocks
import (
http "net/http"
pkghttp "github.com/SAP/jenkins-library/pkg/http"
mock "github.com/stretchr/testify/mock"
)
// HadolintClient is an autogenerated mock type for the HadolintClient type
type HadolintClient struct {
mock.Mock
}
// DownloadFile provides a mock function with given fields: url, filename, header, cookies
func (_m *HadolintClient) DownloadFile(url string, filename string, header http.Header, cookies []*http.Cookie) error {
ret := _m.Called(url, filename, header, cookies)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, http.Header, []*http.Cookie) error); ok {
r0 = rf(url, filename, header, cookies)
} else {
r0 = ret.Error(0)
}
return r0
}
// SetOptions provides a mock function with given fields: options
func (_m *HadolintClient) SetOptions(options pkghttp.ClientOptions) {
_m.Called(options)
}

View File

@@ -0,0 +1,49 @@
// Code generated by mockery v2.0.0-alpha.13. DO NOT EDIT.
package mocks
import (
os "os"
mock "github.com/stretchr/testify/mock"
)
// HadolintPiperFileUtils is an autogenerated mock type for the HadolintPiperFileUtils type
type HadolintPiperFileUtils struct {
mock.Mock
}
// FileExists provides a mock function with given fields: filename
func (_m *HadolintPiperFileUtils) FileExists(filename string) (bool, error) {
ret := _m.Called(filename)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(filename)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(filename)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FileWrite provides a mock function with given fields: filename, data, perm
func (_m *HadolintPiperFileUtils) FileWrite(filename string, data []byte, perm os.FileMode) error {
ret := _m.Called(filename, data, perm)
var r0 error
if rf, ok := ret.Get(0).(func(string, []byte, os.FileMode) error); ok {
r0 = rf(filename, data, perm)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@@ -269,10 +269,6 @@ steps:
runCommand: 'bundle install && bundle exec gauge run'
testOptions: 'specs'
hadolintExecute:
configurationFile: '.hadolint.yaml'
configurationUrl: ''
dockerFile: './Dockerfile'
dockerImage: 'hadolint/hadolint:latest-debian'
qualityGates:
- threshold: 1
type: 'TOTAL_ERROR'

View File

@@ -0,0 +1,78 @@
metadata:
name: hadolintExecute
description: Executes the Haskell Dockerfile Linter which is a smarter Dockerfile linter that helps you build [best practice](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) Docker images.
longDescription: |-
Executes the Haskell Dockerfile Linter which is a smarter Dockerfile linter that helps you build [best practice](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) Docker images.
The linter is parsing the Dockerfile into an abstract syntax tree (AST) and performs rules on top of the AST.
spec:
inputs:
secrets:
- name: configurationCredentialsId
type: jenkins
description: Jenkins 'Username with password' credentials ID containing username/password for access to your remote configuration file.
params:
- name: configurationUrl
type: string
description: URL pointing to the .hadolint.yaml exclude configuration to be used for linting. Also have a look at `configurationFile` which could avoid central configuration download in case the file is part of your repository.
mandatory: false
scope:
- PARAMETERS
- STAGES
- STEPS
default: null
- name: configurationUsername
type: string
description: The username to authenticate
scope:
- PARAMETERS
- STAGES
- STEPS
secret: true
resourceRef:
- name: configurationCredentialsId
type: secret
param: username
- name: configurationPassword
type: string
description: The password to authenticate
scope:
- PARAMETERS
- STAGES
- STEPS
secret: true
resourceRef:
- name: configurationCredentialsId
type: secret
param: password
- name: dockerFile
aliases:
- name: dockerfile
type: string
description: Dockerfile to be used for the assessment.
mandatory: false
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
default: ./Dockerfile
- name: configurationFile
type: string
description: Name of the configuration file used locally within the step. If a file with this name is detected as part of your repo downloading the central configuration via `configurationUrl` will be skipped. If you change the file's name make sure your stashing configuration also reflects this.
mandatory: false
scope:
- PARAMETERS
- STAGES
- STEPS
default: .hadolint.yaml
- name: reportFile
type: string
description: Name of the result file used locally within the step.
scope:
- PARAMETERS
- STAGES
- STEPS
default: hadolint.xml
containers:
- name: hadolint
image: hadolint/hadolint:latest-debian

View File

@@ -1,82 +0,0 @@
import hudson.AbortException
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.rules.RuleChain
import util.BasePiperTest
import util.JenkinsDockerExecuteRule
import util.JenkinsLoggingRule
import util.JenkinsReadYamlRule
import util.JenkinsShellCallRule
import util.JenkinsStepRule
import util.JenkinsWriteFileRule
import util.Rules
import static org.junit.Assert.assertThat
import static org.hamcrest.Matchers.*
import com.sap.piper.Utils
class HadolintExecuteTest extends BasePiperTest {
private ExpectedException thrown = new ExpectedException().none()
private JenkinsShellCallRule shellRule = new JenkinsShellCallRule(this)
private JenkinsDockerExecuteRule dockerExecuteRule = new JenkinsDockerExecuteRule(this)
private JenkinsStepRule stepRule = new JenkinsStepRule(this)
private JenkinsReadYamlRule yamlRule = new JenkinsReadYamlRule(this)
private JenkinsLoggingRule loggingRule = new JenkinsLoggingRule(this)
private JenkinsWriteFileRule writeFileRule = new JenkinsWriteFileRule(this)
@Rule
public RuleChain ruleChain = Rules
.getCommonRules(this)
.around(thrown)
.around(yamlRule)
.around(dockerExecuteRule)
.around(shellRule)
.around(stepRule)
.around(loggingRule)
.around(writeFileRule)
@Before
void init() {
helper.registerAllowedMethod 'stash', [String, String], { name, includes -> assertThat(name, is('hadolintConfiguration')); assertThat(includes, is('.hadolint.yaml')) }
helper.registerAllowedMethod 'fileExists', [String], { s -> s == './Dockerfile' }
helper.registerAllowedMethod 'checkStyle', [Map], { m -> assertThat(m.pattern, is('hadolint.xml')); return 'checkstyle' }
helper.registerAllowedMethod 'recordIssues', [Map], { m -> assertThat(m.tools, hasItem('checkstyle')) }
helper.registerAllowedMethod 'archiveArtifacts', [String], { String p -> assertThat('hadolint.xml', is(p)) }
helper.registerAllowedMethod('httpRequest', [Map.class] , {
return [content: "empty", status: 200]
})
Utils.metaClass.echo = { def m -> }
}
@After
public void tearDown() {
Utils.metaClass = null
}
@Test
void testHadolintExecute() {
stepRule.step.hadolintExecute(script: nullScript, juStabUtils: utils, dockerImage: 'hadolint/hadolint:latest-debian', configurationUrl: 'https://github.com/raw/SAP/jenkins-library/master/.hadolint.yaml')
assertThat(dockerExecuteRule.dockerParams.dockerImage, is('hadolint/hadolint:latest-debian'))
assertThat(loggingRule.log, containsString("Unstash content: buildDescriptor"))
assertThat(shellRule.shell,
hasItems(
"hadolint ./Dockerfile --config .hadolint.yaml --format checkstyle > hadolint.xml"
)
)
assertThat(writeFileRule.files['.hadolint.yaml'], is('empty'))
}
@Test
void testNoDockerfile() {
helper.registerAllowedMethod 'fileExists', [String], { false }
thrown.expect AbortException
thrown.expectMessage '[hadolintExecute] Dockerfile \'./Dockerfile\' is not found.'
stepRule.step.hadolintExecute(script: nullScript, juStabUtils: utils, dockerImage: 'hadolint/hadolint:latest-debian')
}
}

View File

@@ -1,37 +1,15 @@
import static com.sap.piper.Prerequisites.checkScript
import com.sap.piper.GenerateDocumentation
import com.sap.piper.ConfigurationHelper
import com.sap.piper.JenkinsUtils
import com.sap.piper.Utils
import groovy.transform.Field
@Field def STEP_NAME = getClass().getName()
@Field Set GENERAL_CONFIG_KEYS = [
/**
* Dockerfile to be used for the assessment.
*/
'dockerFile',
/**
* Name of the docker image that should be used, in which node should be installed and configured. Default value is 'hadolint/hadolint:latest-debian'.
*/
'dockerImage'
]
@Field String METADATA_FILE = 'metadata/hadolint.yaml'
@Field Set GENERAL_CONFIG_KEYS = []
@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([
/**
* Name of the configuration file used locally within the step. If a file with this name is detected as part of your repo downloading the central configuration via `configurationUrl` will be skipped. If you change the file's name make sure your stashing configuration also reflects this.
*/
'configurationFile',
/**
* URL pointing to the .hadolint.yaml exclude configuration to be used for linting. Also have a look at `configurationFile` which could avoid central configuration download in case the file is part of your repository.
*/
'configurationUrl',
/**
* If the url provided as configurationUrl is protected, this Jenkins credential can be used to authenticate the request.
*/
'configurationCredentialsId',
/**
* Docker options to be set when starting the container.
*/
'dockerOptions',
/**
* Quality Gates to fail the build, see [warnings-ng plugin documentation](https://github.com/jenkinsci/warnings-plugin/blob/master/doc/Documentation.md#quality-gate-configuration).
*/
@@ -52,80 +30,40 @@ import groovy.transform.Field
*/
@GenerateDocumentation
void call(Map parameters = [:]) {
handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) {
final script = checkScript(this, parameters) ?: this
final utils = parameters.juStabUtils ?: new Utils()
String stageName = parameters.stageName ?: env.STAGE_NAME
final script = checkScript(this, parameters) ?: null
List credentialInfo = [
[type: 'usernamePassword', id: 'configurationCredentialsId', env: ['PIPER_configurationUsername', 'PIPER_configurationPassword']],
]
// load default & individual configuration
Map configuration = ConfigurationHelper.newInstance(this)
.loadStepDefaults([:], stageName)
.mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS)
.mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
.mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS)
.mixin(parameters, PARAMETER_KEYS)
.use()
new Utils().pushToSWA([
step: STEP_NAME,
stepParamKey1: 'scriptMissing',
stepParam1: parameters?.script == null
], configuration)
def existingStashes = utils.unstashAll(configuration.stashContent)
if (!fileExists(configuration.dockerFile)) {
error "[${STEP_NAME}] Dockerfile '${configuration.dockerFile}' is not found."
}
if(!fileExists(configuration.configurationFile) && configuration.configurationUrl) {
downloadFile(configuration.configurationUrl, configuration.configurationFile, configuration.configurationCredentialsId)
if(existingStashes) {
def stashName = 'hadolintConfiguration'
stash name: stashName, includes: configuration.configurationFile
existingStashes += stashName
}
}
def options = [
"--config ${configuration.configurationFile}",
"--format checkstyle > ${configuration.reportFile}"
]
dockerExecute(
script: script,
dockerImage: configuration.dockerImage,
dockerOptions: configuration.dockerOptions,
stashContent: existingStashes
) {
// HaDoLint status code is ignore, results will be handled by recordIssues / archiveArtifacts
def result = sh returnStatus: true, script: "hadolint ${configuration.dockerFile} ${options.join(' ')}"
archiveArtifacts configuration.reportFile
recordIssues(
tools: [checkStyle(
name: configuration.reportName,
pattern: configuration.reportFile,
id: configuration.reportName
)],
qualityGates: configuration.qualityGates,
enabledForFailure: true,
blameDisabled: true
)
def resultFileSize = 0
if (fileExists(configuration.reportFile)) {
resultFileSize = readFile(configuration.reportFile).length()
}
if (result != 0 && resultFileSize == 0) {
error "HaDoLint scan on file ${configuration.dockerFile} failed due to technical issues, please check the log."
}
}
issuesWrapper(parameters, script){
piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentialInfo)
}
}
void downloadFile(url, target, authentication = null){
def response = httpRequest url: url, authentication: authentication, timeout: 20
writeFile text: response.content, file: target
def issuesWrapper(Map parameters = [:], Script script, Closure body){
String stageName = parameters.stageName ?: env.STAGE_NAME
// load default & individual configuration
Map config = ConfigurationHelper.newInstance(this)
.loadStepDefaults([:], stageName)
.mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS)
.mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
.mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS)
.mixin(parameters, PARAMETER_KEYS)
.use()
try {
body()
} finally {
recordIssues(
blameDisabled: true,
enabledForFailure: true,
aggregatingResults: false,
qualityGates: config.qualityGates,
tool: checkStyle(
id: config.reportName,
name: config.reportName,
pattern: config.reportFile
)
)
}
}