You've already forked sap-jenkins-library
mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-07-13 01:30:24 +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(),
|
"npmExecuteScripts": npmExecuteScriptsMetadata(),
|
||||||
"pipelineCreateScanSummary": pipelineCreateScanSummaryMetadata(),
|
"pipelineCreateScanSummary": pipelineCreateScanSummaryMetadata(),
|
||||||
"protecodeExecuteScan": protecodeExecuteScanMetadata(),
|
"protecodeExecuteScan": protecodeExecuteScanMetadata(),
|
||||||
|
"pythonBuild": pythonBuildMetadata(),
|
||||||
"shellExecute": shellExecuteMetadata(),
|
"shellExecute": shellExecuteMetadata(),
|
||||||
"sonarExecuteScan": sonarExecuteScanMetadata(),
|
"sonarExecuteScan": sonarExecuteScanMetadata(),
|
||||||
"terraformExecute": terraformExecuteMetadata(),
|
"terraformExecute": terraformExecuteMetadata(),
|
||||||
|
@ -183,6 +183,7 @@ func Execute() {
|
|||||||
rootCmd.AddCommand(ApiProxyUploadCommand())
|
rootCmd.AddCommand(ApiProxyUploadCommand())
|
||||||
rootCmd.AddCommand(GradleExecuteBuildCommand())
|
rootCmd.AddCommand(GradleExecuteBuildCommand())
|
||||||
rootCmd.AddCommand(ApiKeyValueMapUploadCommand())
|
rootCmd.AddCommand(ApiKeyValueMapUploadCommand())
|
||||||
|
rootCmd.AddCommand(PythonBuildCommand())
|
||||||
|
|
||||||
addRootFlags(rootCmd)
|
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
|
'gradleExecuteBuild', //implementing new golang pattern without fields
|
||||||
'shellExecute', //implementing new golang pattern without fields
|
'shellExecute', //implementing new golang pattern without fields
|
||||||
'apiKeyValueMapUpload', //implementing new golang pattern without fields
|
'apiKeyValueMapUpload', //implementing new golang pattern without fields
|
||||||
|
'pythonBuild', //implementing new golang pattern without fields
|
||||||
]
|
]
|
||||||
|
|
||||||
@Test
|
@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