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 
			
		
		
		
	PythonBuild: Implementation of pythonBuild step (#3483)
* Implementation of pythonBuild step * minor update and refactoring * minor update * add integration test and test project to testdata dir * remove generated build data dir * Rewrite some logic. Minor fix in integration tests for python * Add new input parameters to pythonBuild.yaml * rewrite logic remove some checks * rollback * resolve merge conflict in piper.go Update logic in python build. Create bom now works fine * remove duplicate line * refactoring fix * resolve comment. Remove install build and change build command. Change twine upload command * add groovy wrapper for pythonBuild step * Rewrite tests. Remove some cheks from pythonBuild.go * add some test to pythonBuild_test.go * Add some parameters and credentials to the pythonBuild.groovy * fix issue in unit tests * add pythonBuild to fieldRelatedWhitelist * update integration test for pythonBuild * add imports * update integration tests and add a new one * minor fix * fix some issues in integration tests * update integration tests. Make it works again Co-authored-by: Anil Keshav <anil.keshav@sap.com> Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
This commit is contained in:
		| @@ -88,6 +88,7 @@ func GetAllStepMetadata() map[string]config.StepData { | ||||
| 		"npmExecuteScripts":                         npmExecuteScriptsMetadata(), | ||||
| 		"pipelineCreateScanSummary":                 pipelineCreateScanSummaryMetadata(), | ||||
| 		"protecodeExecuteScan":                      protecodeExecuteScanMetadata(), | ||||
| 		"pythonBuild":                               pythonBuildMetadata(), | ||||
| 		"shellExecute":                              shellExecuteMetadata(), | ||||
| 		"sonarExecuteScan":                          sonarExecuteScanMetadata(), | ||||
| 		"terraformExecute":                          terraformExecuteMetadata(), | ||||
|   | ||||
| @@ -183,6 +183,7 @@ func Execute() { | ||||
| 	rootCmd.AddCommand(ApiProxyUploadCommand()) | ||||
| 	rootCmd.AddCommand(GradleExecuteBuildCommand()) | ||||
| 	rootCmd.AddCommand(ApiKeyValueMapUploadCommand()) | ||||
| 	rootCmd.AddCommand(PythonBuildCommand()) | ||||
|  | ||||
| 	addRootFlags(rootCmd) | ||||
|  | ||||
|   | ||||
							
								
								
									
										107
									
								
								cmd/pythonBuild.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								cmd/pythonBuild.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"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" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	PyBomFilename = "bom.xml" | ||||
| ) | ||||
|  | ||||
| type pythonBuildUtils interface { | ||||
| 	command.ExecRunner | ||||
| 	FileExists(filename string) (bool, error) | ||||
| 	piperutils.FileUtils | ||||
| } | ||||
|  | ||||
| type pythonBuildUtilsBundle struct { | ||||
| 	*command.Command | ||||
| 	*piperutils.Files | ||||
| } | ||||
|  | ||||
| func newPythonBuildUtils() pythonBuildUtils { | ||||
| 	utils := pythonBuildUtilsBundle{ | ||||
| 		Command: &command.Command{}, | ||||
| 		Files:   &piperutils.Files{}, | ||||
| 	} | ||||
| 	// Reroute command output to logging framework | ||||
| 	utils.Stdout(log.Writer()) | ||||
| 	utils.Stderr(log.Writer()) | ||||
| 	return &utils | ||||
| } | ||||
|  | ||||
| func pythonBuild(config pythonBuildOptions, telemetryData *telemetry.CustomData) { | ||||
| 	utils := newPythonBuildUtils() | ||||
|  | ||||
| 	err := runPythonBuild(&config, telemetryData, utils) | ||||
| 	if err != nil { | ||||
| 		log.Entry().WithError(err).Fatal("step execution failed") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func runPythonBuild(config *pythonBuildOptions, telemetryData *telemetry.CustomData, utils pythonBuildUtils) error { | ||||
|  | ||||
| 	installFlags := []string{"-m", "pip", "install", "--upgrade"} | ||||
|  | ||||
| 	err := buildExecute(config, utils, installFlags) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Python build failed with error: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	if config.CreateBOM { | ||||
| 		if err := runBOMCreationForPy(utils, installFlags); err != nil { | ||||
| 			return fmt.Errorf("BOM creation failed: %w", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if config.Publish { | ||||
| 		if err := publishWithTwine(config, utils, installFlags); err != nil { | ||||
| 			return fmt.Errorf("failed to publish: %w", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func buildExecute(config *pythonBuildOptions, utils pythonBuildUtils, installFlags []string) error { | ||||
| 	var flags []string | ||||
| 	flags = append(flags, config.BuildFlags...) | ||||
| 	flags = append(flags, "setup.py", "sdist", "bdist_wheel") | ||||
|  | ||||
| 	log.Entry().Info("starting building python project:") | ||||
| 	err := utils.RunExecutable("python3", flags...) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func runBOMCreationForPy(utils pythonBuildUtils, installFlags []string) error { | ||||
| 	installFlags = append(installFlags, "cyclonedx-bom") | ||||
| 	if err := utils.RunExecutable("python3", installFlags...); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := utils.RunExecutable("cyclonedx-bom", "--e", "--output", PyBomFilename); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func publishWithTwine(config *pythonBuildOptions, utils pythonBuildUtils, installFlags []string) error { | ||||
| 	installFlags = append(installFlags, "twine") | ||||
| 	if err := utils.RunExecutable("python3", installFlags...); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := utils.RunExecutable("twine", "upload", "--username", config.TargetRepositoryUser, | ||||
| 		"--password", config.TargetRepositoryPassword, "--repository-url", config.TargetRepositoryURL, | ||||
| 		"dist/*"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										215
									
								
								cmd/pythonBuild_generated.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								cmd/pythonBuild_generated.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | ||||
| // 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/splunk" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/SAP/jenkins-library/pkg/validation" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| type pythonBuildOptions struct { | ||||
| 	BuildFlags               []string `json:"buildFlags,omitempty"` | ||||
| 	CreateBOM                bool     `json:"createBOM,omitempty"` | ||||
| 	Publish                  bool     `json:"publish,omitempty"` | ||||
| 	TargetRepositoryPassword string   `json:"targetRepositoryPassword,omitempty"` | ||||
| 	TargetRepositoryUser     string   `json:"targetRepositoryUser,omitempty"` | ||||
| 	TargetRepositoryURL      string   `json:"targetRepositoryURL,omitempty"` | ||||
| } | ||||
|  | ||||
| // PythonBuildCommand Step build a python project | ||||
| func PythonBuildCommand() *cobra.Command { | ||||
| 	const STEP_NAME = "pythonBuild" | ||||
|  | ||||
| 	metadata := pythonBuildMetadata() | ||||
| 	var stepConfig pythonBuildOptions | ||||
| 	var startTime time.Time | ||||
| 	var logCollector *log.CollectorHook | ||||
| 	var splunkClient *splunk.Splunk | ||||
| 	telemetryClient := &telemetry.Telemetry{} | ||||
|  | ||||
| 	var createPythonBuildCmd = &cobra.Command{ | ||||
| 		Use:   STEP_NAME, | ||||
| 		Short: "Step build a python project", | ||||
| 		Long:  `Step build python project with using test Vault credentials`, | ||||
| 		PreRunE: func(cmd *cobra.Command, _ []string) error { | ||||
| 			startTime = time.Now() | ||||
| 			log.SetStepName(STEP_NAME) | ||||
| 			log.SetVerbose(GeneralConfig.Verbose) | ||||
|  | ||||
| 			GeneralConfig.GitHubAccessTokens = ResolveAccessTokens(GeneralConfig.GitHubTokens) | ||||
|  | ||||
| 			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.TargetRepositoryPassword) | ||||
| 			log.RegisterSecret(stepConfig.TargetRepositoryUser) | ||||
|  | ||||
| 			if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { | ||||
| 				sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) | ||||
| 				log.RegisterHook(&sentryHook) | ||||
| 			} | ||||
|  | ||||
| 			if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { | ||||
| 				splunkClient = &splunk.Splunk{} | ||||
| 				logCollector = &log.CollectorHook{CorrelationID: GeneralConfig.CorrelationID} | ||||
| 				log.RegisterHook(logCollector) | ||||
| 			} | ||||
|  | ||||
| 			validation, err := validation.New(validation.WithJSONNamesForStructFields(), validation.WithPredefinedErrorMessages()) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			if err = validation.ValidateStruct(stepConfig); err != nil { | ||||
| 				log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			return nil | ||||
| 		}, | ||||
| 		Run: func(_ *cobra.Command, _ []string) { | ||||
| 			stepTelemetryData := telemetry.CustomData{} | ||||
| 			stepTelemetryData.ErrorCode = "1" | ||||
| 			handler := func() { | ||||
| 				config.RemoveVaultSecretFiles() | ||||
| 				stepTelemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) | ||||
| 				stepTelemetryData.ErrorCategory = log.GetErrorCategory().String() | ||||
| 				stepTelemetryData.PiperCommitHash = GitCommit | ||||
| 				telemetryClient.SetData(&stepTelemetryData) | ||||
| 				telemetryClient.Send() | ||||
| 				if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { | ||||
| 					splunkClient.Send(telemetryClient.GetData(), logCollector) | ||||
| 				} | ||||
| 			} | ||||
| 			log.DeferExitHandler(handler) | ||||
| 			defer handler() | ||||
| 			telemetryClient.Initialize(GeneralConfig.NoTelemetry, STEP_NAME) | ||||
| 			if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { | ||||
| 				splunkClient.Initialize(GeneralConfig.CorrelationID, | ||||
| 					GeneralConfig.HookConfig.SplunkConfig.Dsn, | ||||
| 					GeneralConfig.HookConfig.SplunkConfig.Token, | ||||
| 					GeneralConfig.HookConfig.SplunkConfig.Index, | ||||
| 					GeneralConfig.HookConfig.SplunkConfig.SendLogs) | ||||
| 			} | ||||
| 			pythonBuild(stepConfig, &stepTelemetryData) | ||||
| 			stepTelemetryData.ErrorCode = "0" | ||||
| 			log.Entry().Info("SUCCESS") | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	addPythonBuildFlags(createPythonBuildCmd, &stepConfig) | ||||
| 	return createPythonBuildCmd | ||||
| } | ||||
|  | ||||
| func addPythonBuildFlags(cmd *cobra.Command, stepConfig *pythonBuildOptions) { | ||||
| 	cmd.Flags().StringSliceVar(&stepConfig.BuildFlags, "buildFlags", []string{}, "Defines list of build flags to be used.") | ||||
| 	cmd.Flags().BoolVar(&stepConfig.CreateBOM, "createBOM", false, "Creates the bill of materials (BOM) using CycloneDX plugin.") | ||||
| 	cmd.Flags().BoolVar(&stepConfig.Publish, "publish", false, "Configures the build to publish artifacts to a repository.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.TargetRepositoryPassword, "targetRepositoryPassword", os.Getenv("PIPER_targetRepositoryPassword"), "Password for the target repository where the compiled binaries shall be uploaded - typically provided by the CI/CD environment.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.TargetRepositoryUser, "targetRepositoryUser", os.Getenv("PIPER_targetRepositoryUser"), "Username for the target repository where the compiled binaries shall be uploaded - typically provided by the CI/CD environment.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.TargetRepositoryURL, "targetRepositoryURL", os.Getenv("PIPER_targetRepositoryURL"), "URL of the target repository where the compiled binaries shall be uploaded - typically provided by the CI/CD environment.") | ||||
|  | ||||
| } | ||||
|  | ||||
| // retrieve step metadata | ||||
| func pythonBuildMetadata() config.StepData { | ||||
| 	var theMetaData = config.StepData{ | ||||
| 		Metadata: config.StepMetadata{ | ||||
| 			Name:        "pythonBuild", | ||||
| 			Aliases:     []config.Alias{}, | ||||
| 			Description: "Step build a python project", | ||||
| 		}, | ||||
| 		Spec: config.StepSpec{ | ||||
| 			Inputs: config.StepInputs{ | ||||
| 				Parameters: []config.StepParameters{ | ||||
| 					{ | ||||
| 						Name:        "buildFlags", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "[]string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 						Default:     []string{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "createBOM", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"GENERAL", "STEPS", "STAGES", "PARAMETERS"}, | ||||
| 						Type:        "bool", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 						Default:     false, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "publish", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"STEPS", "STAGES", "PARAMETERS"}, | ||||
| 						Type:        "bool", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 						Default:     false, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "targetRepositoryPassword", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "commonPipelineEnvironment", | ||||
| 								Param: "custom/repositoryPassword", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: false, | ||||
| 						Aliases:   []config.Alias{}, | ||||
| 						Default:   os.Getenv("PIPER_targetRepositoryPassword"), | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "targetRepositoryUser", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "commonPipelineEnvironment", | ||||
| 								Param: "custom/repositoryUsername", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: false, | ||||
| 						Aliases:   []config.Alias{}, | ||||
| 						Default:   os.Getenv("PIPER_targetRepositoryUser"), | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "targetRepositoryURL", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "commonPipelineEnvironment", | ||||
| 								Param: "custom/repositoryUrl", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: false, | ||||
| 						Aliases:   []config.Alias{}, | ||||
| 						Default:   os.Getenv("PIPER_targetRepositoryURL"), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Containers: []config.Container{ | ||||
| 				{Name: "python", Image: "python:3.9", WorkingDir: "/home/node"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	return theMetaData | ||||
| } | ||||
							
								
								
									
										17
									
								
								cmd/pythonBuild_generated_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cmd/pythonBuild_generated_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestPythonBuildCommand(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	testCmd := PythonBuildCommand() | ||||
|  | ||||
| 	// only high level testing performed - details are tested in step generation procedure | ||||
| 	assert.Equal(t, "pythonBuild", testCmd.Use, "command name incorrect") | ||||
|  | ||||
| } | ||||
							
								
								
									
										127
									
								
								cmd/pythonBuild_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								cmd/pythonBuild_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	piperhttp "github.com/SAP/jenkins-library/pkg/http" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/mock" | ||||
| ) | ||||
|  | ||||
| type pythonBuildMockUtils struct { | ||||
| 	t      *testing.T | ||||
| 	config *pythonBuildOptions | ||||
| 	*mock.ExecMockRunner | ||||
| 	*mock.FilesMock | ||||
| } | ||||
|  | ||||
| type puthonBuildMockUtils struct { | ||||
| 	*mock.ExecMockRunner | ||||
| 	*mock.FilesMock | ||||
|  | ||||
| 	clientOptions []piperhttp.ClientOptions // set by mock | ||||
| 	fileUploads   map[string]string         // set by mock | ||||
| } | ||||
|  | ||||
| func newPythonBuildTestsUtils() pythonBuildMockUtils { | ||||
| 	utils := pythonBuildMockUtils{ | ||||
| 		ExecMockRunner: &mock.ExecMockRunner{}, | ||||
| 		FilesMock:      &mock.FilesMock{}, | ||||
| 	} | ||||
| 	return utils | ||||
| } | ||||
|  | ||||
| func (f *pythonBuildMockUtils) GetConfig() *pythonBuildOptions { | ||||
| 	return f.config | ||||
| } | ||||
|  | ||||
| func TestRunPythonBuild(t *testing.T) { | ||||
|  | ||||
| 	t.Run("success - build", func(t *testing.T) { | ||||
| 		config := pythonBuildOptions{} | ||||
| 		utils := newPythonBuildTestsUtils() | ||||
| 		telemetryData := telemetry.CustomData{} | ||||
|  | ||||
| 		err := runPythonBuild(&config, &telemetryData, utils) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "python3", utils.ExecMockRunner.Calls[0].Exec) | ||||
| 		assert.Equal(t, []string{"setup.py", "sdist", "bdist_wheel"}, utils.ExecMockRunner.Calls[0].Params) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("failure - build failure", func(t *testing.T) { | ||||
| 		config := pythonBuildOptions{} | ||||
| 		utils := newPythonBuildTestsUtils() | ||||
| 		utils.ShouldFailOnCommand = map[string]error{"python3 setup.py sdist bdist_wheel": fmt.Errorf("build failure")} | ||||
| 		telemetryData := telemetry.CustomData{} | ||||
|  | ||||
| 		err := runPythonBuild(&config, &telemetryData, utils) | ||||
| 		assert.EqualError(t, err, "Python build failed with error: build failure") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("success - publishes binaries", func(t *testing.T) { | ||||
| 		config := pythonBuildOptions{ | ||||
| 			Publish:                  true, | ||||
| 			TargetRepositoryURL:      "https://my.target.repository.local", | ||||
| 			TargetRepositoryUser:     "user", | ||||
| 			TargetRepositoryPassword: "password", | ||||
| 		} | ||||
| 		utils := newPythonBuildTestsUtils() | ||||
| 		telemetryData := telemetry.CustomData{} | ||||
|  | ||||
| 		err := runPythonBuild(&config, &telemetryData, utils) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "python3", utils.ExecMockRunner.Calls[0].Exec) | ||||
| 		assert.Equal(t, []string{"setup.py", "sdist", "bdist_wheel"}, utils.ExecMockRunner.Calls[0].Params) | ||||
| 		assert.Equal(t, "python3", utils.ExecMockRunner.Calls[1].Exec) | ||||
| 		assert.Equal(t, []string{"-m", "pip", "install", "--upgrade", "twine"}, utils.ExecMockRunner.Calls[1].Params) | ||||
| 		assert.Equal(t, "twine", utils.ExecMockRunner.Calls[2].Exec) | ||||
| 		assert.Equal(t, []string{"upload", "--username", config.TargetRepositoryUser, | ||||
| 			"--password", config.TargetRepositoryPassword, "--repository-url", config.TargetRepositoryURL, | ||||
| 			"dist/*"}, utils.ExecMockRunner.Calls[2].Params) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("success - create BOM", func(t *testing.T) { | ||||
| 		config := pythonBuildOptions{ | ||||
| 			CreateBOM: true, | ||||
| 		} | ||||
| 		utils := newPythonBuildTestsUtils() | ||||
| 		telemetryData := telemetry.CustomData{} | ||||
|  | ||||
| 		err := runPythonBuild(&config, &telemetryData, utils) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "python3", utils.ExecMockRunner.Calls[0].Exec) | ||||
| 		assert.Equal(t, []string{"setup.py", "sdist", "bdist_wheel"}, utils.ExecMockRunner.Calls[0].Params) | ||||
| 		assert.Equal(t, "python3", utils.ExecMockRunner.Calls[1].Exec) | ||||
| 		assert.Equal(t, []string{"-m", "pip", "install", "--upgrade", "cyclonedx-bom"}, utils.ExecMockRunner.Calls[1].Params) | ||||
| 		assert.Equal(t, "cyclonedx-bom", utils.ExecMockRunner.Calls[2].Exec) | ||||
| 		assert.Equal(t, []string{"--e", "--output", "bom.xml"}, utils.ExecMockRunner.Calls[2].Params) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("failure - install pre-requisites for BOM creation", func(t *testing.T) { | ||||
| 		config := pythonBuildOptions{ | ||||
| 			CreateBOM: true, | ||||
| 		} | ||||
| 		utils := newPythonBuildTestsUtils() | ||||
| 		utils.ShouldFailOnCommand = map[string]error{"python3 -m pip install --upgrade cyclonedx-bom": fmt.Errorf("install failure")} | ||||
| 		telemetryData := telemetry.CustomData{} | ||||
|  | ||||
| 		err := runPythonBuild(&config, &telemetryData, utils) | ||||
| 		assert.EqualError(t, err, "BOM creation failed: install failure") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("failure - install pre-requisites for Twine upload", func(t *testing.T) { | ||||
| 		config := pythonBuildOptions{ | ||||
| 			Publish: true, | ||||
| 		} | ||||
| 		utils := newPythonBuildTestsUtils() | ||||
| 		utils.ShouldFailOnCommand = map[string]error{"python3 -m pip install --upgrade twine": fmt.Errorf("install failure")} | ||||
| 		telemetryData := telemetry.CustomData{} | ||||
|  | ||||
| 		err := runPythonBuild(&config, &telemetryData, utils) | ||||
| 		assert.EqualError(t, err, "failed to publish: install failure") | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										89
									
								
								integration/integration_python_build_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								integration/integration_python_build_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| //go:build integration | ||||
| // +build integration | ||||
|  | ||||
| // can be execute with go test -tags=integration ./integration/... | ||||
|  | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/testcontainers/testcontainers-go" | ||||
| ) | ||||
|  | ||||
| func TestBuildPythonProject(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 	ctx := context.Background() | ||||
| 	pwd, err := os.Getwd() | ||||
| 	assert.NoError(t, err, "Getting current working directory failed.") | ||||
| 	pwd = filepath.Dir(pwd) | ||||
|  | ||||
| 	tempDir, err := createTmpDir("") | ||||
| 	defer os.RemoveAll(tempDir) // clean up | ||||
| 	assert.NoError(t, err, "Error when creating temp dir") | ||||
|  | ||||
| 	err = copyDir(filepath.Join(pwd, "integration", "testdata", "TestPythonIntegration", "python-project"), tempDir) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("Failed to copy test project.") | ||||
| 	} | ||||
|  | ||||
| 	//workaround to use test script util it is possible to set workdir for Exec call | ||||
| 	testScript := fmt.Sprintf(`#!/bin/sh | ||||
| 		cd /test | ||||
| 		/piperbin/piper pythonBuild >test-log.txt 2>&1`) | ||||
| 	ioutil.WriteFile(filepath.Join(tempDir, "runPiper.sh"), []byte(testScript), 0700) | ||||
|  | ||||
| 	reqNode := testcontainers.ContainerRequest{ | ||||
| 		Image: "python:3.9", | ||||
| 		Cmd:   []string{"tail", "-f"}, | ||||
| 		BindMounts: map[string]string{ | ||||
| 			pwd:     "/piperbin", | ||||
| 			tempDir: "/test", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	nodeContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ | ||||
| 		ContainerRequest: reqNode, | ||||
| 		Started:          true, | ||||
| 	}) | ||||
|  | ||||
| 	code, err := nodeContainer.Exec(ctx, []string{"sh", "/test/runPiper.sh"}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 0, code) | ||||
|  | ||||
| 	content, err := ioutil.ReadFile(filepath.Join(tempDir, "/test-log.txt")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("Could not read test-log.txt.", err) | ||||
| 	} | ||||
| 	output := string(content) | ||||
|  | ||||
| 	assert.Contains(t, output, "info  pythonBuild - running command: python3 setup.py sdist bdist_wheel") | ||||
| 	assert.Contains(t, output, "info  pythonBuild - running command: python3 -m pip install --upgrade cyclonedx-bom") | ||||
| 	assert.Contains(t, output, "info  pythonBuild - running command: cyclonedx-bom --e --output bom.xml") | ||||
| 	assert.Contains(t, output, "info  pythonBuild - SUCCESS") | ||||
|  | ||||
| 	//workaround to use test script util it is possible to set workdir for Exec call | ||||
| 	testScript = fmt.Sprintf(`#!/bin/sh | ||||
| 		cd /test | ||||
| 		ls -l . dist build >files-list.txt 2>&1`) | ||||
| 	ioutil.WriteFile(filepath.Join(tempDir, "runPiper.sh"), []byte(testScript), 0700) | ||||
|  | ||||
| 	code, err = nodeContainer.Exec(ctx, []string{"sh", "/test/runPiper.sh"}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 0, code) | ||||
|  | ||||
| 	content, err = ioutil.ReadFile(filepath.Join(tempDir, "/files-list.txt")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("Could not read files-list.txt.", err) | ||||
| 	} | ||||
| 	output = string(content) | ||||
| 	assert.Contains(t, output, "bom.xml") | ||||
| 	assert.Contains(t, output, "example-pkg-0.0.1.tar.gz") | ||||
| 	assert.Contains(t, output, "example_pkg-0.0.1-py3-none-any.whl") | ||||
| } | ||||
							
								
								
									
										6
									
								
								integration/testdata/TestPythonIntegration/python-project/.pipeline/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								integration/testdata/TestPythonIntegration/python-project/.pipeline/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| general: | ||||
|  | ||||
| steps: | ||||
|   pythonBuild: | ||||
|     createBOM: true | ||||
|     publish: false | ||||
							
								
								
									
										19
									
								
								integration/testdata/TestPythonIntegration/python-project/LICENSE
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								integration/testdata/TestPythonIntegration/python-project/LICENSE
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| Copyright (c) 2018 The Python Packaging Authority | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
							
								
								
									
										5
									
								
								integration/testdata/TestPythonIntegration/python-project/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								integration/testdata/TestPythonIntegration/python-project/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| # Example Package | ||||
|  | ||||
| This is a simple example package. You can use | ||||
| [Github-flavored Markdown](https://guides.github.com/features/mastering-markdown/) | ||||
| to write your content. | ||||
							
								
								
									
										6
									
								
								integration/testdata/TestPythonIntegration/python-project/pyproject.toml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								integration/testdata/TestPythonIntegration/python-project/pyproject.toml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| [build-system] | ||||
| requires = [ | ||||
|     "setuptools>=42", | ||||
|     "wheel" | ||||
| ] | ||||
| build-backend = "setuptools.build_meta" | ||||
							
								
								
									
										24
									
								
								integration/testdata/TestPythonIntegration/python-project/setup.cfg
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								integration/testdata/TestPythonIntegration/python-project/setup.cfg
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| [metadata] | ||||
| name = example-package-TEST | ||||
| version = 0.0.1 | ||||
| author = Example Author | ||||
| author_email = author@example.com | ||||
| description = A small example package | ||||
| long_description = file: README.md | ||||
| long_description_content_type = text/markdown | ||||
| url = https://github.com/pypa/sampleproject | ||||
| project_urls = | ||||
|     Bug Tracker = https://github.com/pypa/sampleproject/issues | ||||
| classifiers = | ||||
|     Programming Language :: Python :: 3 | ||||
|     License :: OSI Approved :: MIT License | ||||
|     Operating System :: OS Independent | ||||
|  | ||||
| [options] | ||||
| package_dir = | ||||
|     = src | ||||
| packages = find: | ||||
| python_requires = >=3.6 | ||||
|  | ||||
| [options.packages.find] | ||||
| where = src | ||||
							
								
								
									
										19
									
								
								integration/testdata/TestPythonIntegration/python-project/setup.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								integration/testdata/TestPythonIntegration/python-project/setup.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import setuptools | ||||
|  | ||||
| setuptools.setup( | ||||
|     name="example-pkg", | ||||
|     version="0.0.1", | ||||
|     author="Example Author", | ||||
|     author_email="author@example.com", | ||||
|     description="A small example package", | ||||
|     long_description="Long description for small example package", | ||||
|     long_description_content_type="text/markdown", | ||||
|     url="https://github.com/example/pypi/github", | ||||
|     packages=setuptools.find_packages(), | ||||
|     classifiers=[ | ||||
|         "Programming Language :: Python :: 3", | ||||
|         "License :: OSI Approved :: MIT License", | ||||
|         "Operating System :: OS Independent", | ||||
|     ], | ||||
|     python_requires='>=3.6', | ||||
| ) | ||||
							
								
								
									
										0
									
								
								integration/testdata/TestPythonIntegration/python-project/src/example_package/__init__.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								integration/testdata/TestPythonIntegration/python-project/src/example_package/__init__.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
									
										2
									
								
								integration/testdata/TestPythonIntegration/python-project/src/example_package/example.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								integration/testdata/TestPythonIntegration/python-project/src/example_package/example.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| def add_one(number): | ||||
|     return number + 1 | ||||
							
								
								
									
										16
									
								
								integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/PKG-INFO
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/PKG-INFO
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| Metadata-Version: 2.1 | ||||
| Name: example-pkg | ||||
| Version: 0.0.1 | ||||
| Summary: A small example package | ||||
| Home-page: https://github.com/example/pypi/github | ||||
| Author: Example Author | ||||
| Author-email: author@example.com | ||||
| License: UNKNOWN | ||||
| Project-URL: Bug Tracker, https://github.com/pypa/sampleproject/issues | ||||
| Description: Long description for small example package | ||||
| Platform: UNKNOWN | ||||
| Classifier: Programming Language :: Python :: 3 | ||||
| Classifier: License :: OSI Approved :: MIT License | ||||
| Classifier: Operating System :: OS Independent | ||||
| Requires-Python: >=3.6 | ||||
| Description-Content-Type: text/markdown | ||||
							
								
								
									
										10
									
								
								integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/SOURCES.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/SOURCES.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| README.md | ||||
| pyproject.toml | ||||
| setup.cfg | ||||
| setup.py | ||||
| src/example_package/__init__.py | ||||
| src/example_package/example.py | ||||
| src/example_pkg.egg-info/PKG-INFO | ||||
| src/example_pkg.egg-info/SOURCES.txt | ||||
| src/example_pkg.egg-info/dependency_links.txt | ||||
| src/example_pkg.egg-info/top_level.txt | ||||
| @@ -0,0 +1 @@ | ||||
|  | ||||
| @@ -0,0 +1 @@ | ||||
| example_package | ||||
							
								
								
									
										66
									
								
								resources/metadata/pythonBuild.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								resources/metadata/pythonBuild.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| metadata: | ||||
|   name: pythonBuild | ||||
|   description: Step build a python project | ||||
|   longDescription: Step build python project with using test Vault credentials | ||||
| spec: | ||||
|   inputs: | ||||
|     params: | ||||
|       - name: buildFlags | ||||
|         type: "[]string" | ||||
|         description: Defines list of build flags to be used. | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|       - name: createBOM | ||||
|         type: bool | ||||
|         description: Creates the bill of materials (BOM) using CycloneDX plugin. | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - STEPS | ||||
|           - STAGES | ||||
|           - PARAMETERS | ||||
|         default: false | ||||
|       - name: publish | ||||
|         type: bool | ||||
|         description: Configures the build to publish artifacts to a repository. | ||||
|         scope: | ||||
|           - STEPS | ||||
|           - STAGES | ||||
|           - PARAMETERS | ||||
|       - name: targetRepositoryPassword | ||||
|         description: "Password for the target repository where the compiled binaries shall be uploaded - typically provided by the CI/CD environment." | ||||
|         type: string | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         secret: true | ||||
|         resourceRef: | ||||
|           - name: commonPipelineEnvironment | ||||
|             param: custom/repositoryPassword | ||||
|       - name: targetRepositoryUser | ||||
|         description: "Username for the target repository where the compiled binaries shall be uploaded - typically provided by the CI/CD environment." | ||||
|         type: string | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         secret: true | ||||
|         resourceRef: | ||||
|           - name: commonPipelineEnvironment | ||||
|             param: custom/repositoryUsername | ||||
|       - name: targetRepositoryURL | ||||
|         description: "URL of the target repository where the compiled binaries shall be uploaded - typically provided by the CI/CD environment." | ||||
|         type: string | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         resourceRef: | ||||
|           - name: commonPipelineEnvironment | ||||
|             param: custom/repositoryUrl | ||||
|   containers: | ||||
|     - name: python | ||||
|       image: python:3.9 | ||||
|       workingDir: /home/node | ||||
| @@ -213,6 +213,7 @@ public class CommonStepsTest extends BasePiperTest{ | ||||
|         'gradleExecuteBuild', //implementing new golang pattern without fields | ||||
|         'shellExecute', //implementing new golang pattern without fields | ||||
|         'apiKeyValueMapUpload', //implementing new golang pattern without fields | ||||
|         'pythonBuild', //implementing new golang pattern without fields | ||||
|     ] | ||||
|  | ||||
|     @Test | ||||
|   | ||||
							
								
								
									
										16
									
								
								vars/pythonBuild.groovy
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								vars/pythonBuild.groovy
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import com.sap.piper.BuildTool | ||||
| import com.sap.piper.DownloadCacheUtils | ||||
| import groovy.transform.Field | ||||
|  | ||||
| import static com.sap.piper.Prerequisites.checkScript | ||||
|  | ||||
| @Field String METADATA_FILE = 'metadata/pythonBuild.yaml' | ||||
| @Field String STEP_NAME = getClass().getName() | ||||
|  | ||||
| void call(Map parameters = [:]) { | ||||
|     List credentials = [[type: 'token', id: 'altDeploymentRepositoryPasswordId', env: ['PIPER_altDeploymentRepositoryPassword']]] | ||||
|     final script = checkScript(this, parameters) ?: this | ||||
|     parameters = DownloadCacheUtils.injectDownloadCacheInParameters(script, parameters, BuildTool.PIP) | ||||
|  | ||||
|     piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user