You've already forked sap-jenkins-library
							
							
				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:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							e8c74a4867
						
					
				
				
					commit
					81c8553d6a
				
			
							
								
								
									
										127
									
								
								cmd/hadolintExecute.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								cmd/hadolintExecute.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										169
									
								
								cmd/hadolintExecute_generated.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								cmd/hadolintExecute_generated.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										16
									
								
								cmd/hadolintExecute_generated_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								cmd/hadolintExecute_generated_test.go
									
									
									
									
									
										Normal 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") | ||||
|  | ||||
| } | ||||
							
								
								
									
										86
									
								
								cmd/hadolintExecute_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								cmd/hadolintExecute_test.go
									
									
									
									
									
										Normal 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) | ||||
| 	}) | ||||
| } | ||||
| @@ -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()) | ||||
|   | ||||
							
								
								
									
										34
									
								
								pkg/hadolint/mocks/hadolintClient.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								pkg/hadolint/mocks/hadolintClient.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										49
									
								
								pkg/hadolint/mocks/hadolintPiperFileUtils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								pkg/hadolint/mocks/hadolintPiperFileUtils.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -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' | ||||
|   | ||||
							
								
								
									
										78
									
								
								resources/metadata/hadolint.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								resources/metadata/hadolint.yaml
									
									
									
									
									
										Normal 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 | ||||
| @@ -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') | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user