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 
			
		
		
		
	Support maven params in detect scan (#1855)
Co-authored-by: Florian Wilhelm <florian.wilhelm02@sap.com> Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>
This commit is contained in:
		| @@ -2,12 +2,15 @@ package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	piperhttp "github.com/SAP/jenkins-library/pkg/http" | ||||
| 	"github.com/SAP/jenkins-library/pkg/maven" | ||||
| 	"strings" | ||||
|  | ||||
| 	sliceUtils "github.com/SAP/jenkins-library/pkg/piperutils" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/command" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/piperutils" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/SAP/jenkins-library/pkg/versioning" | ||||
| ) | ||||
| @@ -17,20 +20,12 @@ func detectExecuteScan(config detectExecuteScanOptions, telemetryData *telemetry | ||||
| 	// reroute command output to logging framework | ||||
| 	c.Stdout(log.Writer()) | ||||
| 	c.Stderr(log.Writer()) | ||||
| 	runDetect(config, &c) | ||||
| } | ||||
|  | ||||
| func runDetect(config detectExecuteScanOptions, command command.ShellRunner) { | ||||
| 	// detect execution details, see https://synopsys.atlassian.net/wiki/spaces/INTDOCS/pages/88440888/Sample+Synopsys+Detect+Scan+Configuration+Scenarios+for+Black+Duck | ||||
| 	fileUtils := piperutils.Files{} | ||||
| 	httpClient := piperhttp.Client{} | ||||
|  | ||||
| 	args := []string{"bash <(curl -s https://detect.synopsys.com/detect.sh)"} | ||||
| 	args = addDetectArgs(args, config) | ||||
| 	script := strings.Join(args, " ") | ||||
| 	err := runDetect(config, &c, &fileUtils, &httpClient) | ||||
|  | ||||
| 	command.SetDir(".") | ||||
| 	command.SetEnv([]string{"BLACKDUCK_SKIP_PHONE_HOME=true"}) | ||||
|  | ||||
| 	err := command.RunShell("/bin/bash", script) | ||||
| 	if err != nil { | ||||
| 		log.Entry(). | ||||
| 			WithError(err). | ||||
| @@ -38,7 +33,29 @@ func runDetect(config detectExecuteScanOptions, command command.ShellRunner) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func addDetectArgs(args []string, config detectExecuteScanOptions) []string { | ||||
| func runDetect(config detectExecuteScanOptions, command command.ShellRunner, fileUtils piperutils.FileUtils, httpClient piperhttp.Downloader) error { | ||||
| 	// detect execution details, see https://synopsys.atlassian.net/wiki/spaces/INTDOCS/pages/88440888/Sample+Synopsys+Detect+Scan+Configuration+Scenarios+for+Black+Duck | ||||
| 	httpClient.DownloadFile("https://detect.synopsys.com/detect.sh", "detect.sh", nil, nil) | ||||
| 	err := fileUtils.Chmod("detect.sh", 0700) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	args := []string{"./detect.sh"} | ||||
| 	args, err = addDetectArgs(args, config, fileUtils, httpClient) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	script := strings.Join(args, " ") | ||||
|  | ||||
| 	envs := []string{"BLACKDUCK_SKIP_PHONE_HOME=true"} | ||||
|  | ||||
| 	command.SetDir(".") | ||||
| 	command.SetEnv(envs) | ||||
|  | ||||
| 	return command.RunShell("/bin/bash", script) | ||||
| } | ||||
|  | ||||
| func addDetectArgs(args []string, config detectExecuteScanOptions, fileUtils piperutils.FileUtils, httpClient piperhttp.Downloader) ([]string, error) { | ||||
|  | ||||
| 	coordinates := struct { | ||||
| 		Version string | ||||
| @@ -80,5 +97,23 @@ func addDetectArgs(args []string, config detectExecuteScanOptions) []string { | ||||
| 	if sliceUtils.ContainsString(config.Scanners, "source") { | ||||
| 		args = append(args, fmt.Sprintf("--detect.source.path=%v", config.ScanPaths[0])) | ||||
| 	} | ||||
| 	return args | ||||
|  | ||||
| 	mavenArgs, err := maven.DownloadAndGetMavenParameters(config.GlobalSettingsFile, config.ProjectSettingsFile, fileUtils, httpClient) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if len(config.M2Path) > 0 { | ||||
| 		absolutePath, err := fileUtils.Abs(config.M2Path) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		mavenArgs = append(mavenArgs, fmt.Sprintf("-Dmaven.repo.local=%v", absolutePath)) | ||||
| 	} | ||||
|  | ||||
| 	if len(mavenArgs) > 0 { | ||||
| 		args = append(args, fmt.Sprintf("\"--detect.maven.build.command='%v'\"", strings.Join(mavenArgs, " "))) | ||||
| 	} | ||||
|  | ||||
| 	return args, nil | ||||
| } | ||||
|   | ||||
| @@ -25,6 +25,9 @@ type detectExecuteScanOptions struct { | ||||
| 	FailOn              []string `json:"failOn,omitempty"` | ||||
| 	Version             string   `json:"version,omitempty"` | ||||
| 	VersioningModel     string   `json:"versioningModel,omitempty"` | ||||
| 	ProjectSettingsFile string   `json:"projectSettingsFile,omitempty"` | ||||
| 	GlobalSettingsFile  string   `json:"globalSettingsFile,omitempty"` | ||||
| 	M2Path              string   `json:"m2Path,omitempty"` | ||||
| } | ||||
|  | ||||
| // DetectExecuteScanCommand Executes Synopsys Detect scan | ||||
| @@ -96,6 +99,9 @@ func addDetectExecuteScanFlags(cmd *cobra.Command, stepConfig *detectExecuteScan | ||||
| 	cmd.Flags().StringSliceVar(&stepConfig.FailOn, "failOn", []string{`BLOCKER`}, "Mark the current build as fail based on the policy categories applied.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.Version, "version", os.Getenv("PIPER_version"), "Defines the version number of the artifact being build in the pipeline. It is used as source for the Detect version.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.VersioningModel, "versioningModel", `major`, "The versioning model used for result reporting (based on the artifact version). Example 1.2.3 using `major` will result in version 1") | ||||
| 	cmd.Flags().StringVar(&stepConfig.ProjectSettingsFile, "projectSettingsFile", os.Getenv("PIPER_projectSettingsFile"), "Path or url to the mvn settings file that should be used as project settings file.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.GlobalSettingsFile, "globalSettingsFile", os.Getenv("PIPER_globalSettingsFile"), "Path or url to the mvn settings file that should be used as global settings file") | ||||
| 	cmd.Flags().StringVar(&stepConfig.M2Path, "m2Path", os.Getenv("PIPER_m2Path"), "Path to the location of the local repository that should be used.") | ||||
|  | ||||
| 	cmd.MarkFlagRequired("apiToken") | ||||
| 	cmd.MarkFlagRequired("projectName") | ||||
| @@ -200,6 +206,30 @@ func detectExecuteScanMetadata() config.StepData { | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "projectSettingsFile", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{{Name: "maven/projectSettingsFile"}}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "globalSettingsFile", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{{Name: "maven/globalSettingsFile"}}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "m2Path", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"GENERAL", "STEPS", "STAGES", "PARAMETERS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{{Name: "maven/m2Path"}}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
|   | ||||
| @@ -2,37 +2,93 @@ package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	piperhttp "github.com/SAP/jenkins-library/pkg/http" | ||||
| 	"github.com/SAP/jenkins-library/pkg/mock" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/mock" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| type httpClientMock struct { | ||||
| 	expectedError   error | ||||
| 	downloadedFiles map[string]string // src, dest | ||||
| } | ||||
|  | ||||
| func (c *httpClientMock) SetOptions(options piperhttp.ClientOptions) { | ||||
|  | ||||
| } | ||||
|  | ||||
| func (c *httpClientMock) DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error { | ||||
|  | ||||
| 	if c.expectedError != nil { | ||||
| 		return c.expectedError | ||||
| 	} | ||||
|  | ||||
| 	if c.downloadedFiles == nil { | ||||
| 		c.downloadedFiles = make(map[string]string) | ||||
| 	} | ||||
| 	c.downloadedFiles[url] = filename | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func TestRunDetect(t *testing.T) { | ||||
|  | ||||
| 	t.Run("success case", func(t *testing.T) { | ||||
| 		s := mock.ShellMockRunner{} | ||||
| 		runDetect(detectExecuteScanOptions{}, &s) | ||||
| 		fileUtilsMock := mock.FilesMock{} | ||||
| 		fileUtilsMock.AddFile("detect.sh", []byte("")) | ||||
| 		httpClient := httpClientMock{} | ||||
| 		err := runDetect(detectExecuteScanOptions{}, &s, &fileUtilsMock, &httpClient) | ||||
|  | ||||
| 		assert.Equal(t, httpClient.downloadedFiles["https://detect.synopsys.com/detect.sh"], "detect.sh") | ||||
| 		fileStatus, err := fileUtilsMock.Stat("detect.sh") | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, fileStatus.Mode(), os.FileMode(0700)) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, ".", s.Dir, "Wrong execution directory used") | ||||
| 		assert.Equal(t, "/bin/bash", s.Shell[0], "Bash shell expected") | ||||
| 		expectedScript := "bash <(curl -s https://detect.synopsys.com/detect.sh) --blackduck.url= --blackduck.api.token= --detect.project.name=\\\"\\\" --detect.project.version.name=\\\"\\\" --detect.code.location.name=\\\"\\\"" | ||||
| 		expectedScript := "./detect.sh --blackduck.url= --blackduck.api.token= --detect.project.name=\\\"\\\" --detect.project.version.name=\\\"\\\" --detect.code.location.name=\\\"\\\"" | ||||
| 		assert.Equal(t, expectedScript, s.Calls[0]) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("failure case", func(t *testing.T) { | ||||
| 		var hasFailed bool | ||||
| 		log.Entry().Logger.ExitFunc = func(int) { hasFailed = true } | ||||
| 		s := mock.ShellMockRunner{ShouldFailOnCommand: map[string]error{"./detect.sh --blackduck.url= --blackduck.api.token= --detect.project.name=\\\"\\\" --detect.project.version.name=\\\"\\\" --detect.code.location.name=\\\"\\\"": fmt.Errorf("Test Error")}} | ||||
| 		fileUtilsMock := mock.FilesMock{} | ||||
| 		httpClient := httpClientMock{} | ||||
| 		err := runDetect(detectExecuteScanOptions{}, &s, &fileUtilsMock, &httpClient) | ||||
| 		assert.NotNil(t, err) | ||||
| 	}) | ||||
|  | ||||
| 		s := mock.ShellMockRunner{ShouldFailOnCommand: map[string]error{"bash <(curl -s https://detect.synopsys.com/detect.sh) --blackduck.url= --blackduck.api.token= --detect.project.name=\\\"\\\" --detect.project.version.name=\\\"\\\" --detect.code.location.name=\\\"\\\"": fmt.Errorf("Test Error")}} | ||||
| 		runDetect(detectExecuteScanOptions{}, &s) | ||||
| 		assert.True(t, hasFailed, "expected command to exit with fatal") | ||||
| 	t.Run("maven parameters", func(t *testing.T) { | ||||
| 		s := mock.ShellMockRunner{} | ||||
| 		fileUtilsMock := mock.FilesMock{ | ||||
| 			CurrentDir: "root_folder", | ||||
| 		} | ||||
| 		fileUtilsMock.AddFile("detect.sh", []byte("")) | ||||
| 		httpClient := httpClientMock{} | ||||
| 		err := runDetect(detectExecuteScanOptions{ | ||||
| 			M2Path:              ".pipeline/local_repo", | ||||
| 			ProjectSettingsFile: "project-settings.xml", | ||||
| 			GlobalSettingsFile:  "global-settings.xml", | ||||
| 		}, &s, &fileUtilsMock, &httpClient) | ||||
|  | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, ".", s.Dir, "Wrong execution directory used") | ||||
| 		assert.Equal(t, "/bin/bash", s.Shell[0], "Bash shell expected") | ||||
| 		absoluteLocalPath := string(os.PathSeparator) + filepath.Join("root_folder", ".pipeline", "local_repo") | ||||
|  | ||||
| 		expectedParam := "\"--detect.maven.build.command='--global-settings global-settings.xml --settings project-settings.xml -Dmaven.repo.local=" + absoluteLocalPath + "'\"" | ||||
| 		assert.Contains(t, s.Calls[0], expectedParam) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestAddDetectArgs(t *testing.T) { | ||||
| 	httpClient := piperhttp.Client{} | ||||
| 	fileUtilsMock := mock.FilesMock{} | ||||
|  | ||||
| 	testData := []struct { | ||||
| 		args     []string | ||||
| 		options  detectExecuteScanOptions | ||||
| @@ -119,7 +175,8 @@ func TestAddDetectArgs(t *testing.T) { | ||||
|  | ||||
| 	for k, v := range testData { | ||||
| 		t.Run(fmt.Sprintf("run %v", k), func(t *testing.T) { | ||||
| 			got := addDetectArgs(v.args, v.options) | ||||
| 			got, err := addDetectArgs(v.args, v.options, &fileUtilsMock, &httpClient) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.Equal(t, v.expected, got) | ||||
| 		}) | ||||
| 	} | ||||
|   | ||||
| @@ -78,6 +78,10 @@ func (f *kanikoFileMock) Abs(path string) (string, error) { | ||||
| 	return "", fmt.Errorf("not implemented. func is only present in order to fullfil the interface contract. Needs to be ajusted in case it gets used.") | ||||
| } | ||||
|  | ||||
| func (f *kanikoFileMock) Glob(pattern string) (matches []string, err error) { | ||||
| 	return nil, fmt.Errorf("not implemented. func is only present in order to fullfil the interface contract. Needs to be ajusted in case it gets used.") | ||||
| } | ||||
|  | ||||
| func TestRunKanikoExecute(t *testing.T) { | ||||
|  | ||||
| 	t.Run("success case", func(t *testing.T) { | ||||
|   | ||||
| @@ -39,7 +39,7 @@ modules: | ||||
|       build-result: dist` | ||||
|  | ||||
| // for mocking | ||||
| var getSettingsFile = maven.GetSettingsFile | ||||
| var downloadAndCopySettingsFiles = maven.DownloadAndCopySettingsFiles | ||||
|  | ||||
| // MTABuildTarget ... | ||||
| type MTABuildTarget int | ||||
| @@ -348,28 +348,7 @@ func handleSettingsFiles(config mtaBuildOptions, | ||||
| 	p piperutils.FileUtils, | ||||
| 	httpClient piperhttp.Downloader) error { | ||||
|  | ||||
| 	if len(config.ProjectSettingsFile) > 0 { | ||||
|  | ||||
| 		if err := getSettingsFile(maven.ProjectSettingsFile, config.ProjectSettingsFile, p, httpClient); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 	} else { | ||||
|  | ||||
| 		log.Entry().Debugf("Project settings file not provided via configuration.") | ||||
| 	} | ||||
|  | ||||
| 	if len(config.GlobalSettingsFile) > 0 { | ||||
|  | ||||
| 		if err := getSettingsFile(maven.GlobalSettingsFile, config.GlobalSettingsFile, p, httpClient); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else { | ||||
|  | ||||
| 		log.Entry().Debugf("Global settings file not provided via configuration.") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| 	return downloadAndCopySettingsFiles(config.GlobalSettingsFile, config.ProjectSettingsFile, p, httpClient) | ||||
| } | ||||
|  | ||||
| func generateMta(id, applicationName, version string) (string, error) { | ||||
|   | ||||
| @@ -281,20 +281,20 @@ func TestMarBuild(t *testing.T) { | ||||
|  | ||||
| 	t.Run("Settings file releatd tests", func(t *testing.T) { | ||||
|  | ||||
| 		var settingsFile string | ||||
| 		var settingsFileType maven.SettingsFileType | ||||
| 		var projectSettingsFile string | ||||
| 		var globalSettingsFile string | ||||
|  | ||||
| 		defer func() { | ||||
| 			getSettingsFile = maven.GetSettingsFile | ||||
| 			downloadAndCopySettingsFiles = maven.DownloadAndCopySettingsFiles | ||||
| 		}() | ||||
|  | ||||
| 		getSettingsFile = func( | ||||
| 			sfType maven.SettingsFileType, | ||||
| 			src string, | ||||
| 			fileUtilsMock piperutils.FileUtils, | ||||
| 			httpClientMock piperhttp.Downloader) error { | ||||
| 			settingsFile = src | ||||
| 			settingsFileType = sfType | ||||
| 		downloadAndCopySettingsFiles = func( | ||||
| 			globalSettings string, | ||||
| 			projectSettings string, | ||||
| 			fileUtils piperutils.FileUtils, | ||||
| 			httpClient maven.SettingsDownloadUtils) error { | ||||
| 			projectSettingsFile = projectSettings | ||||
| 			globalSettingsFile = globalSettings | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| @@ -305,8 +305,8 @@ func TestMarBuild(t *testing.T) { | ||||
| 		t.Run("Copy global settings file", func(t *testing.T) { | ||||
|  | ||||
| 			defer func() { | ||||
| 				settingsFile = "" | ||||
| 				settingsFileType = -1 | ||||
| 				projectSettingsFile = "" | ||||
| 				globalSettingsFile = "" | ||||
| 			}() | ||||
|  | ||||
| 			e := mock.ExecMockRunner{} | ||||
| @@ -317,15 +317,15 @@ func TestMarBuild(t *testing.T) { | ||||
|  | ||||
| 			assert.Nil(t, err) | ||||
|  | ||||
| 			assert.Equal(t, settingsFile, "/opt/maven/settings.xml") | ||||
| 			assert.Equal(t, settingsFileType, maven.GlobalSettingsFile) | ||||
| 			assert.Equal(t, globalSettingsFile, "/opt/maven/settings.xml") | ||||
| 			assert.Equal(t, projectSettingsFile, "") | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("Copy project settings file", func(t *testing.T) { | ||||
|  | ||||
| 			defer func() { | ||||
| 				settingsFile = "" | ||||
| 				settingsFileType = -1 | ||||
| 				projectSettingsFile = "" | ||||
| 				globalSettingsFile = "" | ||||
| 			}() | ||||
|  | ||||
| 			e := mock.ExecMockRunner{} | ||||
| @@ -336,8 +336,8 @@ func TestMarBuild(t *testing.T) { | ||||
|  | ||||
| 			assert.Nil(t, err) | ||||
|  | ||||
| 			assert.Equal(t, "/my/project/settings.xml", settingsFile) | ||||
| 			assert.Equal(t, maven.ProjectSettingsFile, settingsFileType) | ||||
| 			assert.Equal(t, "/my/project/settings.xml", projectSettingsFile) | ||||
| 			assert.Equal(t, "", globalSettingsFile) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| @@ -391,6 +391,10 @@ func (f *MtaTestFileUtilsMock) Abs(path string) (string, error) { | ||||
| 	return "/root_folder/workspace/" + path, nil | ||||
| } | ||||
|  | ||||
| func (f *MtaTestFileUtilsMock) Glob(pattern string) (matches []string, err error) { | ||||
| 	return nil, fmt.Errorf("not implemented. func is only present in order to fullfil the interface contract. Needs to be ajusted in case it gets used.") | ||||
| } | ||||
|  | ||||
| func (f *MtaTestFileUtilsMock) Chmod(path string, mode os.FileMode) error { | ||||
| 	return fmt.Errorf("not implemented. func is only present in order to fullfil the interface contract. Needs to be ajusted in case it gets used.") | ||||
| } | ||||
|   | ||||
| @@ -46,6 +46,10 @@ func (f *FileUtilsMock) Abs(path string) (string, error) { | ||||
| 	return "", fmt.Errorf("not implemented. func is only present in order to fullfil the interface contract. Needs to be ajusted in case it gets used.") | ||||
| } | ||||
|  | ||||
| func (f *FileUtilsMock) Glob(pattern string) (matches []string, err error) { | ||||
| 	return nil, fmt.Errorf("not implemented. func is only present in order to fullfil the interface contract. Needs to be ajusted in case it gets used.") | ||||
| } | ||||
|  | ||||
| func TestDeploy(t *testing.T) { | ||||
| 	myXsDeployOptions := xsDeployOptions{ | ||||
| 		APIURL:                "https://example.org:12345", | ||||
|   | ||||
| @@ -43,11 +43,8 @@ type mavenExecRunner interface { | ||||
| } | ||||
|  | ||||
| type mavenUtils interface { | ||||
| 	FileExists(path string) (bool, error) | ||||
| 	piperutils.FileUtils | ||||
| 	DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error | ||||
| 	Glob(pattern string) (matches []string, err error) | ||||
| 	Getwd() (dir string, err error) | ||||
| 	Chdir(dir string) error | ||||
| } | ||||
|  | ||||
| type utilsBundle struct { | ||||
| @@ -290,21 +287,10 @@ func evaluateStdOut(options *ExecuteOptions) (*bytes.Buffer, io.Writer) { | ||||
| func getParametersFromOptions(options *ExecuteOptions, utils mavenUtils) ([]string, error) { | ||||
| 	var parameters []string | ||||
|  | ||||
| 	if options.GlobalSettingsFile != "" { | ||||
| 		globalSettingsFileName, err := downloadSettingsIfURL(options.GlobalSettingsFile, ".pipeline/mavenGlobalSettings.xml", utils) | ||||
| 	parameters, err := DownloadAndGetMavenParameters(options.GlobalSettingsFile, options.ProjectSettingsFile, utils, utils) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 		parameters = append(parameters, "--global-settings", globalSettingsFileName) | ||||
| 	} | ||||
|  | ||||
| 	if options.ProjectSettingsFile != "" { | ||||
| 		projectSettingsFileName, err := downloadSettingsIfURL(options.ProjectSettingsFile, ".pipeline/mavenProjectSettings.xml", utils) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		parameters = append(parameters, "--settings", projectSettingsFileName) | ||||
| 	} | ||||
|  | ||||
| 	if options.M2Path != "" { | ||||
| 		parameters = append(parameters, "-Dmaven.repo.local="+options.M2Path) | ||||
| @@ -333,33 +319,6 @@ func getParametersFromOptions(options *ExecuteOptions, utils mavenUtils) ([]stri | ||||
| 	return parameters, nil | ||||
| } | ||||
|  | ||||
| func downloadSettingsIfURL(settingsFileOption, settingsFile string, utils mavenUtils) (string, error) { | ||||
| 	result := settingsFileOption | ||||
| 	if strings.HasPrefix(settingsFileOption, "http:") || strings.HasPrefix(settingsFileOption, "https:") { | ||||
| 		err := downloadSettingsFromURL(settingsFileOption, settingsFile, utils) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		result = settingsFile | ||||
| 	} | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // ToDo replace with pkg/maven/settings GetSettingsFile | ||||
| func downloadSettingsFromURL(url, filename string, utils mavenUtils) error { | ||||
| 	exists, _ := utils.FileExists(filename) | ||||
| 	if exists { | ||||
| 		log.Entry().Infof("Not downloading maven settings file, because it already exists at '%s'", filename) | ||||
| 		return nil | ||||
| 	} | ||||
| 	err := utils.DownloadFile(url, filename, nil, nil) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to download maven settings from URL '%s' to file '%s': %w", | ||||
| 			url, filename, err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func GetTestModulesExcludes() []string { | ||||
| 	return getTestModulesExcludes(newUtils()) | ||||
| } | ||||
|   | ||||
| @@ -146,19 +146,6 @@ func TestGetParameters(t *testing.T) { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestDownloadSettingsFromURL(t *testing.T) { | ||||
| 	t.Run("should pass if download is successful", func(t *testing.T) { | ||||
| 		utils := newMockUtils(false) | ||||
| 		err := downloadSettingsFromURL("anyURL", "settings.xml", &utils) | ||||
| 		assert.NoError(t, err) | ||||
| 	}) | ||||
| 	t.Run("should fail if download fails", func(t *testing.T) { | ||||
| 		utils := newMockUtils(true) | ||||
| 		err := downloadSettingsFromURL("anyURL", "settings.xml", &utils) | ||||
| 		assert.EqualError(t, err, "failed to download maven settings from URL 'anyURL' to file 'settings.xml': something happened") | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestGetTestModulesExcludes(t *testing.T) { | ||||
| 	t.Run("Should return excludes for unit- and integration-tests", func(t *testing.T) { | ||||
| 		utils := newMockUtils(false) | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| package maven | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	piperhttp "github.com/SAP/jenkins-library/pkg/http" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/piperutils" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| @@ -13,44 +12,81 @@ import ( | ||||
|  | ||||
| var getenv = os.Getenv | ||||
|  | ||||
| // SettingsFileType ... | ||||
| type SettingsFileType int | ||||
|  | ||||
| const ( | ||||
| 	// GlobalSettingsFile ... | ||||
| 	GlobalSettingsFile SettingsFileType = iota | ||||
| 	// ProjectSettingsFile ... | ||||
| 	ProjectSettingsFile | ||||
| ) | ||||
|  | ||||
| // GetSettingsFile ... | ||||
| func GetSettingsFile(settingsFileType SettingsFileType, src string, fileUtils piperutils.FileUtils, httpClient piperhttp.Downloader) error { | ||||
|  | ||||
| 	var dest string | ||||
| 	var err error | ||||
|  | ||||
| 	switch settingsFileType { | ||||
| 	case GlobalSettingsFile: | ||||
| 		dest, err = getGlobalSettingsFileDest() | ||||
| 	case ProjectSettingsFile: | ||||
| 		dest, err = getProjectSettingsFileDest() | ||||
| 	default: | ||||
| 		return errors.New("Invalid SettingsFileType") | ||||
| type SettingsDownloadUtils interface { | ||||
| 	DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error | ||||
| } | ||||
|  | ||||
| func DownloadAndGetMavenParameters(globalSettingsFile string, projectSettingsFile string, fileUtils piperutils.FileUtils, httpClient SettingsDownloadUtils) ([]string, error) { | ||||
| 	mavenArgs := []string{} | ||||
| 	if len(globalSettingsFile) > 0 { | ||||
| 		globalSettingsFileName, err := downloadSettingsIfURL(globalSettingsFile, ".pipeline/mavenGlobalSettings.xml", fileUtils, httpClient, false) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		mavenArgs = append(mavenArgs, "--global-settings", globalSettingsFileName) | ||||
| 	} else { | ||||
|  | ||||
| 		log.Entry().Debugf("Global settings file not provided via configuration.") | ||||
| 	} | ||||
|  | ||||
| 	if len(projectSettingsFile) > 0 { | ||||
| 		projectSettingsFileName, err := downloadSettingsIfURL(projectSettingsFile, ".pipeline/mavenProjectSettings.xml", fileUtils, httpClient, false) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		mavenArgs = append(mavenArgs, "--settings", projectSettingsFileName) | ||||
| 	} else { | ||||
|  | ||||
| 		log.Entry().Debugf("Project settings file not provided via configuration.") | ||||
| 	} | ||||
| 	return mavenArgs, nil | ||||
| } | ||||
|  | ||||
| func DownloadAndCopySettingsFiles(globalSettingsFile string, projectSettingsFile string, fileUtils piperutils.FileUtils, httpClient SettingsDownloadUtils) error { | ||||
| 	if len(projectSettingsFile) > 0 { | ||||
| 		destination, err := getProjectSettingsFileDest() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if err := downloadAndCopySettingsFile(projectSettingsFile, destination, fileUtils, httpClient); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else { | ||||
|  | ||||
| 		log.Entry().Debugf("Project settings file not provided via configuration.") | ||||
| 	} | ||||
|  | ||||
| 	if len(globalSettingsFile) > 0 { | ||||
| 		destination, err := getGlobalSettingsFileDest() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := downloadAndCopySettingsFile(globalSettingsFile, destination, fileUtils, httpClient); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else { | ||||
|  | ||||
| 		log.Entry().Debugf("Global settings file not provided via configuration.") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func downloadAndCopySettingsFile(src string, dest string, fileUtils piperutils.FileUtils, httpClient SettingsDownloadUtils) error { | ||||
| 	if len(src) == 0 { | ||||
| 		return fmt.Errorf("Settings file source location not provided") | ||||
| 	} | ||||
|  | ||||
| 	if len(dest) == 0 { | ||||
| 		return fmt.Errorf("Settings file destination location not provided") | ||||
| 	} | ||||
|  | ||||
| 	log.Entry().Debugf("Copying file \"%s\" to \"%s\"", src, dest) | ||||
|  | ||||
| 	if strings.HasPrefix(src, "http:") || strings.HasPrefix(src, "https:") { | ||||
|  | ||||
| 		if err := httpClient.DownloadFile(src, dest, nil, nil); err != nil { | ||||
| 		err := downloadSettingsFromURL(src, dest, fileUtils, httpClient, true) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else { | ||||
| @@ -79,6 +115,32 @@ func GetSettingsFile(settingsFileType SettingsFileType, src string, fileUtils pi | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func downloadSettingsIfURL(settingsFileOption, settingsFile string, fileUtils piperutils.FileUtils, httpClient SettingsDownloadUtils, overwrite bool) (string, error) { | ||||
| 	result := settingsFileOption | ||||
| 	if strings.HasPrefix(settingsFileOption, "http:") || strings.HasPrefix(settingsFileOption, "https:") { | ||||
| 		err := downloadSettingsFromURL(settingsFileOption, settingsFile, fileUtils, httpClient, overwrite) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		result = settingsFile | ||||
| 	} | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| func downloadSettingsFromURL(url, filename string, fileUtils piperutils.FileUtils, httpClient SettingsDownloadUtils, overwrite bool) error { | ||||
| 	exists, _ := fileUtils.FileExists(filename) | ||||
| 	if exists && !overwrite { | ||||
| 		log.Entry().Infof("Not downloading maven settings file, because it already exists at '%s'", filename) | ||||
| 		return nil | ||||
| 	} | ||||
| 	err := httpClient.DownloadFile(url, filename, nil, nil) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to download maven settings from URL '%s' to file '%s': %w", | ||||
| 			url, filename, err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func getGlobalSettingsFileDest() (string, error) { | ||||
|  | ||||
| 	m2Home, err := getEnvironmentVariable("M2_HOME") | ||||
|   | ||||
| @@ -24,64 +24,50 @@ func TestSettings(t *testing.T) { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	t.Run("Invalid settings file type", func(t *testing.T) { | ||||
|  | ||||
| 		httpClient := httpMock{} | ||||
| 		fileUtils := fileUtilsMock{} | ||||
|  | ||||
| 		err := GetSettingsFile(-1, "/dev/null", &fileUtils, &httpClient) | ||||
|  | ||||
| 		if assert.Error(t, err) { | ||||
| 			assert.Equal(t, "Invalid SettingsFileType", err.Error()) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Settings file source location not provided", func(t *testing.T) { | ||||
|  | ||||
| 		httpClient := httpMock{} | ||||
| 		fileUtils := fileUtilsMock{} | ||||
|  | ||||
| 		err := GetSettingsFile(1, "", &fileUtils, &httpClient) | ||||
| 		err := downloadAndCopySettingsFile("", "foo", &fileUtils, &httpClient) | ||||
|  | ||||
| 		if assert.Error(t, err) { | ||||
| 			assert.Equal(t, "Settings file source location not provided", err.Error()) | ||||
| 		} | ||||
| 		assert.EqualError(t, err, "Settings file source location not provided") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Retrieve global settings file", func(t *testing.T) { | ||||
| 	t.Run("Settings file destination location not provided", func(t *testing.T) { | ||||
|  | ||||
| 		httpClient := httpMock{} | ||||
| 		fileUtils := fileUtilsMock{existingFiles: map[string]string{"/opt/sap/maven/global-settings.xml": ""}} | ||||
| 		fileUtils := fileUtilsMock{} | ||||
|  | ||||
| 		err := GetSettingsFile(GlobalSettingsFile, "/opt/sap/maven/global-settings.xml", &fileUtils, &httpClient) | ||||
| 		err := downloadAndCopySettingsFile("/opt/sap/maven/global-settings.xml", "", &fileUtils, &httpClient) | ||||
|  | ||||
| 		assert.EqualError(t, err, "Settings file destination location not provided") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Retrieve settings files", func(t *testing.T) { | ||||
|  | ||||
| 		httpClient := httpMock{} | ||||
| 		fileUtils := fileUtilsMock{existingFiles: map[string]string{ | ||||
| 			"/opt/sap/maven/global-settings.xml":  "", | ||||
| 			"/opt/sap/maven/project-settings.xml": "", | ||||
| 		}} | ||||
|  | ||||
| 		err := DownloadAndCopySettingsFiles("/opt/sap/maven/global-settings.xml", "/opt/sap/maven/project-settings.xml", &fileUtils, &httpClient) | ||||
|  | ||||
| 		if assert.NoError(t, err) { | ||||
| 			assert.Equal(t, "/usr/share/maven/conf/settings.xml", fileUtils.copiedFiles["/opt/sap/maven/global-settings.xml"]) | ||||
| 		} | ||||
|  | ||||
| 		assert.Empty(t, httpClient.downloadedFiles) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Retrieve project settings file", func(t *testing.T) { | ||||
|  | ||||
| 		httpClient := httpMock{} | ||||
| 		fileUtils := fileUtilsMock{existingFiles: map[string]string{"/opt/sap/maven/project-settings.xml": ""}} | ||||
|  | ||||
| 		err := GetSettingsFile(ProjectSettingsFile, "/opt/sap/maven/project-settings.xml", &fileUtils, &httpClient) | ||||
|  | ||||
| 		if assert.NoError(t, err) { | ||||
| 			assert.Equal(t, "/home/me/.m2/settings.xml", fileUtils.copiedFiles["/opt/sap/maven/project-settings.xml"]) | ||||
| 		} | ||||
|  | ||||
| 		assert.Empty(t, httpClient.downloadedFiles) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Retrieve global settings file via http", func(t *testing.T) { | ||||
| 	t.Run("Retrieve settings file via http", func(t *testing.T) { | ||||
|  | ||||
| 		httpClient := httpMock{} | ||||
| 		fileUtils := fileUtilsMock{} | ||||
|  | ||||
| 		err := GetSettingsFile(GlobalSettingsFile, "https://example.org/maven/global-settings.xml", &fileUtils, &httpClient) | ||||
| 		err := downloadAndCopySettingsFile("https://example.org/maven/global-settings.xml", "/usr/share/maven/conf/settings.xml", &fileUtils, &httpClient) | ||||
|  | ||||
| 		if assert.NoError(t, err) { | ||||
| 			assert.Equal(t, "/usr/share/maven/conf/settings.xml", httpClient.downloadedFiles["https://example.org/maven/global-settings.xml"]) | ||||
| @@ -93,22 +79,10 @@ func TestSettings(t *testing.T) { | ||||
| 		httpClient := httpMock{expectedError: fmt.Errorf("Download failed")} | ||||
| 		fileUtils := fileUtilsMock{} | ||||
|  | ||||
| 		err := GetSettingsFile(GlobalSettingsFile, "https://example.org/maven/global-settings.xml", &fileUtils, &httpClient) | ||||
| 		err := downloadAndCopySettingsFile("https://example.org/maven/global-settings.xml", "/usr/share/maven/conf/settings.xml", &fileUtils, &httpClient) | ||||
|  | ||||
| 		if assert.Error(t, err) { | ||||
| 			assert.Equal(t, "Download failed", err.Error()) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Retrieve project settings file via http", func(t *testing.T) { | ||||
|  | ||||
| 		httpClient := httpMock{} | ||||
| 		fileUtils := fileUtilsMock{} | ||||
|  | ||||
| 		err := GetSettingsFile(ProjectSettingsFile, "https://example.org/maven/project-settings.xml", &fileUtils, &httpClient) | ||||
|  | ||||
| 		if assert.NoError(t, err) { | ||||
| 			assert.Equal(t, "/home/me/.m2/settings.xml", httpClient.downloadedFiles["https://example.org/maven/project-settings.xml"]) | ||||
| 			assert.Contains(t, err.Error(), "failed to download maven settings from URL") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| @@ -117,7 +91,7 @@ func TestSettings(t *testing.T) { | ||||
| 		httpClient := httpMock{} | ||||
| 		fileUtils := fileUtilsMock{} | ||||
|  | ||||
| 		err := GetSettingsFile(ProjectSettingsFile, "/opt/sap/maven/project-settings.xml", &fileUtils, &httpClient) | ||||
| 		err := downloadAndCopySettingsFile("/opt/sap/maven/project-settings.xml", "/home/me/.m2/settings.xml", &fileUtils, &httpClient) | ||||
|  | ||||
| 		if assert.Error(t, err) { | ||||
| 			assert.Contains(t, err.Error(), "Source file '/opt/sap/maven/project-settings.xml' does not exist") | ||||
| @@ -208,3 +182,7 @@ func (f *fileUtilsMock) Chmod(path string, mode os.FileMode) error { | ||||
| func (f *fileUtilsMock) Abs(path string) (string, error) { | ||||
| 	return "", fmt.Errorf("not implemented. func is only present in order to fullfil the interface contract. Needs to be ajusted in case it gets used.") | ||||
| } | ||||
|  | ||||
| func (f *fileUtilsMock) Glob(pattern string) (matches []string, err error) { | ||||
| 	return nil, fmt.Errorf("not implemented. func is only present in order to fullfil the interface contract. Needs to be ajusted in case it gets used.") | ||||
| } | ||||
|   | ||||
| @@ -49,7 +49,7 @@ type FilesMock struct { | ||||
| 	files        map[string]*fileProperties | ||||
| 	writtenFiles []string | ||||
| 	removedFiles []string | ||||
| 	currentDir   string | ||||
| 	CurrentDir   string | ||||
| 	Separator    string | ||||
| } | ||||
|  | ||||
| @@ -67,10 +67,10 @@ func (f *FilesMock) init() { | ||||
| // Relative segments such as "../" are currently NOT supported. | ||||
| func (f *FilesMock) toAbsPath(path string) string { | ||||
| 	if path == "." { | ||||
| 		return f.Separator + f.currentDir | ||||
| 		return f.Separator + f.CurrentDir | ||||
| 	} | ||||
| 	if !strings.HasPrefix(path, f.Separator) { | ||||
| 		path = f.Separator + filepath.Join(f.currentDir, path) | ||||
| 		path = f.Separator + filepath.Join(f.CurrentDir, path) | ||||
| 	} | ||||
| 	return path | ||||
| } | ||||
| @@ -246,7 +246,7 @@ func (f *FilesMock) FileRemove(path string) error { | ||||
| 	leaf := filepath.Base(absPath) | ||||
| 	absPath = strings.TrimSuffix(absPath, f.Separator+leaf) | ||||
| 	if absPath != f.Separator { | ||||
| 		relPath := strings.TrimPrefix(absPath, f.Separator+f.currentDir+f.Separator) | ||||
| 		relPath := strings.TrimPrefix(absPath, f.Separator+f.CurrentDir+f.Separator) | ||||
| 		dirExists, _ := f.DirExists(relPath) | ||||
| 		if !dirExists { | ||||
| 			f.AddDir(relPath) | ||||
| @@ -304,7 +304,7 @@ func (f *FilesMock) Chdir(path string) error { | ||||
| 		return fmt.Errorf("failed to change current directory into '%s': %w", path, os.ErrNotExist) | ||||
| 	} | ||||
|  | ||||
| 	f.currentDir = strings.TrimLeft(path, f.Separator) | ||||
| 	f.CurrentDir = strings.TrimLeft(path, f.Separator) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @@ -368,5 +368,6 @@ func (f *FilesMock) Chmod(path string, mode os.FileMode) error { | ||||
| } | ||||
|  | ||||
| func (f *FilesMock) Abs(path string) (string, error) { | ||||
| 	f.init() | ||||
| 	return f.toAbsPath(path), nil | ||||
| } | ||||
|   | ||||
| @@ -21,6 +21,7 @@ type FileUtils interface { | ||||
| 	FileWrite(path string, content []byte, perm os.FileMode) error | ||||
| 	MkdirAll(path string, perm os.FileMode) error | ||||
| 	Chmod(path string, mode os.FileMode) error | ||||
| 	Glob(pattern string) (matches []string, err error) | ||||
| } | ||||
|  | ||||
| // Files ... | ||||
|   | ||||
| @@ -172,6 +172,39 @@ spec: | ||||
|           - major-minor | ||||
|           - semantic | ||||
|           - full | ||||
|       - name: projectSettingsFile | ||||
|         type: string | ||||
|         description: "Path or url to the mvn settings file that should be used as project settings file." | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         mandatory: false | ||||
|         aliases: | ||||
|           - name: maven/projectSettingsFile | ||||
|       - name: globalSettingsFile | ||||
|         type: string | ||||
|         description: "Path or url to the mvn settings file that should be used as global settings file" | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         mandatory: false | ||||
|         aliases: | ||||
|           - name: maven/globalSettingsFile | ||||
|       - name: m2Path | ||||
|         type: string | ||||
|         description: Path to the location of the local repository that should be used. | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - STEPS | ||||
|           - STAGES | ||||
|           - PARAMETERS | ||||
|         mandatory: false | ||||
|         aliases: | ||||
|           - name: maven/m2Path | ||||
|   containers: | ||||
|     - name: openjdk | ||||
|       image: openjdk:11 | ||||
|   | ||||
| @@ -1,9 +1,15 @@ | ||||
| import com.sap.piper.BuildTool | ||||
| import com.sap.piper.DownloadCacheUtils | ||||
| import groovy.transform.Field | ||||
|  | ||||
| import static com.sap.piper.Prerequisites.checkScript | ||||
|  | ||||
| @Field String STEP_NAME = getClass().getName() | ||||
| @Field String METADATA_FILE = 'metadata/detect.yaml' | ||||
|  | ||||
| void call(Map parameters = [:]) { | ||||
|     final script = checkScript(this, parameters) ?: this | ||||
|     parameters = DownloadCacheUtils.injectDownloadCacheInParameters(script, parameters, BuildTool.MAVEN) | ||||
|     List credentials = [ | ||||
|         [type: 'token', id: 'detectTokenCredentialsId', env: ['PIPER_apiToken']] | ||||
|     ] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user