1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-02-21 19:48:53 +02:00

Fix(gctsDeploy) Refactoring gctsDeploy step (#2789)

* Changed gcts deploy step to include create, clone and switching branches

* Added create only flag for repository

* Changed the logic of Rollback

* Added more logs to deployCommitToAbapSystem

* Changes to deploy to abap system

* Changes to deploy to abap system

* Changes to condition for pullbycommit

* Added Current commit deploy scope handling

* Changed VCS_NO_IMPORT to take abap bool

* Added delete config functionality

* Functionality to parse boolean configuration

* Fix to get config metadata url

* Added additional error messages for switch branch

* Better error dump handling

* Better error dump handling contd

* Added dump errors to all http requests

* Error logging changes

* More Unit Tests

* Added more logs

* Updated docs for gCTS deploy

* Added scope in documentation

* Removal of some nested loops, fix of unit tests

* Documentation changes and more comments in the code

Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com>
This commit is contained in:
Sarat Krishnan 2021-05-10 13:05:21 +01:00 committed by GitHub
parent c43afe1355
commit 77557c41c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1588 additions and 142 deletions

View File

@ -1,18 +1,32 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"github.com/Jeffail/gabs/v2"
"github.com/SAP/jenkins-library/pkg/command"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/pkg/errors"
)
const repoStateExists = "RepoExists"
const repoStateNew = "RepoNew"
func gctsDeploy(config gctsDeployOptions, telemetryData *telemetry.CustomData) {
// for command execution use Command
c := command.Command{}
// reroute command output to logging framework
c.Stdout(log.Writer())
c.Stderr(log.Writer())
// for http calls import piperhttp "github.com/SAP/jenkins-library/pkg/http"
// and use a &piperhttp.Client{} in a custom system
@ -20,13 +34,434 @@ func gctsDeploy(config gctsDeployOptions, telemetryData *telemetry.CustomData) {
httpClient := &piperhttp.Client{}
// error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end
err := deployCommit(&config, nil, httpClient)
err := gctsDeployRepository(&config, telemetryData, &c, httpClient)
if err != nil {
log.Entry().WithError(err).Fatal("step execution failed")
}
}
func deployCommit(config *gctsDeployOptions, telemetryData *telemetry.CustomData, httpClient piperhttp.Sender) error {
func gctsDeployRepository(config *gctsDeployOptions, telemetryData *telemetry.CustomData, command command.ExecRunner, httpClient piperhttp.Sender) error {
cookieJar, cookieErr := cookiejar.New(nil)
repoState := repoStateExists
branchRollbackRequired := false
if cookieErr != nil {
return errors.Wrap(cookieErr, "creating a cookie jar failed")
}
clientOptions := piperhttp.ClientOptions{
CookieJar: cookieJar,
Username: config.Username,
Password: config.Password,
}
httpClient.SetOptions(clientOptions)
log.Entry().Infof("Start of gCTS Deploy Step with Configuration Values: %v", config)
configurationMetadata, getConfigMetadataErr := getConfigurationMetadata(config, httpClient)
if getConfigMetadataErr != nil {
log.Entry().WithError(getConfigMetadataErr).Error("step execution failed at configuration metadata retrieval. Please Check if system is up!.")
return getConfigMetadataErr
}
createRepoOptions := gctsCreateRepositoryOptions{
Username: config.Username,
Password: config.Password,
Repository: config.Repository,
Host: config.Host,
Client: config.Client,
RemoteRepositoryURL: config.RemoteRepositoryURL,
Role: config.Role,
VSID: config.VSID,
Type: config.Type,
}
log.Entry().Infof("gCTS Deploy : Checking if repository %v already exists", config.Repository)
repoMetadataInitState, getRepositoryErr := getRepository(config, httpClient)
currentBranch := repoMetadataInitState.Result.Branch
// If Repository does not exist in the system then Create and Clone Repository
if getRepositoryErr != nil {
// If scope is set for a new repository then creation/cloning of the repository cannot be done
if config.Scope != "" {
log.Entry().Error("Error during deploy : deploy scope cannot be provided while deploying a new repo")
return errors.New("Error in config file")
}
// State of the repository set for further processing during the step
repoState = repoStateNew
log.Entry().Infof("gCTS Deploy : Creating Repository Step for repository : %v", config.Repository)
// Parse the configuration parameter to a format that is accepted by the gcts api
configurations, _ := splitConfigurationToMap(config.Configuration, *configurationMetadata)
createErr := createRepositoryForDeploy(&createRepoOptions, telemetryData, command, httpClient, configurations)
if createErr != nil {
//Dump error log (Log it)
log.Entry().WithError(createErr).Error("step execution failed at Create Repository")
return createErr
}
cloneRepoOptions := gctsCloneRepositoryOptions{
Username: config.Username,
Password: config.Password,
Repository: config.Repository,
Host: config.Host,
Client: config.Client,
}
// No Import has to be set when there is a commit or branch parameter set
// This is required so that during the clone of the repo it is not imported into the system
// The import would be done at a later stage with the help of gcts deploy api call
if config.Branch != "" || config.Commit != "" {
setNoImportAndCloneRepoErr := setNoImportAndCloneRepo(config, &cloneRepoOptions, httpClient, telemetryData)
if setNoImportAndCloneRepoErr != nil {
log.Entry().WithError(setNoImportAndCloneRepoErr).Error("step execution failed")
return setNoImportAndCloneRepoErr
}
} else {
// Clone Repository and Exit the step since there is no commit or branch parameters provided
cloneErr := cloneRepository(&cloneRepoOptions, telemetryData, httpClient)
if cloneErr != nil {
// Dump Error Log
log.Entry().WithError(cloneErr).Error("step execution failed at Clone Repository")
return cloneErr
}
log.Entry().Infof("gCTS Deploy : Step has completed for the repository %v : ", config.Repository)
// End of the step.
return nil
}
log.Entry().Infof("gCTS Deploy : Reading repo information after cloning repository %v : ", config.Repository)
// Get the repository information for further processing of the step.
repoMetadataInitState, getRepositoryErr = getRepository(config, httpClient)
if getRepositoryErr != nil {
// Dump Error Log
log.Entry().WithError(getRepositoryErr).Error("step execution failed at get repository after clone")
return getRepositoryErr
}
currentBranch = repoMetadataInitState.Result.Branch
} else {
log.Entry().Infof("Repository %v already exists in the system, Checking for deploy scope", config.Repository)
// If deploy scope provided for an existing repository then deploy api is called and then execution ends
if config.Scope != "" {
log.Entry().Infof("Deploy scope exists for the repository in the configuration file")
log.Entry().Infof("gCTS Deploy: Deploying Commit to ABAP System for Repository %v with scope %v", config.Repository, config.Scope)
deployErr := deployCommitToAbapSystem(config, httpClient)
if deployErr != nil {
log.Entry().WithError(deployErr).Error("step execution failed at Deploying Commit to ABAP system.")
return deployErr
}
return nil
}
log.Entry().Infof("Deploy scope not set in the configuration file for repository : %v", config.Repository)
}
// branch to which the switching has to be done to
targetBranch := config.Branch
if config.Branch != "" {
// switch to a target branch, and if it fails rollback to the previous working state
_, switchBranchWithRollbackErr := switchBranchWithRollback(config, httpClient, currentBranch, targetBranch, repoState, repoMetadataInitState)
if switchBranchWithRollbackErr != nil {
return switchBranchWithRollbackErr
}
currentBranch = config.Branch
branchRollbackRequired = true
}
if config.Commit != "" {
// switch to a target commit and if it fails rollback to the previous working state
pullByCommitWithRollbackErr := pullByCommitWithRollback(config, telemetryData, command, httpClient, repoState, repoMetadataInitState, currentBranch, targetBranch, branchRollbackRequired)
if pullByCommitWithRollbackErr != nil {
return pullByCommitWithRollbackErr
}
} else {
// if commit parameter is not provided and its a new repo , then set config scope to "Current Commit" to be used with deploy api
if repoState == repoStateNew && (config.Commit != "" || config.Branch != "") {
log.Entry().Infof("Setting deploy scope as current commit")
config.Scope = "CRNTCOMMIT"
}
if config.Scope != "" {
removeNoImportAndDeployToSystemErr := removeNoImportAndDeployToSystem(config, httpClient)
if removeNoImportAndDeployToSystemErr != nil {
return removeNoImportAndDeployToSystemErr
}
// Step Execution Ends here
return nil
}
pullByCommitWithRollbackErr := pullByCommitWithRollback(config, telemetryData, command, httpClient, repoState, repoMetadataInitState, currentBranch, targetBranch, branchRollbackRequired)
if pullByCommitWithRollbackErr != nil {
return pullByCommitWithRollbackErr
}
}
// A deploy is done with scope current commit if the repository is a new repo and
// branch and a commit parameters where also provided
// This is required so that the code base is imported into the system because during the
// switch branch and pull by commit the no import flag was set as true
if repoState == repoStateNew {
log.Entry().Infof("Setting deploy scope as current commit")
config.Scope = "CRNTCOMMIT"
removeNoImportAndDeployToSystemErr := removeNoImportAndDeployToSystem(config, httpClient)
if removeNoImportAndDeployToSystemErr != nil {
return removeNoImportAndDeployToSystemErr
}
}
return nil
}
// Function to remove the VCS_NO_IMPORT flag and do deploy into the abap system
func removeNoImportAndDeployToSystem(config *gctsDeployOptions, httpClient piperhttp.Sender) error {
log.Entry().Infof("Removing VCS_NO_IMPORT configuration")
configToDelete := "VCS_NO_IMPORT"
deleteConfigKeyErr := deleteConfigKey(config, httpClient, configToDelete)
if deleteConfigKeyErr != nil {
log.Entry().WithError(deleteConfigKeyErr).Error("step execution failed at Set Config key for VCS_NO_IMPORT")
return deleteConfigKeyErr
}
// Get deploy scope and gctsDeploy
log.Entry().Infof("gCTS Deploy: Deploying Commit to ABAP System for Repository %v with scope %v", config.Repository, config.Scope)
deployErr := deployCommitToAbapSystem(config, httpClient)
if deployErr != nil {
log.Entry().WithError(deployErr).Error("step execution failed at Deploying Commit to ABAP system.")
return deployErr
}
return nil
}
// Function to pull by commit, it also does a rollback incase of errors during the pull
func pullByCommitWithRollback(config *gctsDeployOptions, telemetryData *telemetry.CustomData, command command.ExecRunner,
httpClient piperhttp.Sender, repoState string, repoMetadataInitState *getRepositoryResponseBody,
currentBranch string, targetBranch string, branchRollbackRequired bool) error {
log.Entry().Infof("gCTS Deploy: Pull by Commit step execution to commit %v", config.Commit)
pullByCommitErr := pullByCommit(config, telemetryData, command, httpClient)
if pullByCommitErr != nil {
log.Entry().WithError(pullByCommitErr).Error("step execution failed at Pull By Commit. Trying to rollback to last commit")
if config.Rollback {
//Rollback to last commit.
rollbackOptions := gctsRollbackOptions{
Username: config.Username,
Password: config.Password,
Repository: config.Repository,
Host: config.Host,
Client: config.Client,
}
rollbackErr := rollback(&rollbackOptions, telemetryData, command, httpClient)
if rollbackErr != nil {
log.Entry().WithError(rollbackErr).Error("step execution failed while rolling back commit")
return rollbackErr
}
if repoState == repoStateNew && branchRollbackRequired {
// Rollback branch
// Rollback branch. Resetting branches
targetBranch = repoMetadataInitState.Result.Branch
currentBranch = config.Branch
log.Entry().Errorf("Rolling Back from %v to %v", currentBranch, targetBranch)
switchBranch(config, httpClient, currentBranch, targetBranch)
}
}
return pullByCommitErr
}
return nil
}
// Function to switch branches, it also does a rollback incase of errors during the switch
func switchBranchWithRollback(config *gctsDeployOptions, httpClient piperhttp.Sender, currentBranch string, targetBranch string, repoState string, repoMetadataInitState *getRepositoryResponseBody) (*switchBranchResponseBody, error) {
var response *switchBranchResponseBody
response, switchBranchErr := switchBranch(config, httpClient, currentBranch, targetBranch)
if switchBranchErr != nil {
log.Entry().WithError(switchBranchErr).Error("step execution failed at Switch Branch")
if repoState == repoStateNew && config.Rollback {
// Rollback branch. Resetting branches
targetBranch = repoMetadataInitState.Result.Branch
currentBranch = config.Branch
log.Entry().WithError(switchBranchErr).Errorf("Rolling Back from %v to %v", currentBranch, targetBranch)
switchBranch(config, httpClient, currentBranch, targetBranch)
}
return nil, switchBranchErr
}
return response, nil
}
// Set VCS_NO_IMPORT flag to true and do a clone of the repo. This disables the repository objects to be pulled in to the system
func setNoImportAndCloneRepo(config *gctsDeployOptions, cloneRepoOptions *gctsCloneRepositoryOptions, httpClient piperhttp.Sender, telemetryData *telemetry.CustomData) error {
log.Entry().Infof("Setting VCS_NO_IMPORT to true")
noImportConfig := setConfigKeyBody{
Key: "VCS_NO_IMPORT",
Value: "X",
}
setConfigKeyErr := setConfigKey(config, httpClient, &noImportConfig)
if setConfigKeyErr != nil {
log.Entry().WithError(setConfigKeyErr).Error("step execution failed at Set Config key for VCS_NO_IMPORT")
return setConfigKeyErr
}
cloneErr := cloneRepository(cloneRepoOptions, telemetryData, httpClient)
if cloneErr != nil {
log.Entry().WithError(cloneErr).Error("step execution failed at Clone Repository")
return cloneErr
}
return nil
}
// Function to switch branch
func switchBranch(config *gctsDeployOptions, httpClient piperhttp.Sender, currentBranch string, targetBranch string) (*switchBranchResponseBody, error) {
var response switchBranchResponseBody
log.Entry().Infof("gCTS Deploy : Switching branch for repository : %v, from branch: %v to %v", config.Repository, currentBranch, targetBranch)
requestURL := config.Host +
"/sap/bc/cts_abapvcs/repository/" + config.Repository + "/branches/" + currentBranch +
"/switch?branch=" + targetBranch + "&sap-client=" + config.Client
resp, httpErr := httpClient.SendRequest("GET", requestURL, nil, nil, nil)
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
if httpErr != nil {
_, errorDumpParseErr := parseErrorDumpFromResponseBody(resp)
if errorDumpParseErr != nil {
return nil, errorDumpParseErr
}
return &response, httpErr
} else if resp == nil {
return &response, errors.New("did not retrieve a HTTP response")
}
parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &response)
if parsingErr != nil {
return &response, parsingErr
}
log.Entry().Infof("Switched branches from %v to %v. The commits where switched from %v to %v", currentBranch, config.Branch, response.Result.FromCommit, response.Result.ToCommit)
return &response, nil
}
func deployCommitToAbapSystem(config *gctsDeployOptions, httpClient piperhttp.Sender) error {
var response getRepositoryResponseBody
deployRequestBody := deployCommitToAbapSystemBody{
Scope: config.Scope,
}
log.Entry().Info("gCTS Deploy : Start of deploying commit to ABAP System.")
requestURL := config.Host +
"/sap/bc/cts_abapvcs/repository/" + config.Repository +
"/deploy?sap-client=" + config.Client
reqBody := deployRequestBody
jsonBody, marshalErr := json.Marshal(reqBody)
if marshalErr != nil {
return errors.Wrapf(marshalErr, "Deploying repository to abap system failed json body marshalling")
}
header := make(http.Header)
header.Set("Content-Type", "application/json")
header.Add("Accept", "application/json")
resp, httpErr := httpClient.SendRequest("POST", requestURL, bytes.NewBuffer(jsonBody), header, nil)
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
if httpErr != nil {
_, errorDumpParseErr := parseErrorDumpFromResponseBody(resp)
if errorDumpParseErr != nil {
return errorDumpParseErr
}
log.Entry().Error("Failed During Deploy to Abap system")
return httpErr
}
parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &response)
if parsingErr != nil {
return parsingErr
}
log.Entry().Infof("Response for deploy command : %v", response.Result)
return nil
}
// Uses the repository details to check if the repository already exists in the system or not
func getRepository(config *gctsDeployOptions, httpClient piperhttp.Sender) (*getRepositoryResponseBody, error) {
var response getRepositoryResponseBody
requestURL := config.Host +
"/sap/bc/cts_abapvcs/repository/" + config.Repository +
"?sap-client=" + config.Client
resp, httpErr := httpClient.SendRequest("GET", requestURL, nil, nil, nil)
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
if httpErr != nil {
_, errorDumpParseErr := parseErrorDumpFromResponseBody(resp)
if errorDumpParseErr != nil {
return nil, errorDumpParseErr
}
log.Entry().Infof("Error while repository Check : %v", httpErr)
return &response, httpErr
} else if resp == nil {
return &response, errors.New("did not retrieve a HTTP response")
}
parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &response)
if parsingErr != nil {
return &response, parsingErr
}
return &response, nil
}
// Function to delete configuration key for repositories
func deleteConfigKey(deployConfig *gctsDeployOptions, httpClient piperhttp.Sender, configToDelete string) error {
log.Entry().Infof("gCTS Deploy : Delete configuration key %v", configToDelete)
requestURL := deployConfig.Host +
"/sap/bc/cts_abapvcs/repository/" + deployConfig.Repository +
"/config/" + configToDelete + "?sap-client=" + deployConfig.Client
header := make(http.Header)
header.Set("Content-Type", "application/json")
header.Add("Accept", "application/json")
resp, httpErr := httpClient.SendRequest("DELETE", requestURL, nil, header, nil)
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
if httpErr != nil {
_, errorDumpParseErr := parseErrorDumpFromResponseBody(resp)
if errorDumpParseErr != nil {
return errorDumpParseErr
}
log.Entry().Error("Failure during deletion of configuration value")
return httpErr
}
log.Entry().Infof("gCTS Deploy : Delete configuration key %v successful", configToDelete)
return nil
}
// Function to set configuration key for repositories
func setConfigKey(deployConfig *gctsDeployOptions, httpClient piperhttp.Sender, configToSet *setConfigKeyBody) error {
log.Entry().Infof("gCTS Deploy : Start of set configuration key %v and value %v", configToSet.Key, configToSet.Value)
requestURL := deployConfig.Host +
"/sap/bc/cts_abapvcs/repository/" + deployConfig.Repository +
"/config?sap-client=" + deployConfig.Client
reqBody := configToSet
jsonBody, marshalErr := json.Marshal(reqBody)
if marshalErr != nil {
return errors.Wrapf(marshalErr, "Setting config key: %v and value: %v on the ABAP system %v failed", configToSet.Key, configToSet.Value, deployConfig.Host)
}
header := make(http.Header)
header.Set("Content-Type", "application/json")
header.Add("Accept", "application/json")
resp, httpErr := httpClient.SendRequest("POST", requestURL, bytes.NewBuffer(jsonBody), header, nil)
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
if httpErr != nil {
_, errorDumpParseErr := parseErrorDumpFromResponseBody(resp)
if errorDumpParseErr != nil {
return errorDumpParseErr
}
log.Entry().Error("Failure during setting configuration value")
return httpErr
}
log.Entry().
WithField("repository", deployConfig.Repository).
Infof("successfully set configuration value key %v and value %v", configToSet.Key, configToSet.Value)
return nil
}
func pullByCommit(config *gctsDeployOptions, telemetryData *telemetry.CustomData, command command.ExecRunner, httpClient piperhttp.Sender) error {
cookieJar, cookieErr := cookiejar.New(nil)
if cookieErr != nil {
@ -41,7 +476,7 @@ func deployCommit(config *gctsDeployOptions, telemetryData *telemetry.CustomData
requestURL := config.Host +
"/sap/bc/cts_abapvcs/repository/" + config.Repository +
"/pullByCommit?sap-client=" + config.Client
"/pullByCommit?sap-client=" + config.Client + "&request=" + config.Commit
if config.Commit != "" {
log.Entry().Infof("preparing to deploy specified commit %v", config.Commit)
@ -59,6 +494,10 @@ func deployCommit(config *gctsDeployOptions, telemetryData *telemetry.CustomData
}()
if httpErr != nil {
_, errorDumpParseErr := parseErrorDumpFromResponseBody(resp)
if errorDumpParseErr != nil {
return errorDumpParseErr
}
return httpErr
} else if resp == nil {
return errors.New("did not retrieve a HTTP response")
@ -81,3 +520,287 @@ func deployCommit(config *gctsDeployOptions, telemetryData *telemetry.CustomData
Infof("successfully deployed commit %v (previous commit was %v)", response.Path("toCommit").Data().(string), response.Path("fromCommit").Data().(string))
return nil
}
func createRepositoryForDeploy(config *gctsCreateRepositoryOptions, telemetryData *telemetry.CustomData, command command.ExecRunner, httpClient piperhttp.Sender, repositoryConfig []repositoryConfiguration) error {
cookieJar, cookieErr := cookiejar.New(nil)
if cookieErr != nil {
return errors.Wrapf(cookieErr, "creating repository on the ABAP system %v failed", config.Host)
}
clientOptions := piperhttp.ClientOptions{
CookieJar: cookieJar,
Username: config.Username,
Password: config.Password,
}
httpClient.SetOptions(clientOptions)
type repoData struct {
RID string `json:"rid"`
Name string `json:"name"`
Role string `json:"role"`
Type string `json:"type"`
VSID string `json:"vsid"`
RemoteRepositoryURL string `json:"url"`
Config []repositoryConfiguration `json:"config"`
}
type createRequestBody struct {
Repository string `json:"repository"`
Data repoData `json:"data"`
}
reqBody := createRequestBody{
Repository: config.Repository,
Data: repoData{
RID: config.Repository,
Name: config.Repository,
Role: config.Role,
Type: config.Type,
VSID: config.VSID,
RemoteRepositoryURL: config.RemoteRepositoryURL,
Config: repositoryConfig,
},
}
jsonBody, marshalErr := json.Marshal(reqBody)
if marshalErr != nil {
return errors.Wrapf(marshalErr, "creating repository on the ABAP system %v failed", config.Host)
}
header := make(http.Header)
header.Set("Content-Type", "application/json")
header.Add("Accept", "application/json")
url := config.Host + "/sap/bc/cts_abapvcs/repository?sap-client=" + config.Client
resp, httpErr := httpClient.SendRequest("POST", url, bytes.NewBuffer(jsonBody), header, nil)
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
if resp == nil {
return errors.Errorf("creating repository on the ABAP system %v failed: %v", config.Host, httpErr)
}
if httpErr != nil {
response, errorDumpParseErr := parseErrorDumpFromResponseBody(resp)
if errorDumpParseErr != nil {
return errorDumpParseErr
}
if resp.StatusCode == 500 {
if response.Exception == "Repository already exists" {
log.Entry().
WithField("repository", config.Repository).
Infof("the repository already exists on the ABAP system %v", config.Host)
return nil
}
}
log.Entry().Errorf("a HTTP error occurred! Response body: %v", response)
return errors.Wrapf(httpErr, "creating repository on the ABAP system %v failed", config.Host)
}
log.Entry().
WithField("repository", config.Repository).
Infof("successfully created the repository on ABAP system %v", config.Host)
return nil
}
func getConfigurationMetadata(config *gctsDeployOptions, httpClient piperhttp.Sender) (*configurationMetadataBody, error) {
var response configurationMetadataBody
log.Entry().Infof("Starting to retrieve configuration metadata from the system")
requestURL := config.Host +
"/sap/bc/cts_abapvcs/config"
resp, httpErr := httpClient.SendRequest("GET", requestURL, nil, nil, nil)
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
if httpErr != nil {
_, errorDumpParseErr := parseErrorDumpFromResponseBody(resp)
if errorDumpParseErr != nil {
return nil, errorDumpParseErr
}
log.Entry().Infof("Error while repository Check : %v", httpErr)
return &response, httpErr
} else if resp == nil {
return &response, errors.New("did not retrieve a HTTP response")
}
parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &response)
if parsingErr != nil {
return &response, parsingErr
}
log.Entry().Infof("System Available for further step processing. The configuration metadata was successfully retrieved.")
return &response, nil
}
func splitConfigurationToMap(inputConfigMap map[string]interface{}, configMetadataInSystem configurationMetadataBody) ([]repositoryConfiguration, error) {
log.Entry().Infof("Parsing the configurations from the yml file")
var configurations []repositoryConfiguration
for key, value := range inputConfigMap {
foundConfigMetadata, _ := findConfigurationMetadata(key, configMetadataInSystem)
configValue := fmt.Sprint(value)
if (configMetadata{}) != foundConfigMetadata {
if foundConfigMetadata.Datatype == "BOOLEAN" && foundConfigMetadata.Example == "X" {
if configValue == "false" || configValue == "" {
configValue = ""
} else if configValue == "true" || configValue == "X" {
configValue = "X"
}
}
}
configuration := repositoryConfiguration{
Key: key,
Value: configValue,
}
configurations = append(configurations, configuration)
}
log.Entry().Infof("The Configurations for the repoistory creation are : %v", configurations)
return configurations, nil
}
func findConfigurationMetadata(configToFind string, configurationsAvailable configurationMetadataBody) (configMetadata, error) {
var configStruct configMetadata
for _, config := range configurationsAvailable.Config {
if config.Ckey == configToFind {
return config, nil
}
}
return configStruct, nil
}
// Error handling for failure responses from the gcts api calls
func parseErrorDumpFromResponseBody(responseBody *http.Response) (*errorLogBody, error) {
var errorDump errorLogBody
parsingErr := piperhttp.ParseHTTPResponseBodyJSON(responseBody, &errorDump)
if parsingErr != nil {
return &errorDump, parsingErr
}
for _, errorLogData := range errorDump.ErrorLog {
log.Entry().Errorf("Time: %v, User: %v, Section: %v, Action: %v, Severity: %v, Message: %v",
errorLogData.Time, errorLogData.User, errorLogData.Section,
errorLogData.Action, errorLogData.Severity, errorLogData.Message)
for _, protocolErrorData := range errorLogData.Protocol {
log.Entry().Errorf("Type: %v", protocolErrorData.Type)
for _, protocols := range protocolErrorData.Protocol {
if strings.Contains(protocols, "4 ETW000 ") {
protocols = strings.ReplaceAll(protocols, "4 ETW000 ", "")
} else if strings.Contains(protocols, "4EETW000 ") {
protocols = strings.ReplaceAll(protocols, "4EETW000 ", "ERROR: ")
}
log.Entry().Error(protocols)
}
}
}
return &errorDump, nil
}
type repositoryConfiguration struct {
Key string `json:"key"`
Value string `json:"value"`
}
type getRepositoryResponseBody struct {
Result struct {
Rid string `json:"rid"`
Name string `json:"name"`
Role string `json:"role"`
Vsid string `json:"vsid"`
Status string `json:"status"`
Branch string `json:"branch"`
Url string `json:"url"`
Config []struct {
Key string `json:"key"`
Value string `json:"value"`
Category string `json:"category"`
} `json:"config"`
Objects int64 `json:"objects"`
CurrentCommit string `json:"currentCommit"`
Connection string `json:"connection"`
} `json:"result"`
}
type setConfigKeyBody struct {
Key string `json:"key"`
Value string `json:"value"`
}
type switchBranchResponseBody struct {
Result struct {
FromCommit string `json:"fromCommit"`
ToCommit string `json:"ToCommit"`
} `json:"result"`
Log []struct {
Time string `json:"time"`
User string `json:"user"`
Section string `json:"section"`
Action string `json:"Action"`
Severity string `json:"Severity"`
Message string `json:"Message"`
} `json:"log"`
}
type deployCommitToAbapSystemBody struct {
Repository string `json:"repository"`
Scope string `json:"scope"`
Commit string `json:"commit"`
Objects []struct {
Object string `json:"object"`
Type string `json:"type"`
User string `json:"user"`
Pgmid string `json:"pgmid"`
Keys []struct {
Tabname string `json:"tabname"`
Columns []struct {
Key string `json:"key"`
Field string `json:"field"`
Value string `json:"value"`
Type string `json:"type"`
Inttype string `json:"inttype"`
Length string `json:"length"`
}
}
} `json:"objects"`
}
type configMetadata struct {
Ckey string `json:"ckey"`
Ctype string `json:"ctype"`
Cvisible string `json:"cvisible"`
Datatype string `json:"datatype"`
DefaultValue string `json:"defaultValue"`
Description string `json:"description"`
Category string `json:"category"`
UiElement string `json:"uiElement"`
Example string `json:"example"`
}
type configurationMetadataBody struct {
Config []configMetadata `json:"config"`
}
type errorProtocolbody struct {
Type string `json:"type"`
Protocol []string `json:"protocol"`
}
type errorLog struct {
Time int `json:"time"`
User string `json:"user"`
Section string `json:"section"`
Action string `json:"action"`
Severity string `json:"severity"`
Message string `json:"message"`
Protocol []errorProtocolbody `json:"protocol"`
}
type errorLogBody struct {
ErrorLog []errorLog `json:"errorLog"`
Exception string `json:"exception"`
}

View File

@ -14,15 +14,23 @@ import (
)
type gctsDeployOptions struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Repository string `json:"repository,omitempty"`
Host string `json:"host,omitempty"`
Client string `json:"client,omitempty"`
Commit string `json:"commit,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Repository string `json:"repository,omitempty"`
Host string `json:"host,omitempty"`
Client string `json:"client,omitempty"`
Commit string `json:"commit,omitempty"`
RemoteRepositoryURL string `json:"remoteRepositoryURL,omitempty"`
Role string `json:"role,omitempty"`
VSID string `json:"vSID,omitempty"`
Type string `json:"type,omitempty"`
Branch string `json:"branch,omitempty"`
Scope string `json:"scope,omitempty"`
Rollback bool `json:"rollback,omitempty"`
Configuration map[string]interface{} `json:"configuration,omitempty"`
}
// GctsDeployCommand Pulls a commit from the remote Git repository to a local repository
// GctsDeployCommand Deploys a Git Repository to a local Repository and then to an ABAP System
func GctsDeployCommand() *cobra.Command {
const STEP_NAME = "gctsDeploy"
@ -32,8 +40,9 @@ func GctsDeployCommand() *cobra.Command {
var createGctsDeployCmd = &cobra.Command{
Use: STEP_NAME,
Short: "Pulls a commit from the remote Git repository to a local repository",
Long: `Pulls a commit from the corresponding remote Git repository to a specified local repository on an ABAP system. If no <commit> parameter is specified, this step will pull the latest commit available on the remote repository.`,
Short: "Deploys a Git Repository to a local Repository and then to an ABAP System",
Long: `The steps deploys a git repository to an ABAP System. If a repository does not exists in the system, it creates and clones it to the local
repository and then deploys it into the ABAP system.`,
PreRunE: func(cmd *cobra.Command, _ []string) error {
startTime = time.Now()
log.SetStepName(STEP_NAME)
@ -87,12 +96,20 @@ func addGctsDeployFlags(cmd *cobra.Command, stepConfig *gctsDeployOptions) {
cmd.Flags().StringVar(&stepConfig.Host, "host", os.Getenv("PIPER_host"), "Specifies the protocol and host address, including the port. Please provide in the format `<protocol>://<host>:<port>`. Supported protocols are `http` and `https`.")
cmd.Flags().StringVar(&stepConfig.Client, "client", os.Getenv("PIPER_client"), "Specifies the client of the ABAP system to be addressed")
cmd.Flags().StringVar(&stepConfig.Commit, "commit", os.Getenv("PIPER_commit"), "Specifies the commit to be deployed")
cmd.Flags().StringVar(&stepConfig.RemoteRepositoryURL, "remoteRepositoryURL", os.Getenv("PIPER_remoteRepositoryURL"), "URL of the corresponding remote repository")
cmd.Flags().StringVar(&stepConfig.Role, "role", os.Getenv("PIPER_role"), "Role of the local repository. Choose between 'TARGET' and 'SOURCE'. Local repositories with a TARGET role will NOT be able to be the source of code changes")
cmd.Flags().StringVar(&stepConfig.VSID, "vSID", os.Getenv("PIPER_vSID"), "Virtual SID of the local repository. The vSID corresponds to the transport route that delivers content to the remote Git repository")
cmd.Flags().StringVar(&stepConfig.Type, "type", `GIT`, "Type of the used source code management tool")
cmd.Flags().StringVar(&stepConfig.Branch, "branch", os.Getenv("PIPER_branch"), "Name of the branch to which the deploy has to be done to.")
cmd.Flags().StringVar(&stepConfig.Scope, "scope", os.Getenv("PIPER_scope"), "The scope of the gcts deploy api call")
cmd.Flags().BoolVar(&stepConfig.Rollback, "rollback", false, "The rollback flag for a failure during the deploy step. A true value would mean gCTS would roll back to the last clean state")
cmd.MarkFlagRequired("username")
cmd.MarkFlagRequired("password")
cmd.MarkFlagRequired("repository")
cmd.MarkFlagRequired("host")
cmd.MarkFlagRequired("client")
cmd.MarkFlagRequired("remoteRepositoryURL")
}
// retrieve step metadata
@ -101,7 +118,7 @@ func gctsDeployMetadata() config.StepData {
Metadata: config.StepMetadata{
Name: "gctsDeploy",
Aliases: []config.Alias{},
Description: "Pulls a commit from the remote Git repository to a local repository",
Description: "Deploys a Git Repository to a local Repository and then to an ABAP System",
},
Spec: config.StepSpec{
Inputs: config.StepInputs{
@ -166,6 +183,70 @@ func gctsDeployMetadata() config.StepData {
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "remoteRepositoryURL",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
Aliases: []config.Alias{},
},
{
Name: "role",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "vSID",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "type",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "branch",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "scope",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "rollback",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "bool",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "configuration",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"},
Type: "map[string]interface{}",
Mandatory: false,
Aliases: []config.Alias{{Name: "gctsRepositoryConfigurations"}},
},
},
},
},

View File

@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGctsDeploySuccess(t *testing.T) {
func TestGctsPullByCommitSuccess(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
@ -35,12 +35,12 @@ func TestGctsDeploySuccess(t *testing.T) {
]
}`}
err := deployCommit(&config, nil, &httpClient)
err := pullByCommit(&config, nil, nil, &httpClient)
if assert.NoError(t, err) {
t.Run("check url", func(t *testing.T) {
assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/pullByCommit?sap-client=000", httpClient.URL)
assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/pullByCommit?sap-client=000&request=", httpClient.URL)
})
t.Run("check method", func(t *testing.T) {
@ -60,7 +60,7 @@ func TestGctsDeploySuccess(t *testing.T) {
})
}
func TestGctsDeployFailure(t *testing.T) {
func TestGctsPullByCommitFailure(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
@ -73,39 +73,617 @@ func TestGctsDeployFailure(t *testing.T) {
t.Run("http error occurred", func(t *testing.T) {
httpClient := httpMockGcts{StatusCode: 500, ResponseBody: `{
"log": [
{
"time": 20180606130524,
"user": "JENKINS",
"section": "REPOSITORY_FACTORY",
"action": "CREATE_REPOSITORY",
"severity": "INFO",
"message": "Start action CREATE_REPOSITORY review",
"code": "GCTS.API.410"
}
],
"errorLog": [
{
"time": 20180606130524,
"user": "JENKINS",
"section": "REPOSITORY_FACTORY",
"action": "CREATE_REPOSITORY",
"severity": "INFO",
"message": "Start action CREATE_REPOSITORY review",
"code": "GCTS.API.410"
}
],
"exception": {
"message": "repository_not_found",
"description": "Repository not found",
"code": 404
}
}`}
"exception": "No relation between system and repository"
}`}
err := deployCommit(&config, nil, &httpClient)
err := pullByCommit(&config, nil, nil, &httpClient)
assert.EqualError(t, err, "a http error occurred")
})
}
func TestGctsGetRepositorySuccess(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepo",
Username: "testUser",
Password: "testPassword",
}
t.Run("Get Repository Success Test", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepo" {
httpClient = httpMockGcts{StatusCode: 200, ResponseBody: `{
"result": {
"rid": "testrepo",
"name": "testRepo",
"role": "SOURCE",
"type": "GIT",
"vsid": "GIT",
"status": "READY",
"branch": "dummy_branch",
"url": "https://example.git.com/testRepo",
"createdBy": "testUser",
"createdDate": "dummy_date",
"config": [
{
"key": "CURRENT_COMMIT",
"value": "dummy_commit_number",
"category": "GENERAL",
"scope": "local"
}
],
"objects": 1,
"currentCommit": "dummy_commit_number",
"connection": "ssl"
}
}`}
}
repository, err := getRepository(&config, &httpClient)
if assert.NoError(t, err) {
t.Run("check url", func(t *testing.T) {
assert.Equal(t, "https://example.git.com/testRepo", repository.Result.Url)
})
t.Run("check rid", func(t *testing.T) {
assert.Equal(t, "testrepo", repository.Result.Rid)
})
t.Run("check commit id", func(t *testing.T) {
assert.Equal(t, "dummy_commit_number", repository.Result.CurrentCommit)
})
}
})
}
func TestGctsGetRepositoryFailure(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepoNotExists",
Username: "testUser",
Password: "testPassword",
}
t.Run("Get Repository Success Test", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepoNotExists" {
httpClient = httpMockGcts{StatusCode: 500, ResponseBody: `{
"exception": "No relation between system and repository"
}`}
}
_, err := getRepository(&config, &httpClient)
assert.EqualError(t, err, "a http error occurred")
})
}
func TestGctsSwitchBranchSuccess(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepo",
Branch: "dummyBranch",
Username: "testUser",
Password: "testPassword",
}
t.Run("Switch Branch success", func(t *testing.T) {
var httpClient httpMockGcts
if config.Branch == "dummyBranch" {
httpClient = httpMockGcts{StatusCode: 200, ResponseBody: `{
"result": {
"rid": "testrepo",
"checkoutTime": 20210413082242,
"fromCommit": "from_dummy_commit",
"toCommit": "to_dummy_commit",
"caller": "testUser",
"request": "GITKUKDUMMY",
"type": "BRANCH_SW",
"state": "DONE",
"rc": "0000"
}
}`}
}
responseBody, err := switchBranch(&config, &httpClient, "dummyCurrentBranch", "dummyTargetBranch")
if assert.NoError(t, err) {
t.Run("check from commit", func(t *testing.T) {
assert.Equal(t, "from_dummy_commit", responseBody.Result.FromCommit)
})
t.Run("check to commit", func(t *testing.T) {
assert.Equal(t, "to_dummy_commit", responseBody.Result.ToCommit)
})
}
})
}
func TestGctsSwitchBranchFailure(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepo",
Branch: "dummyBranchNotExists",
Username: "testUser",
Password: "testPassword",
}
t.Run("Switch Branch failure Test", func(t *testing.T) {
var httpClient httpMockGcts
if config.Branch == "dummyBranchNotExists" {
httpClient = httpMockGcts{StatusCode: 500, ResponseBody: `{
"errorLog": [
{
"time": 20210414102742,
"severity": "ERROR",
"message": "The branch to switch to - 'feature1' - does not exist",
"code": "GCTS.CLIENT.1320"
}
],
"log": [
{
"time": 20210414102742,
"user": "testUser",
"section": "REPOSITORY",
"action": "SWITCH_BRANCH",
"severity": "ERROR",
"message": "20210414102742: Error action SWITCH_BRANCH 20210414_102740_B4EC329722B5C611B35B345F3B5F8FAA"
},
{
"time": 20210414102742,
"user": "testUser",
"section": "REPOSITORY",
"action": "SWITCH_BRANCH",
"severity": "ERROR",
"message": "20210414102742: Error action SWITCH_BRANCH Client error"
}
],
"exception": "Cannot switch branch of local repository to selected branch."
}`}
}
_, err := getRepository(&config, &httpClient)
assert.EqualError(t, err, "a http error occurred")
})
}
func TestCreateRepositorySuccess(t *testing.T) {
config := gctsCreateRepositoryOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepo",
Username: "testUser",
Password: "testPassword",
RemoteRepositoryURL: "http://testRepoUrl.com",
Role: "dummyRole",
VSID: "dummyVsid",
Type: "dummyType",
}
t.Run("Create Repository Success", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepo" {
httpClient = httpMockGcts{StatusCode: 200, ResponseBody: `{
"repository": {
"rid": "testrepo",
"name": "testRepo",
"role": "dummyRole",
"type": "dummyType",
"vsid": "dummyVsid",
"status": "CREATED",
"branch": "dummyBranch",
"url": "http://testRepoUrl.com",
"createdBy": "testUser",
"createdDate": "2021-04-14",
"config": [
{
"key": "CLIENT_VCS_CONNTYPE",
"value": "ssl",
"category": "CONNECTION",
"scope": "local"
},
{
"key": "CLIENT_VCS_URI",
"value": "http://testRepoUrl.com",
"category": "CONNECTION",
"scope": "local"
}
],
"connection": "ssl"
}
}`}
}
err := createRepositoryForDeploy(&config, nil, nil, &httpClient, nil)
assert.NoError(t, err)
})
}
func TestCreateRepositoryFailure(t *testing.T) {
config := gctsCreateRepositoryOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepoExists",
Username: "testUser",
Password: "testPassword",
RemoteRepositoryURL: "http://testRepoUrlFail.com",
Role: "dummyRole",
VSID: "dummyVsid",
Type: "dummyType",
}
t.Run("Create Repository Failure", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepoExists" {
httpClient = httpMockGcts{StatusCode: 500, ResponseBody: `{
"errorLog": [
{
"time": 20210506153611,
"user": "testUser",
"section": "SYSTEM",
"action": "CREATE_REPOSITORY",
"severity": "ERROR",
"message": "20210506153611: Error action CREATE_REPOSITORY Repository already exists"
}
],
"log": [
{
"time": 20210506153611,
"user": "testUser",
"section": "SYSTEM",
"action": "CREATE_REPOSITORY",
"severity": "ERROR",
"message": "20210506153611: Error action CREATE_REPOSITORY Repository already exists"
}
],
"exception": "Some Error"
}`}
}
err := createRepositoryForDeploy(&config, nil, nil, &httpClient, nil)
assert.EqualError(t, err, "creating repository on the ABAP system http://testHost.com:50000 failed: a http error occurred")
})
t.Run("Create Repository Failure", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepoExists" {
httpClient = httpMockGcts{StatusCode: 500, ResponseBody: `{
"errorLog": [
{
"time": 20210506153611,
"user": "testUser",
"section": "SYSTEM",
"action": "CREATE_REPOSITORY",
"severity": "ERROR",
"message": "20210506153611: Error action CREATE_REPOSITORY Repository already exists"
}
],
"log": [
{
"time": 20210506153611,
"user": "testUser",
"section": "SYSTEM",
"action": "CREATE_REPOSITORY",
"severity": "ERROR",
"message": "20210506153611: Error action CREATE_REPOSITORY Repository already exists"
}
],
"exception": "Repository already exists"
}`}
}
err := createRepositoryForDeploy(&config, nil, nil, &httpClient, nil)
assert.NoError(t, err)
})
}
func TestGctsSetConfigByKeySuccess(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepo",
Branch: "dummyBranch",
Username: "testUser",
Password: "testPassword",
}
configKey := setConfigKeyBody{
Key: "dummy_key",
Value: "dummy_value",
}
t.Run("Set Config By key Success", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepo" {
httpClient = httpMockGcts{StatusCode: 200, ResponseBody: `{
"result": {
"key": "dummy_key",
"value": "dummy_value"
}
}`}
}
err := setConfigKey(&config, &httpClient, &configKey)
assert.NoError(t, err)
})
}
func TestGctsSetConfigByKeyFailure(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepoNotExists",
Branch: "dummyBranchNotExists",
Username: "testUser",
Password: "testPassword",
}
configKey := setConfigKeyBody{
Key: "dummy_key",
Value: "dummy_value",
}
t.Run("Set Config By key Success", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepoNotExists" {
httpClient = httpMockGcts{StatusCode: 500, ResponseBody: `{
"exception": "No relation between system and repository"
}`}
}
err := setConfigKey(&config, &httpClient, &configKey)
assert.EqualError(t, err, "a http error occurred")
})
}
func TestGctsDeleteConfigByKeySuccess(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepo",
Branch: "dummyBranch",
Username: "testUser",
Password: "testPassword",
}
t.Run("Delete Config By key Success", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepo" {
httpClient = httpMockGcts{StatusCode: 200, ResponseBody: `{
}`}
}
err := deleteConfigKey(&config, &httpClient, "dummy_config")
assert.NoError(t, err)
})
}
func TestGctsDeleteConfigByKeyFailure(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepoNotExists",
Branch: "dummyBranchNotExists",
Username: "testUser",
Password: "testPassword",
}
t.Run("Delete Config By key Failure", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepoNotExists" {
httpClient = httpMockGcts{StatusCode: 500, ResponseBody: `{
"exception": "No relation between system and repository"
}`}
}
err := deleteConfigKey(&config, &httpClient, "dummy_config")
assert.EqualError(t, err, "a http error occurred")
})
}
func TestGctsConfigMetadataSuccess(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepo",
Branch: "dummyBranch",
Username: "testUser",
Password: "testPassword",
}
t.Run("Test Config Metadata Success", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepo" {
httpClient = httpMockGcts{StatusCode: 200, ResponseBody: `{
"config": [
{
"ckey": "dummy_key_system",
"ctype": "SYSTEM",
"cvisible": "X",
"datatype": "STRING",
"defaultValue": "dummy_default_system",
"description": "Dummy Key System",
"category": "SYSTEM",
"example": "dummy"
},
{
"ckey": "dummy_key_repo",
"ctype": "REPOSITORY",
"cvisible": "X",
"datatype": "STRING",
"defaultValue": "dummy_default",
"description": "Dummy Key repository",
"category": "INTERNAL",
"example": "dummy"
}
]
}`}
}
configMetadata, err := getConfigurationMetadata(&config, &httpClient)
if assert.NoError(t, err) {
t.Run("Check if system config matches", func(t *testing.T) {
for _, config := range configMetadata.Config {
if config.Ctype == "SYSTEM" {
assert.Equal(t, "dummy_key_system", config.Ckey)
} else if config.Ctype == "REPOSITORY" {
assert.Equal(t, "dummy_key_repo", config.Ckey)
}
}
})
}
})
}
func TestGctsConfigMetadataFailure(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHostNotregistered.com:50000",
Client: "000",
Repository: "testRepo",
Branch: "dummyBranch",
Username: "testUser",
Password: "testPassword",
}
t.Run("Test Config Metadata Failure", func(t *testing.T) {
var httpClient httpMockGcts
if config.Host == "http://testHostNotregistered.com:50000" {
httpClient = httpMockGcts{StatusCode: 500, ResponseBody: `{
}`}
}
_, err := getConfigurationMetadata(&config, &httpClient)
assert.EqualError(t, err, "a http error occurred")
})
}
func TestDeployToAbapSystemSuccess(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepo",
Username: "testUser",
Password: "testPassword",
Scope: "dummyScope",
}
t.Run("Deploy to ABAP system sucess", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepo" {
httpClient = httpMockGcts{StatusCode: 200, ResponseBody: `{
"result": {
"rid": "testrepo",
"name": "testRepo",
"role": "dummyRole",
"type": "dummyType",
"vsid": "dummyVsid",
"status": "CREATED",
"branch": "dummyBranch",
"url": "http://testRepoUrl.com",
"createdBy": "testUser",
"createdDate": "2021-04-14",
"config": [
{
"key": "CLIENT_VCS_CONNTYPE",
"value": "ssl",
"category": "CONNECTION",
"scope": "local"
},
{
"key": "CLIENT_VCS_URI",
"value": "http://testRepoUrl.com",
"category": "CONNECTION",
"scope": "local"
}
],
"connection": "ssl"
}
}`}
}
err := deployCommitToAbapSystem(&config, &httpClient)
assert.NoError(t, err)
})
}
func TestGctsDeployToAbapSystemFailure(t *testing.T) {
config := gctsDeployOptions{
Host: "http://testHost.com:50000",
Client: "000",
Repository: "testRepoNotExists",
Username: "testUser",
Password: "testPassword",
Scope: "dummyScope",
}
t.Run("Deploy to ABAP system Failure", func(t *testing.T) {
var httpClient httpMockGcts
if config.Repository == "testRepoNotExists" {
httpClient = httpMockGcts{StatusCode: 500, ResponseBody: `{
"exception": "No relation between system and repository"
}`}
}
err := deployCommitToAbapSystem(&config, &httpClient)
assert.EqualError(t, err, "a http error occurred")
})
}
func TestGctsSplitConfigurationToMap(t *testing.T) {
config := []configMetadata{
{
Ckey: "dummyKey1",
Ctype: "REPOSITORY",
Datatype: "BOOLEAN",
Example: "X",
},
{
Ckey: "dummyKey2",
Ctype: "REPOSITORY",
Datatype: "BOOLEAN",
Example: "true",
},
{
Ckey: "dummyKey3",
Ctype: "REPOSITORY",
Datatype: "STRING",
Example: "dummyValue",
},
}
configMetadata := configurationMetadataBody{
Config: config,
}
configMap := map[string]interface{}{
"dummyKey1": "true",
"dummyKey2": "true",
"dummyKey3": "dummyValue2",
}
t.Run("Config Mapping test", func(t *testing.T) {
repoConfig, err := splitConfigurationToMap(configMap, configMetadata)
if assert.NoError(t, err) {
for _, config := range repoConfig {
if config.Key == "dummyKey1" {
assert.Equal(t, "X", config.Value)
} else if config.Key == "dummyKey2" {
assert.Equal(t, "true", config.Value)
} else if config.Key == "dummyKey3" {
assert.Equal(t, "dummyValue2", config.Value)
}
}
}
})
}

