diff --git a/cmd/metadata_generated.go b/cmd/metadata_generated.go index fd34bc07c..601131f26 100644 --- a/cmd/metadata_generated.go +++ b/cmd/metadata_generated.go @@ -88,6 +88,7 @@ func GetAllStepMetadata() map[string]config.StepData { "npmExecuteScripts": npmExecuteScriptsMetadata(), "pipelineCreateScanSummary": pipelineCreateScanSummaryMetadata(), "protecodeExecuteScan": protecodeExecuteScanMetadata(), + "pythonBuild": pythonBuildMetadata(), "shellExecute": shellExecuteMetadata(), "sonarExecuteScan": sonarExecuteScanMetadata(), "terraformExecute": terraformExecuteMetadata(), diff --git a/cmd/piper.go b/cmd/piper.go index b6425a9c5..0a7112ccb 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -183,6 +183,7 @@ func Execute() { rootCmd.AddCommand(ApiProxyUploadCommand()) rootCmd.AddCommand(GradleExecuteBuildCommand()) rootCmd.AddCommand(ApiKeyValueMapUploadCommand()) + rootCmd.AddCommand(PythonBuildCommand()) addRootFlags(rootCmd) diff --git a/cmd/pythonBuild.go b/cmd/pythonBuild.go new file mode 100644 index 000000000..cbfdf5d39 --- /dev/null +++ b/cmd/pythonBuild.go @@ -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 +} diff --git a/cmd/pythonBuild_generated.go b/cmd/pythonBuild_generated.go new file mode 100644 index 000000000..90ecbe842 --- /dev/null +++ b/cmd/pythonBuild_generated.go @@ -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 +} diff --git a/cmd/pythonBuild_generated_test.go b/cmd/pythonBuild_generated_test.go new file mode 100644 index 000000000..e17e54e72 --- /dev/null +++ b/cmd/pythonBuild_generated_test.go @@ -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") + +} diff --git a/cmd/pythonBuild_test.go b/cmd/pythonBuild_test.go new file mode 100644 index 000000000..6ecab4956 --- /dev/null +++ b/cmd/pythonBuild_test.go @@ -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") + }) +} diff --git a/integration/integration_python_build_test.go b/integration/integration_python_build_test.go new file mode 100644 index 000000000..2ed7a25b9 --- /dev/null +++ b/integration/integration_python_build_test.go @@ -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") +} diff --git a/integration/testdata/TestPythonIntegration/python-project/.pipeline/config.yml b/integration/testdata/TestPythonIntegration/python-project/.pipeline/config.yml new file mode 100644 index 000000000..8c01ed9a9 --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/.pipeline/config.yml @@ -0,0 +1,6 @@ +general: + +steps: + pythonBuild: + createBOM: true + publish: false diff --git a/integration/testdata/TestPythonIntegration/python-project/LICENSE b/integration/testdata/TestPythonIntegration/python-project/LICENSE new file mode 100644 index 000000000..96f1555df --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/LICENSE @@ -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. diff --git a/integration/testdata/TestPythonIntegration/python-project/README.md b/integration/testdata/TestPythonIntegration/python-project/README.md new file mode 100644 index 000000000..ad9ff9a35 --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/README.md @@ -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. diff --git a/integration/testdata/TestPythonIntegration/python-project/pyproject.toml b/integration/testdata/TestPythonIntegration/python-project/pyproject.toml new file mode 100644 index 000000000..374b58cbf --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/integration/testdata/TestPythonIntegration/python-project/setup.cfg b/integration/testdata/TestPythonIntegration/python-project/setup.cfg new file mode 100644 index 000000000..e3f61ee13 --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/setup.cfg @@ -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 diff --git a/integration/testdata/TestPythonIntegration/python-project/setup.py b/integration/testdata/TestPythonIntegration/python-project/setup.py new file mode 100644 index 000000000..a1c439fab --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/setup.py @@ -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', +) diff --git a/integration/testdata/TestPythonIntegration/python-project/src/example_package/__init__.py b/integration/testdata/TestPythonIntegration/python-project/src/example_package/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/integration/testdata/TestPythonIntegration/python-project/src/example_package/example.py b/integration/testdata/TestPythonIntegration/python-project/src/example_package/example.py new file mode 100644 index 000000000..c929f885b --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/src/example_package/example.py @@ -0,0 +1,2 @@ +def add_one(number): + return number + 1 diff --git a/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/PKG-INFO b/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/PKG-INFO new file mode 100644 index 000000000..f9e3667ff --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/PKG-INFO @@ -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 diff --git a/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/SOURCES.txt b/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/SOURCES.txt new file mode 100644 index 000000000..8b4c92011 --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/SOURCES.txt @@ -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 \ No newline at end of file diff --git a/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/dependency_links.txt b/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/dependency_links.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/top_level.txt b/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/top_level.txt new file mode 100644 index 000000000..7bb23628a --- /dev/null +++ b/integration/testdata/TestPythonIntegration/python-project/src/example_pkg.egg-info/top_level.txt @@ -0,0 +1 @@ +example_package diff --git a/resources/metadata/pythonBuild.yaml b/resources/metadata/pythonBuild.yaml new file mode 100644 index 000000000..18ef3f662 --- /dev/null +++ b/resources/metadata/pythonBuild.yaml @@ -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 diff --git a/test/groovy/CommonStepsTest.groovy b/test/groovy/CommonStepsTest.groovy index 30ae8b048..b57618688 100644 --- a/test/groovy/CommonStepsTest.groovy +++ b/test/groovy/CommonStepsTest.groovy @@ -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 diff --git a/vars/pythonBuild.groovy b/vars/pythonBuild.groovy new file mode 100644 index 000000000..eaa796e52 --- /dev/null +++ b/vars/pythonBuild.groovy @@ -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) +}