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 
			
		
		
		
	solman upload go the step (#2522)
Upload content into a transport request via SOLMAN Co-authored-by: Oliver Feldmann <oliver.feldmann@sap.com>
This commit is contained in:
		| @@ -68,6 +68,7 @@ func GetAllStepMetadata() map[string]config.StepData { | ||||
| 		"containerSaveImage":                      containerSaveImageMetadata(), | ||||
| 		"sonarExecuteScan":                        sonarExecuteScanMetadata(), | ||||
| 		"transportRequestUploadCTS":               transportRequestUploadCTSMetadata(), | ||||
| 		"transportRequestUploadSOLMAN":            transportRequestUploadSOLMANMetadata(), | ||||
| 		"uiVeri5ExecuteTests":                     uiVeri5ExecuteTestsMetadata(), | ||||
| 		"vaultRotateSecretId":                     vaultRotateSecretIdMetadata(), | ||||
| 		"artifactPrepareVersion":                  artifactPrepareVersionMetadata(), | ||||
|   | ||||
| @@ -128,6 +128,7 @@ func Execute() { | ||||
| 	rootCmd.AddCommand(CheckChangeInDevelopmentCommand()) | ||||
| 	rootCmd.AddCommand(TransportRequestUploadCTSCommand()) | ||||
| 	rootCmd.AddCommand(IntegrationArtifactDeployCommand()) | ||||
| 	rootCmd.AddCommand(TransportRequestUploadSOLMANCommand()) | ||||
| 	rootCmd.AddCommand(IntegrationArtifactUpdateConfigurationCommand()) | ||||
| 	rootCmd.AddCommand(IntegrationArtifactGetMplStatusCommand()) | ||||
| 	rootCmd.AddCommand(IntegrationArtifactDownloadCommand()) | ||||
|   | ||||
							
								
								
									
										83
									
								
								cmd/transportRequestUploadSOLMAN.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								cmd/transportRequestUploadSOLMAN.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"github.com/SAP/jenkins-library/pkg/command" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/piperutils" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/SAP/jenkins-library/pkg/transportrequest/solman" | ||||
| ) | ||||
|  | ||||
| type transportRequestUploadSOLMANUtils interface { | ||||
| 	command.ExecRunner | ||||
|  | ||||
| 	FileExists(filename string) (bool, error) | ||||
|  | ||||
| 	GetExitCode() int | ||||
|  | ||||
| 	// Add more methods here, or embed additional interfaces, or remove/replace as required. | ||||
| 	// The transportRequestUploadSOLMANUtils interface should be descriptive of your runtime dependencies, | ||||
| 	// i.e. include everything you need to be able to mock in tests. | ||||
| 	// Unit tests shall be executable in parallel (not depend on global state), and don't (re-)test dependencies. | ||||
| } | ||||
|  | ||||
| type transportRequestUploadSOLMANUtilsBundle struct { | ||||
| 	*command.Command | ||||
| 	*piperutils.Files | ||||
|  | ||||
| 	// Embed more structs as necessary to implement methods or interfaces you add to transportRequestUploadSOLMANUtils. | ||||
| 	// Structs embedded in this way must each have a unique set of methods attached. | ||||
| 	// If there is no struct which implements the method you need, attach the method to | ||||
| 	// transportRequestUploadSOLMANUtilsBundle and forward to the implementation of the dependency. | ||||
| } | ||||
|  | ||||
| func newTransportRequestUploadSOLMANUtils() transportRequestUploadSOLMANUtils { | ||||
| 	utils := transportRequestUploadSOLMANUtilsBundle{ | ||||
| 		Command: &command.Command{}, | ||||
| 		Files:   &piperutils.Files{}, | ||||
| 	} | ||||
| 	// Reroute command output to logging framework | ||||
| 	utils.Stdout(log.Writer()) | ||||
| 	utils.Stderr(log.Writer()) | ||||
| 	return &utils | ||||
| } | ||||
|  | ||||
| func transportRequestUploadSOLMAN(config transportRequestUploadSOLMANOptions, telemetryData *telemetry.CustomData) { | ||||
| 	// Utils can be used wherever the command.ExecRunner interface is expected. | ||||
| 	// It can also be used for example as a mavenExecRunner. | ||||
| 	utils := newTransportRequestUploadSOLMANUtils() | ||||
|  | ||||
| 	// For HTTP calls import  piperhttp "github.com/SAP/jenkins-library/pkg/http" | ||||
| 	// and use a  &piperhttp.Client{} in a custom system | ||||
| 	// Example: step checkmarxExecuteScan.go | ||||
|  | ||||
| 	// Error situations should be bubbled up until they reach the line below which will then stop execution | ||||
| 	// through the log.Entry().Fatal() call leading to an os.Exit(1) in the end. | ||||
| 	err := runTransportRequestUploadSOLMAN(&config, &solman.UploadAction{}, telemetryData, utils) | ||||
| 	if err != nil { | ||||
| 		log.Entry().WithError(err).Fatal("step execution failed") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func runTransportRequestUploadSOLMAN(config *transportRequestUploadSOLMANOptions, action solman.Action, telemetryData *telemetry.CustomData, utils transportRequestUploadSOLMANUtils) error { | ||||
| 	action.WithConnection(solman.Connection{ | ||||
| 		Endpoint: config.Endpoint, | ||||
| 		User:     config.Username, | ||||
| 		Password: config.Password, | ||||
| 	}) | ||||
| 	action.WithChangeDocumentID(config.ChangeDocumentID) | ||||
| 	action.WithTransportRequestID(config.TransportRequestID) | ||||
| 	action.WithApplicationID(config.ApplicationID) | ||||
| 	action.WithFile(config.FilePath) | ||||
| 	action.WithCMOpts(config.CmClientOpts) | ||||
| 	err := action.Perform(utils, utils) | ||||
|  | ||||
| 	if err == nil { | ||||
| 		log.Entry().Infof("Upload of artifact '%s' to SAP Solution Manager succeeded (ChangeDocumentId: '%s', TransportRequestId: '%s').", | ||||
| 			config.FilePath, | ||||
| 			config.ChangeDocumentID, | ||||
| 			config.TransportRequestID, | ||||
| 		) | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
							
								
								
									
										195
									
								
								cmd/transportRequestUploadSOLMAN_generated.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								cmd/transportRequestUploadSOLMAN_generated.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | ||||
| // Code generated by piper's step-generator. DO NOT EDIT. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/config" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| type transportRequestUploadSOLMANOptions struct { | ||||
| 	Endpoint           string   `json:"endpoint,omitempty"` | ||||
| 	Username           string   `json:"username,omitempty"` | ||||
| 	Password           string   `json:"password,omitempty"` | ||||
| 	ApplicationID      string   `json:"applicationId,omitempty"` | ||||
| 	ChangeDocumentID   string   `json:"changeDocumentId,omitempty"` | ||||
| 	TransportRequestID string   `json:"transportRequestId,omitempty"` | ||||
| 	FilePath           string   `json:"filePath,omitempty"` | ||||
| 	CmClientOpts       []string `json:"cmClientOpts,omitempty"` | ||||
| } | ||||
|  | ||||
| // TransportRequestUploadSOLMANCommand Uploads content to a transport request | ||||
| func TransportRequestUploadSOLMANCommand() *cobra.Command { | ||||
| 	const STEP_NAME = "transportRequestUploadSOLMAN" | ||||
|  | ||||
| 	metadata := transportRequestUploadSOLMANMetadata() | ||||
| 	var stepConfig transportRequestUploadSOLMANOptions | ||||
| 	var startTime time.Time | ||||
|  | ||||
| 	var createTransportRequestUploadSOLMANCmd = &cobra.Command{ | ||||
| 		Use:   STEP_NAME, | ||||
| 		Short: "Uploads content to a transport request", | ||||
| 		Long:  `Uploads content to a transport request which is associated with a change document in SAP Solution Manager`, | ||||
| 		PreRunE: func(cmd *cobra.Command, _ []string) error { | ||||
| 			startTime = time.Now() | ||||
| 			log.SetStepName(STEP_NAME) | ||||
| 			log.SetVerbose(GeneralConfig.Verbose) | ||||
|  | ||||
| 			path, _ := os.Getwd() | ||||
| 			fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path} | ||||
| 			log.RegisterHook(fatalHook) | ||||
|  | ||||
| 			err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile) | ||||
| 			if err != nil { | ||||
| 				log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 				return err | ||||
| 			} | ||||
| 			log.RegisterSecret(stepConfig.Username) | ||||
| 			log.RegisterSecret(stepConfig.Password) | ||||
|  | ||||
| 			if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { | ||||
| 				sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) | ||||
| 				log.RegisterHook(&sentryHook) | ||||
| 			} | ||||
|  | ||||
| 			return nil | ||||
| 		}, | ||||
| 		Run: func(_ *cobra.Command, _ []string) { | ||||
| 			telemetryData := telemetry.CustomData{} | ||||
| 			telemetryData.ErrorCode = "1" | ||||
| 			handler := func() { | ||||
| 				config.RemoveVaultSecretFiles() | ||||
| 				telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) | ||||
| 				telemetryData.ErrorCategory = log.GetErrorCategory().String() | ||||
| 				telemetry.Send(&telemetryData) | ||||
| 			} | ||||
| 			log.DeferExitHandler(handler) | ||||
| 			defer handler() | ||||
| 			telemetry.Initialize(GeneralConfig.NoTelemetry, STEP_NAME) | ||||
| 			transportRequestUploadSOLMAN(stepConfig, &telemetryData) | ||||
| 			telemetryData.ErrorCode = "0" | ||||
| 			log.Entry().Info("SUCCESS") | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	addTransportRequestUploadSOLMANFlags(createTransportRequestUploadSOLMANCmd, &stepConfig) | ||||
| 	return createTransportRequestUploadSOLMANCmd | ||||
| } | ||||
|  | ||||
| func addTransportRequestUploadSOLMANFlags(cmd *cobra.Command, stepConfig *transportRequestUploadSOLMANOptions) { | ||||
| 	cmd.Flags().StringVar(&stepConfig.Endpoint, "endpoint", os.Getenv("PIPER_endpoint"), "Service endpoint") | ||||
| 	cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "Operating system user for triggering the deployment") | ||||
| 	cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password for the deploy user") | ||||
| 	cmd.Flags().StringVar(&stepConfig.ApplicationID, "applicationId", os.Getenv("PIPER_applicationId"), "Id of the application.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.ChangeDocumentID, "changeDocumentId", os.Getenv("PIPER_changeDocumentId"), "Id of the change document to upload the file. This parameter is only taken into account when provided via signature to the step.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.TransportRequestID, "transportRequestId", os.Getenv("PIPER_transportRequestId"), "Id of the transport request to upload the file. This parameter is only taken into account when provided via signature to the step.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.FilePath, "filePath", os.Getenv("PIPER_filePath"), "Name/Path of the file which should be uploaded") | ||||
| 	cmd.Flags().StringSliceVar(&stepConfig.CmClientOpts, "cmClientOpts", []string{}, "Additional options handed over to the cm client") | ||||
|  | ||||
| 	cmd.MarkFlagRequired("endpoint") | ||||
| 	cmd.MarkFlagRequired("username") | ||||
| 	cmd.MarkFlagRequired("password") | ||||
| 	cmd.MarkFlagRequired("applicationId") | ||||
| 	cmd.MarkFlagRequired("changeDocumentId") | ||||
| 	cmd.MarkFlagRequired("transportRequestId") | ||||
| 	cmd.MarkFlagRequired("filePath") | ||||
| 	cmd.MarkFlagRequired("cmClientOpts") | ||||
| } | ||||
|  | ||||
| // retrieve step metadata | ||||
| func transportRequestUploadSOLMANMetadata() config.StepData { | ||||
| 	var theMetaData = config.StepData{ | ||||
| 		Metadata: config.StepMetadata{ | ||||
| 			Name:        "transportRequestUploadSOLMAN", | ||||
| 			Aliases:     []config.Alias{{Name: "transportRequestUploadFile", Deprecated: false}}, | ||||
| 			Description: "Uploads content to a transport request", | ||||
| 		}, | ||||
| 		Spec: config.StepSpec{ | ||||
| 			Inputs: config.StepInputs{ | ||||
| 				Parameters: []config.StepParameters{ | ||||
| 					{ | ||||
| 						Name:        "endpoint", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   true, | ||||
| 						Aliases:     []config.Alias{{Name: "changeManagement/endpoint"}}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "username", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   true, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "password", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   true, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "applicationId", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   true, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "changeDocumentId", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   true, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "transportRequestId", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "commonPipelineEnvironment", | ||||
| 								Param: "custom/transportRequestId", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"PARAMETERS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: true, | ||||
| 						Aliases:   []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "filePath", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "commonPipelineEnvironment", | ||||
| 								Param: "mtarFilePath", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: true, | ||||
| 						Aliases:   []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "cmClientOpts", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEP", "GENERAL"}, | ||||
| 						Type:        "[]string", | ||||
| 						Mandatory:   true, | ||||
| 						Aliases:     []config.Alias{{Name: "clientOpts"}, {Name: "changeManagement/clientOpts"}}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	return theMetaData | ||||
| } | ||||
							
								
								
									
										17
									
								
								cmd/transportRequestUploadSOLMAN_generated_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cmd/transportRequestUploadSOLMAN_generated_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestTransportRequestUploadSOLMANCommand(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	testCmd := TransportRequestUploadSOLMANCommand() | ||||
|  | ||||
| 	// only high level testing performed - details are tested in step generation procedure | ||||
| 	assert.Equal(t, "transportRequestUploadSOLMAN", testCmd.Use, "command name incorrect") | ||||
|  | ||||
| } | ||||
							
								
								
									
										103
									
								
								cmd/transportRequestUploadSOLMAN_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								cmd/transportRequestUploadSOLMAN_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/SAP/jenkins-library/pkg/mock" | ||||
| 	"github.com/SAP/jenkins-library/pkg/transportrequest/solman" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| type transportRequestUploadSOLMANMockUtils struct { | ||||
| 	*mock.ExecMockRunner | ||||
| 	*mock.FilesMock | ||||
| } | ||||
|  | ||||
| func newTransportRequestUploadSOLMANTestsUtils() transportRequestUploadSOLMANMockUtils { | ||||
| 	utils := transportRequestUploadSOLMANMockUtils{ | ||||
| 		ExecMockRunner: &mock.ExecMockRunner{}, | ||||
| 		FilesMock:      &mock.FilesMock{}, | ||||
| 	} | ||||
| 	return utils | ||||
| } | ||||
|  | ||||
| type ActionMock struct { | ||||
| 	received      solman.UploadAction | ||||
| 	performCalled bool | ||||
| 	failWith      error | ||||
| } | ||||
|  | ||||
| func (a *ActionMock) WithConnection(c solman.Connection) { | ||||
| 	a.received.Connection = c | ||||
| } | ||||
| func (a *ActionMock) WithChangeDocumentID(id string) { | ||||
| 	a.received.ChangeDocumentID = id | ||||
| } | ||||
| func (a *ActionMock) WithTransportRequestID(id string) { | ||||
| 	a.received.TransportRequestID = id | ||||
| } | ||||
| func (a *ActionMock) WithApplicationID(id string) { | ||||
| 	a.received.ApplicationID = id | ||||
| } | ||||
| func (a *ActionMock) WithFile(f string) { | ||||
| 	a.received.File = f | ||||
| } | ||||
| func (a *ActionMock) WithCMOpts(opts []string) { | ||||
| 	a.received.CMOpts = opts | ||||
| } | ||||
| func (a *ActionMock) Perform(fs solman.FileSystem, command solman.Exec) error { | ||||
| 	a.performCalled = true | ||||
| 	return a.failWith | ||||
| } | ||||
|  | ||||
| func TestRunTransportRequestUploadSOLMAN(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	t.Run("solmand upload", func(t *testing.T) { | ||||
| 		t.Parallel() | ||||
|  | ||||
| 		config := transportRequestUploadSOLMANOptions{ | ||||
| 			Endpoint:           "https://example.org/solman", | ||||
| 			Username:           "me", | ||||
| 			Password:           "********", | ||||
| 			ApplicationID:      "XYZ", | ||||
| 			ChangeDocumentID:   "12345678", | ||||
| 			TransportRequestID: "87654321", | ||||
| 			FilePath:           "myApp.xxx", | ||||
| 			CmClientOpts:       []string{"-Dtest=abc123"}, | ||||
| 		} | ||||
|  | ||||
| 		t.Run("straight forward", func(t *testing.T) { | ||||
| 			utils := newTransportRequestUploadSOLMANTestsUtils() | ||||
| 			action := ActionMock{} | ||||
|  | ||||
| 			err := runTransportRequestUploadSOLMAN(&config, &action, nil, utils) | ||||
|  | ||||
| 			if assert.NoError(t, err) { | ||||
| 				assert.Equal(t, action.received, solman.UploadAction{ | ||||
| 					Connection: solman.Connection{ | ||||
| 						Endpoint: "https://example.org/solman", | ||||
| 						User:     "me", | ||||
| 						Password: "********", | ||||
| 					}, | ||||
| 					ApplicationID:      "XYZ", | ||||
| 					ChangeDocumentID:   "12345678", | ||||
| 					TransportRequestID: "87654321", | ||||
| 					File:               "myApp.xxx", | ||||
| 					CMOpts:             []string{"-Dtest=abc123"}, | ||||
| 				}) | ||||
| 				assert.True(t, action.performCalled) | ||||
| 			} | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("Error during deployment", func(t *testing.T) { | ||||
| 			utils := newTransportRequestUploadSOLMANTestsUtils() | ||||
| 			action := ActionMock{failWith: fmt.Errorf("upload failed")} | ||||
|  | ||||
| 			err := runTransportRequestUploadSOLMAN(&config, &action, nil, utils) | ||||
|  | ||||
| 			assert.Error(t, err, "upload failed") | ||||
| 		}) | ||||
|  | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										19
									
								
								pkg/transportrequest/solman/common.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								pkg/transportrequest/solman/common.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| package solman | ||||
|  | ||||
| import ( | ||||
| 	"github.com/SAP/jenkins-library/pkg/command" | ||||
| ) | ||||
|  | ||||
| // Exec interface collecting everything which is execution related | ||||
| // and needed in the context of a SOLMAN upload. | ||||
| type Exec interface { | ||||
| 	command.ExecRunner | ||||
| 	GetExitCode() int | ||||
| } | ||||
|  | ||||
| // Connection Everything we need for connecting to CTS | ||||
| type Connection struct { | ||||
| 	Endpoint string | ||||
| 	User     string | ||||
| 	Password string | ||||
| } | ||||
							
								
								
									
										137
									
								
								pkg/transportrequest/solman/upload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								pkg/transportrequest/solman/upload.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| package solman | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/SAP/jenkins-library/pkg/config/validation" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // FileSystem interface collecting everything which is file system | ||||
| // related and needed in the context of a SOLMAN upload. | ||||
| type FileSystem interface { | ||||
| 	FileExists(path string) (bool, error) | ||||
| } | ||||
|  | ||||
| // UploadAction Collects all the properties we need for the deployment | ||||
| type UploadAction struct { | ||||
| 	Connection         Connection | ||||
| 	ChangeDocumentID   string | ||||
| 	TransportRequestID string | ||||
| 	ApplicationID      string | ||||
| 	File               string | ||||
| 	CMOpts             []string | ||||
| } | ||||
|  | ||||
| // Action collects everything which is needed to perform a SOLMAN upload | ||||
| type Action interface { | ||||
| 	WithConnection(Connection) | ||||
| 	WithChangeDocumentID(string) | ||||
| 	WithTransportRequestID(string) | ||||
| 	WithApplicationID(string) | ||||
| 	WithFile(string) | ||||
| 	WithCMOpts([]string) | ||||
| 	Perform(fs FileSystem, command Exec) error | ||||
| } | ||||
|  | ||||
| // WithConnection specifies all the connection details which | ||||
| // are required in order to connect to SOLMAN | ||||
| func (a *UploadAction) WithConnection(c Connection) { | ||||
| 	a.Connection = c | ||||
| } | ||||
|  | ||||
| // WithChangeDocumentID specifies the change document which | ||||
| // receives the executable. | ||||
| func (a *UploadAction) WithChangeDocumentID(id string) { | ||||
| 	a.ChangeDocumentID = id | ||||
| } | ||||
|  | ||||
| // WithTransportRequestID specifies the transport request which | ||||
| // receives the executable. | ||||
| func (a *UploadAction) WithTransportRequestID(id string) { | ||||
| 	a.TransportRequestID = id | ||||
| } | ||||
|  | ||||
| // WithApplicationID specifies the application ID. | ||||
| func (a *UploadAction) WithApplicationID(id string) { | ||||
| 	a.ApplicationID = id | ||||
| } | ||||
|  | ||||
| // WithFile specifies the executable which should be uploaded into | ||||
| // a transport on SOLMAN | ||||
| func (a *UploadAction) WithFile(f string) { | ||||
| 	a.File = f | ||||
| } | ||||
|  | ||||
| // WithCMOpts sets additional options for calling the | ||||
| // cm client tool. E.g. -D options. Useful for troubleshooting | ||||
| func (a *UploadAction) WithCMOpts(opts []string) { | ||||
| 	a.CMOpts = opts | ||||
| } | ||||
|  | ||||
| // Perform performs the SOLMAN upload | ||||
| func (a *UploadAction) Perform(fs FileSystem, command Exec) error { | ||||
|  | ||||
| 	log.Entry().Infof("Deploying artifact '%s' to '%s'.", | ||||
| 		a.File, a.Connection.Endpoint) | ||||
|  | ||||
| 	missingParameters, err := validation.FindEmptyStringsInConfigStruct(*a) | ||||
|  | ||||
| 	if err == nil { | ||||
| 		notInitialized := len(missingParameters) != 0 | ||||
| 		if notInitialized { | ||||
| 			err = fmt.Errorf("the following parameters are not available '%s'", missingParameters) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err == nil { | ||||
| 		var exists bool | ||||
| 		exists, err = fs.FileExists(a.File) | ||||
|  | ||||
| 		if err == nil && !exists { | ||||
| 			err = fmt.Errorf("file '%s' does not exist", a.File) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err == nil { | ||||
| 		if len(a.CMOpts) > 0 { | ||||
| 			command.SetEnv([]string{fmt.Sprintf("CMCLIENT_OPTS=%s", strings.Join(a.CMOpts, " "))}) | ||||
| 		} | ||||
|  | ||||
| 		err = command.RunExecutable("cmclient", | ||||
| 			"--endpoint", a.Connection.Endpoint, | ||||
| 			"--user", a.Connection.User, | ||||
| 			"--password", a.Connection.Password, | ||||
| 			"--backend-type", "SOLMAN", | ||||
| 			"upload-file-to-transport", | ||||
| 			"-cID", a.ChangeDocumentID, | ||||
| 			"-tID", a.TransportRequestID, | ||||
| 			a.ApplicationID, a.File) | ||||
|  | ||||
| 		exitCode := command.GetExitCode() | ||||
|  | ||||
| 		if exitCode != 0 { | ||||
| 			message := fmt.Sprintf("upload command returned with exit code '%d'", exitCode) | ||||
| 			if err != nil { | ||||
| 				// Using the wrapping here is to some extend an abuse, since it is not really | ||||
| 				// error chaining (the other error is not necessaryly a "predecessor" of this one). | ||||
| 				// But it is a pragmatic approach for not loosing information for trouble shooting. There | ||||
| 				// is no possibility to have something like suppressed errors. | ||||
| 				err = errors.Wrap(err, message) | ||||
| 			} else { | ||||
| 				err = errors.New(message) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err == nil { | ||||
| 		log.Entry().Infof("Deployment succeeded, artifact: '%s', endpoint: '%s'", | ||||
| 			a.File, a.Connection.Endpoint) | ||||
| 	} else { | ||||
| 		log.Entry().WithError(err).Warnf("Deployment failed, artifact: '%s', endpoint: '%s'", | ||||
| 			a.File, a.Connection.Endpoint) | ||||
| 	} | ||||
|  | ||||
| 	return errors.Wrapf(err, "cannot upload artifact '%s'", a.File) | ||||
| } | ||||
							
								
								
									
										108
									
								
								pkg/transportrequest/solman/upload_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								pkg/transportrequest/solman/upload_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| package solman | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/SAP/jenkins-library/pkg/mock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestSolmanUpload(t *testing.T) { | ||||
|  | ||||
| 	f := &mock.FilesMock{} | ||||
| 	f.AddFile("myDeployable.xxx", []byte("")) | ||||
|  | ||||
| 	defaultUploadAction := UploadAction{} | ||||
| 	defaultUploadAction.WithConnection( | ||||
| 		Connection{ | ||||
| 			Endpoint: "https://example.org/solman", | ||||
| 			User:     "me", | ||||
| 			Password: "******", | ||||
| 		}) | ||||
| 	defaultUploadAction.WithChangeDocumentID("123456") | ||||
| 	defaultUploadAction.WithTransportRequestID("000K11111111") | ||||
| 	defaultUploadAction.WithApplicationID("MY_APP") | ||||
| 	defaultUploadAction.WithFile("myDeployable.xxx") | ||||
| 	defaultUploadAction.WithCMOpts([]string{"-Dmyprop1=abc", "-Dmyprop2=def"}) | ||||
|  | ||||
| 	t.Run("Deployable does not exist", func(t *testing.T) { | ||||
|  | ||||
| 		uploadActionFileMissing := defaultUploadAction | ||||
| 		uploadActionFileMissing.WithFile("myMissingDeployable.xxx") | ||||
| 		e := &mock.ExecMockRunner{} | ||||
|  | ||||
| 		err := uploadActionFileMissing.Perform(f, e) | ||||
|  | ||||
| 		assert.EqualError(t, err, "cannot upload artifact 'myMissingDeployable.xxx': file 'myMissingDeployable.xxx' does not exist") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Straight forward", func(t *testing.T) { | ||||
|  | ||||
| 		e := &mock.ExecMockRunner{} | ||||
|  | ||||
| 		err := defaultUploadAction.Perform(f, e) | ||||
|  | ||||
| 		if assert.NoError(t, err) { | ||||
| 			assert.Len(t, e.Calls, 1) | ||||
| 			assert.Equal(t, mock.ExecCall{ | ||||
| 				Exec: "cmclient", | ||||
| 				Params: []string{ | ||||
| 					"--endpoint", "https://example.org/solman", | ||||
| 					"--user", "me", | ||||
| 					"--password", "******", | ||||
| 					"--backend-type", "SOLMAN", | ||||
| 					"upload-file-to-transport", | ||||
| 					"-cID", "123456", | ||||
| 					"-tID", "000K11111111", | ||||
| 					"MY_APP", | ||||
| 					"myDeployable.xxx", | ||||
| 				}, | ||||
| 			}, e.Calls[0]) | ||||
| 			assert.Equal(t, []string{"CMCLIENT_OPTS=-Dmyprop1=abc -Dmyprop2=def"}, e.Env) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Missing parameters", func(t *testing.T) { | ||||
| 		e := &mock.ExecMockRunner{} | ||||
| 		uploadAction := defaultUploadAction | ||||
| 		uploadAction.WithConnection( | ||||
| 			Connection{ | ||||
| 				Endpoint: "", | ||||
| 				User:     "me", | ||||
| 				Password: "******", | ||||
| 			}, | ||||
| 		) | ||||
| 		uploadAction.WithTransportRequestID("") | ||||
| 		err := uploadAction.Perform(f, e) | ||||
| 		if assert.Error(t, err) { | ||||
| 			// we should not rely on the order of the missing parameters | ||||
| 			assert.Contains(t, err.Error(), "cannot upload artifact 'myDeployable.xxx': the following parameters are not available") | ||||
| 			assert.Contains(t, err.Error(), "Connection.Endpoint") | ||||
| 			assert.Contains(t, err.Error(), "TransportRequestID") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Deploy command returns with return code not equal zero", func(t *testing.T) { | ||||
|  | ||||
| 		e := &mock.ExecMockRunner{} | ||||
| 		e.ExitCode = 1 | ||||
|  | ||||
| 		err := defaultUploadAction.Perform(f, e) | ||||
|  | ||||
| 		assert.EqualError(t, err, "cannot upload artifact 'myDeployable.xxx': upload command returned with exit code '1'") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Deploy command cannot be executed", func(t *testing.T) { | ||||
|  | ||||
| 		e := &mock.ExecMockRunner{ | ||||
| 			ShouldFailOnCommand: map[string]error{ | ||||
| 				"cmclient.*": fmt.Errorf("cannot execute upload command"), | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		err := defaultUploadAction.Perform(f, e) | ||||
|  | ||||
| 		assert.EqualError(t, err, "cannot upload artifact 'myDeployable.xxx': cannot execute upload command") | ||||
| 	}) | ||||
|  | ||||
| } | ||||
							
								
								
									
										92
									
								
								resources/metadata/transportRequestUploadSOLMAN.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								resources/metadata/transportRequestUploadSOLMAN.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| metadata: | ||||
|   name: transportRequestUploadSOLMAN | ||||
|   aliases: | ||||
|     - name: transportRequestUploadFile | ||||
|   description: "Uploads content to a transport request" | ||||
|   longDescription: | | ||||
|     Uploads content to a transport request which is associated with a change document in SAP Solution Manager | ||||
| spec: | ||||
|   inputs: | ||||
|     secrets: | ||||
|       - name: uploadCredentialsId | ||||
|         description: Jenkins 'Username with password' credentials ID containing user and password to authenticate against the ABAP backend. | ||||
|         type: jenkins | ||||
|         aliases: | ||||
|           - name: changeManagement/credentialsId | ||||
|     params: | ||||
|       - name: endpoint | ||||
|         type: string | ||||
|         mandatory: true | ||||
|         description: "Service endpoint" | ||||
|         aliases: | ||||
|           - name: changeManagement/endpoint | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|           - GENERAL | ||||
|       - name: username | ||||
|         type: string | ||||
|         mandatory: true | ||||
|         description: "Operating system user for triggering the deployment" | ||||
|         secret: true | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|           - GENERAL | ||||
|       - name: password | ||||
|         type: string | ||||
|         mandatory: true | ||||
|         description: "Password for the deploy user" | ||||
|         secret: true | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|       - name: applicationId | ||||
|         type: string | ||||
|         mandatory: true | ||||
|         description: "Id of the application." | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|           - GENERAL | ||||
|       - name: changeDocumentId | ||||
|         type: string | ||||
|         mandatory: true | ||||
|         description: "Id of the change document to upload the file. This parameter is only taken into account when provided via signature to the step." | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|       - name: transportRequestId | ||||
|         resourceRef: | ||||
|           - name: commonPipelineEnvironment | ||||
|             param: custom/transportRequestId | ||||
|         type: string | ||||
|         mandatory: true | ||||
|         description: "Id of the transport request to upload the file. This parameter is only taken into account when provided via signature to the step." | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|       - name: filePath | ||||
|         resourceRef: | ||||
|           - name: commonPipelineEnvironment | ||||
|             param: mtarFilePath | ||||
|         type: string | ||||
|         mandatory: true | ||||
|         description: "Name/Path of the file which should be uploaded" | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|           - GENERAL | ||||
|       - name: cmClientOpts | ||||
|         aliases: | ||||
|           - name: clientOpts | ||||
|           - name: changeManagement/clientOpts | ||||
|         type: "[]string" | ||||
|         mandatory: true | ||||
|         description: "Additional options handed over to the cm client" | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEP | ||||
|           - GENERAL | ||||
		Reference in New Issue
	
	Block a user