View File

@ -54,7 +54,6 @@ func rollback(config *gctsRollbackOptions, telemetryData *telemetry.CustomData,
return errors.Errorf("no remote repository URL configured")
}
parsedURL, err := url.Parse(repoInfo.Result.URL)
if err != nil {
return errors.Wrap(err, "could not parse remote repository URL as valid URL")
}
@ -73,28 +72,6 @@ func rollback(config *gctsRollbackOptions, telemetryData *telemetry.CustomData,
Commit: config.Commit,
}
} else if parsedURL.Host == "github.com" {
log.Entry().Info("Remote repository domain is 'github.com'. Trying to rollback to last commit with status 'success'.")
commitList, err := getCommits(config, telemetryData, httpClient)
if err != nil {
return errors.Wrap(err, "could not get repository commits")
}
successCommit, err := getLastSuccessfullCommit(config, telemetryData, httpClient, parsedURL, commitList)
if err != nil {
return errors.Wrap(err, "could not determine successful commit")
}
deployOptions = gctsDeployOptions{
Username: config.Username,
Password: config.Password,
Host: config.Host,
Repository: config.Repository,
Client: config.Client,
Commit: successCommit,
}
} else {
repoHistory, err := getRepoHistory(config, telemetryData, httpClient)
if err != nil {
@ -117,7 +94,7 @@ func rollback(config *gctsRollbackOptions, telemetryData *telemetry.CustomData,
}
}
deployErr := deployCommit(&deployOptions, telemetryData, httpClient)
deployErr := pullByCommit(&deployOptions, telemetryData, command, httpClient)
if deployErr != nil {
return errors.Wrap(deployErr, "rollback commit failed")

View File

@ -4,7 +4,9 @@
## Prerequisites
With this step you can deploy a commit from a remote Git repository to a local repository on an ABAP server. If no `commit` parameter is specified, this step will pull the latest commit available on the remote repository.
With this step you can deploy a remote Git repository to a local repository on an ABAP server. If a `commit` parameter is specified the step would pull the
repository to the commit that was mentioned. If a `branch` is provided then the repository would be switched to the respective branch specified. The rollback flag can be used to decide if you want a rollback to the working state of the reposiotry
in the case of a failure.
Learn more about the SAP Git-enabled Change & Transport System (gCTS) [here](https://help.sap.com/viewer/4a368c163b08418890a406d413933ba7/201909.001/en-US/f319b168e87e42149e25e13c08d002b9.html). With gCTS, ABAP developments on ABAP servers can be maintained in Git repositories.
## ${docGenParameters}
@ -23,7 +25,15 @@ gctsDeploy(
host: 'https://abap.server.com:port',
client: '000',
abapCredentialsId: 'ABAPUserPasswordCredentialsId',
repository: 'myrepo'
repository: 'myrepo',
remoteRepositoryURL: "https://remote.repository.url.com",
role: 'SOURCE',
vSID: 'ABC',
branch: 'branch',
commit: 'commit',
scope: 'scope',
rollback: false,
configuration: [dummyConfig: 'dummyval']
)
```
@ -37,4 +47,13 @@ steps:
client: '000'
abapCredentialsId: 'ABAPUserPasswordCredentialsId'
repository: 'myrepo'
remoteRepositoryURL: "https://remote.repository.url.com"
role: 'SOURCE'
vSID: 'ABC'
branch: 'branch'
commit: 'commit'
scope: 'scope'
rollback: false
configuration:
dummyconfig: "dummyval"
```

View File

@ -0,0 +1,138 @@
metadata:
name: gctsDeploy
description: Deploys a Git Repository to a local Repository and then to an ABAP System
longDescription: |
The steps deploys a git repository to an ABAP System. If a repository does not exists in the system, it creates and clones it to the local
repository and then deploys it into the ABAP system.
spec:
inputs:
secrets:
- name: abapCredentialsId
description: Jenkins credentials ID containing username and password for authentication to the ABAP system on which you want to deploy a commit
type: jenkins
params:
- name: username
type: string
description: User to authenticate to the ABAP system
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
secret: true
resourceRef:
- name: abapCredentialsId
type: secret
param: username
- name: password
type: string
description: Password to authenticate to the ABAP system
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
secret: true
resourceRef:
- name: abapCredentialsId
type: secret
param: password
- name: repository
type: string
description: Specifies the name (ID) of the local repsitory on the ABAP system
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: host
type: string
description: Specifies the protocol and host address, including the port. Please provide in the format `<protocol>://<host>:<port>`. Supported protocols are `http` and `https`.
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: client
type: string
description: Specifies the client of the ABAP system to be addressed
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: commit
type: string
description: Specifies the commit to be deployed
scope:
- PARAMETERS
- STAGES
- STEPS
- name: remoteRepositoryURL
type: string
description: URL of the corresponding remote repository
mandatory: true
scope:
- PARAMETERS
- STAGES
- STEPS
- name: role
type: string
description: Role of the local repository. Choose between 'TARGET' and 'SOURCE'. Local repositories with a TARGET role will NOT be able to be the source of code changes
scope:
- PARAMETERS
- STAGES
- STEPS
possibleValues:
- SOURCE
- TARGET
- name: vSID
type: string
description: Virtual SID of the local repository. The vSID corresponds to the transport route that delivers content to the remote Git repository
scope:
- PARAMETERS
- STAGES
- STEPS
- name: type
type: string
description: Type of the used source code management tool
scope:
- PARAMETERS
- STAGES
- STEPS
default: GIT
possibleValues:
- GIT
- name: branch
type: string
description: Name of the branch to which the deploy has to be done to.
scope:
- PARAMETERS
- STAGES
- STEPS
- name: scope
type: string
description: The scope of the gcts deploy api call
scope:
- PARAMETERS
- STAGES
- STEPS
- name: rollback
type: bool
description: The rollback flag for a failure during the deploy step. A true value would mean gCTS would roll back to the last clean state
scope:
- PARAMETERS
- STAGES
- STEPS
- name: configuration
type: "map[string]interface{}"
description: "Defines the configuration values for the repository. This map needs to be created as `configKeyName`:`configKeyValue`"
scope:
- PARAMETERS
- STAGES
- STEPS
- GENERAL
mandatory: false
aliases:
- name: gctsRepositoryConfigurations

View File

@ -1,70 +0,0 @@
metadata:
name: gctsDeploy
description: Pulls a commit from the remote Git repository to a local repository
longDescription: |
Pulls a commit from the corresponding remote Git repository to a specified local repository on an ABAP system. If no <commit> parameter is specified, this step will pull the latest commit available on the remote repository.
spec:
inputs:
secrets:
- name: abapCredentialsId
description: Jenkins credentials ID containing username and password for authentication to the ABAP system on which you want to deploy a commit
type: jenkins
params:
- name: username
type: string
description: User to authenticate to the ABAP system
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
secret: true
resourceRef:
- name: abapCredentialsId
type: secret
param: username
- name: password
type: string
description: Password to authenticate to the ABAP system
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
secret: true
resourceRef:
- name: abapCredentialsId
type: secret
param: password
- name: repository
type: string
description: Specifies the name (ID) of the local repsitory on the ABAP system
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: host
type: string
description: Specifies the protocol and host address, including the port. Please provide in the format `<protocol>://<host>:<port>`. Supported protocols are `http` and `https`.
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: client
type: string
description: Specifies the client of the ABAP system to be addressed
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: commit
type: string
description: Specifies the commit to be deployed
scope:
- PARAMETERS
- STAGES
- STEPS