github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/cmd/abapEnvironmentCloneGitRepo.go (about)

     1  package cmd
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/http/cookiejar"
     9  	"reflect"
    10  	"time"
    11  
    12  	"github.com/SAP/jenkins-library/pkg/abaputils"
    13  	"github.com/SAP/jenkins-library/pkg/command"
    14  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    15  	"github.com/SAP/jenkins-library/pkg/log"
    16  	"github.com/SAP/jenkins-library/pkg/telemetry"
    17  	"github.com/pkg/errors"
    18  )
    19  
    20  func abapEnvironmentCloneGitRepo(config abapEnvironmentCloneGitRepoOptions, _ *telemetry.CustomData) {
    21  
    22  	c := command.Command{}
    23  
    24  	c.Stdout(log.Writer())
    25  	c.Stderr(log.Writer())
    26  
    27  	var autils = abaputils.AbapUtils{
    28  		Exec: &c,
    29  	}
    30  
    31  	client := piperhttp.Client{}
    32  	// error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end
    33  	err := runAbapEnvironmentCloneGitRepo(&config, &autils, &client)
    34  	if err != nil {
    35  		log.Entry().WithError(err).Fatal("step execution failed")
    36  	}
    37  }
    38  
    39  func runAbapEnvironmentCloneGitRepo(config *abapEnvironmentCloneGitRepoOptions, com abaputils.Communication, client piperhttp.Sender) error {
    40  	// Mapping for options
    41  	subOptions := convertCloneConfig(config)
    42  
    43  	// Determine the host, user and password, either via the input parameters or via a cloud foundry service key
    44  	connectionDetails, errorGetInfo := com.GetAbapCommunicationArrangementInfo(subOptions, "")
    45  	if errorGetInfo != nil {
    46  		return errors.Wrap(errorGetInfo, "Parameters for the ABAP Connection not available")
    47  	}
    48  
    49  	// Configuring the HTTP Client and CookieJar
    50  	cookieJar, errorCookieJar := cookiejar.New(nil)
    51  	if errorCookieJar != nil {
    52  		return errors.Wrap(errorCookieJar, "Could not create a Cookie Jar")
    53  	}
    54  
    55  	client.SetOptions(piperhttp.ClientOptions{
    56  		MaxRequestDuration: 180 * time.Second,
    57  		CookieJar:          cookieJar,
    58  		Username:           connectionDetails.User,
    59  		Password:           connectionDetails.Password,
    60  	})
    61  
    62  	errConfig := checkConfiguration(config)
    63  	if errConfig != nil {
    64  		return errors.Wrap(errConfig, "The provided configuration is not allowed")
    65  	}
    66  
    67  	repositories, errGetRepos := abaputils.GetRepositories(&abaputils.RepositoriesConfig{BranchName: config.BranchName, RepositoryName: config.RepositoryName, Repositories: config.Repositories}, true)
    68  	if errGetRepos != nil {
    69  		return fmt.Errorf("Something failed during the clone: %w", errGetRepos)
    70  	}
    71  
    72  	log.Entry().Infof("Start cloning %v repositories", len(repositories))
    73  	for _, repo := range repositories {
    74  
    75  		logString := repo.GetCloneLogString()
    76  		errorString := "Clone of " + logString + " failed on the ABAP system"
    77  
    78  		abaputils.AddDefaultDashedLine()
    79  		log.Entry().Info("Start cloning " + logString)
    80  		abaputils.AddDefaultDashedLine()
    81  
    82  		// Triggering the Clone of the repository into the ABAP Environment system
    83  		uriConnectionDetails, errorTriggerClone, didCheckoutPullInstead := triggerClone(repo, connectionDetails, client)
    84  		if errorTriggerClone != nil {
    85  			return errors.Wrapf(errorTriggerClone, errorString)
    86  		}
    87  
    88  		if !didCheckoutPullInstead {
    89  			// Polling the status of the repository import on the ABAP Environment system
    90  			// If the repository had been cloned already, as checkout/pull has been done - polling the status is not necessary anymore
    91  			status, errorPollEntity := abaputils.PollEntity(repo.Name, uriConnectionDetails, client, com.GetPollIntervall())
    92  			if errorPollEntity != nil {
    93  				return errors.Wrapf(errorPollEntity, errorString)
    94  			}
    95  			if status == "E" {
    96  				return errors.New("Clone of " + logString + " failed on the ABAP System")
    97  			}
    98  			log.Entry().Info("The " + logString + " was cloned successfully")
    99  		}
   100  	}
   101  	abaputils.AddDefaultDashedLine()
   102  	log.Entry().Info("All repositories were cloned successfully")
   103  	return nil
   104  }
   105  
   106  func checkConfiguration(config *abapEnvironmentCloneGitRepoOptions) error {
   107  	if config.Repositories != "" && config.RepositoryName != "" {
   108  		return errors.New("It is not allowed to configure the parameters `repositories`and `repositoryName` at the same time")
   109  	}
   110  	if config.Repositories == "" && config.RepositoryName == "" {
   111  		return errors.New("Please provide one of the following parameters: `repositories` or `repositoryName`")
   112  	}
   113  	return nil
   114  }
   115  
   116  func triggerClone(repo abaputils.Repository, cloneConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (abaputils.ConnectionDetailsHTTP, error, bool) {
   117  
   118  	uriConnectionDetails := cloneConnectionDetails
   119  	cloneConnectionDetails.XCsrfToken = "fetch"
   120  
   121  	cloneConnectionDetails.URL = cloneConnectionDetails.URL + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Clones"
   122  
   123  	// Loging into the ABAP System - getting the x-csrf-token and cookies
   124  	resp, err := abaputils.GetHTTPResponse("HEAD", cloneConnectionDetails, nil, client)
   125  	if err != nil {
   126  		err = abaputils.HandleHTTPError(resp, err, "Authentication on the ABAP system failed", cloneConnectionDetails)
   127  		return uriConnectionDetails, err, false
   128  	}
   129  	defer resp.Body.Close()
   130  
   131  	log.Entry().WithField("StatusCode", resp.Status).WithField("ABAP Endpoint", cloneConnectionDetails.URL).Debug("Authentication on the ABAP system successful")
   132  	uriConnectionDetails.XCsrfToken = resp.Header.Get("X-Csrf-Token")
   133  	cloneConnectionDetails.XCsrfToken = uriConnectionDetails.XCsrfToken
   134  
   135  	// Trigger the Clone of a Repository
   136  	if repo.Name == "" {
   137  		return uriConnectionDetails, errors.New("An empty string was passed for the parameter 'repositoryName'"), false
   138  	}
   139  
   140  	jsonBody := []byte(repo.GetCloneRequestBody())
   141  	resp, err = abaputils.GetHTTPResponse("POST", cloneConnectionDetails, jsonBody, client)
   142  	if err != nil {
   143  		err, alreadyCloned := handleCloneError(resp, err, cloneConnectionDetails, client, repo)
   144  		return uriConnectionDetails, err, alreadyCloned
   145  	}
   146  	defer resp.Body.Close()
   147  	log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", repo.Name).WithField("branchName", repo.Branch).WithField("commitID", repo.CommitID).WithField("Tag", repo.Tag).Info("Triggered Clone of Repository / Software Component")
   148  
   149  	// Parse Response
   150  	var body abaputils.CloneEntity
   151  	var abapResp map[string]*json.RawMessage
   152  	bodyText, errRead := io.ReadAll(resp.Body)
   153  	if errRead != nil {
   154  		return uriConnectionDetails, err, false
   155  	}
   156  	if err := json.Unmarshal(bodyText, &abapResp); err != nil {
   157  		return uriConnectionDetails, err, false
   158  	}
   159  	if err := json.Unmarshal(*abapResp["d"], &body); err != nil {
   160  		return uriConnectionDetails, err, false
   161  	}
   162  	if reflect.DeepEqual(abaputils.CloneEntity{}, body) {
   163  		log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", repo.Name).WithField("branchName", repo.Branch).WithField("commitID", repo.CommitID).WithField("Tag", repo.Tag).Error("Could not Clone the Repository / Software Component")
   164  		err := errors.New("Request to ABAP System not successful")
   165  		return uriConnectionDetails, err, false
   166  	}
   167  
   168  	// The entity "Clones" does not allow for polling. To poll the progress, the related entity "Pull" has to be called
   169  	// While "Clones" has the key fields UUID, SC_NAME and BRANCH_NAME, "Pull" only has the key field UUID
   170  	uriConnectionDetails.URL = uriConnectionDetails.URL + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Pull(uuid=guid'" + body.UUID + "')"
   171  	return uriConnectionDetails, nil, false
   172  }
   173  
   174  func handleCloneError(resp *http.Response, err error, cloneConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, repo abaputils.Repository) (returnedError error, alreadyCloned bool) {
   175  	alreadyCloned = false
   176  	returnedError = nil
   177  	if resp == nil {
   178  		log.Entry().WithError(err).WithField("ABAP Endpoint", cloneConnectionDetails.URL).Error("Request failed")
   179  		returnedError = errors.New("Response is nil")
   180  		return
   181  	}
   182  	defer resp.Body.Close()
   183  	errorText, errorCode, parsingError := abaputils.GetErrorDetailsFromResponse(resp)
   184  	if parsingError != nil {
   185  		returnedError = err
   186  		return
   187  	}
   188  	if errorCode == "A4C_A2G/257" {
   189  		// With the latest release, a repeated "clone" was prohibited
   190  		// As an intermediate workaround, we react to the error message A4C_A2G/257 that gets thrown, if the repository had already been cloned
   191  		// In this case, a checkout branch and a pull will be performed
   192  		alreadyCloned = true
   193  		abaputils.AddDefaultDashedLine()
   194  		abaputils.AddDefaultDashedLine()
   195  		log.Entry().Infof("%s", "The repository / software component has already been cloned on the ABAP Environment system ")
   196  		log.Entry().Infof("%s", "A `checkout branch` and a `pull` will be performed instead")
   197  		abaputils.AddDefaultDashedLine()
   198  		abaputils.AddDefaultDashedLine()
   199  		checkoutOptions := abapEnvironmentCheckoutBranchOptions{
   200  			Username:       cloneConnectionDetails.User,
   201  			Password:       cloneConnectionDetails.Password,
   202  			Host:           cloneConnectionDetails.Host,
   203  			RepositoryName: repo.Name,
   204  			BranchName:     repo.Branch,
   205  		}
   206  		c := command.Command{}
   207  		c.Stdout(log.Writer())
   208  		c.Stderr(log.Writer())
   209  		com := abaputils.AbapUtils{
   210  			Exec: &c,
   211  		}
   212  		returnedError = runAbapEnvironmentCheckoutBranch(&checkoutOptions, &com, client)
   213  		if returnedError != nil {
   214  			return
   215  		}
   216  		abaputils.AddDefaultDashedLine()
   217  		abaputils.AddDefaultDashedLine()
   218  		pullOptions := abapEnvironmentPullGitRepoOptions{
   219  			Username:       cloneConnectionDetails.User,
   220  			Password:       cloneConnectionDetails.Password,
   221  			Host:           cloneConnectionDetails.Host,
   222  			RepositoryName: repo.Name,
   223  			CommitID:       repo.CommitID,
   224  		}
   225  		returnedError = runAbapEnvironmentPullGitRepo(&pullOptions, &com, client)
   226  		if returnedError != nil {
   227  			return
   228  		}
   229  	} else {
   230  		log.Entry().WithField("StatusCode", resp.Status).Error("Could not clone the " + repo.GetCloneLogString())
   231  		abapError := errors.New(fmt.Sprintf("%s - %s", errorCode, errorText))
   232  		returnedError = errors.Wrap(abapError, err.Error())
   233  	}
   234  	return
   235  }
   236  
   237  func convertCloneConfig(config *abapEnvironmentCloneGitRepoOptions) abaputils.AbapEnvironmentOptions {
   238  	subOptions := abaputils.AbapEnvironmentOptions{}
   239  
   240  	subOptions.CfAPIEndpoint = config.CfAPIEndpoint
   241  	subOptions.CfServiceInstance = config.CfServiceInstance
   242  	subOptions.CfServiceKeyName = config.CfServiceKeyName
   243  	subOptions.CfOrg = config.CfOrg
   244  	subOptions.CfSpace = config.CfSpace
   245  	subOptions.Host = config.Host
   246  	subOptions.Password = config.Password
   247  	subOptions.Username = config.Username
   248  	return subOptions
   249  }