package abaputils import ( "encoding/json" "fmt" "io/ioutil" "reflect" "sort" "strings" "time" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" "github.com/pkg/errors" ) const failureMessageClonePull = "Could not pull the Repository / Software Component " // PollEntity periodically polls the pull/import entity to get the status. Check if the import is still running func PollEntity(repositoryName string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender, pollIntervall time.Duration) (string, error) { log.Entry().Info("Start polling the status...") var status string = "R" for { pullEntity, responseStatus, err := GetStatus(failureMessageClonePull+repositoryName, connectionDetails, client) if err != nil { return status, err } status = pullEntity.Status log.Entry().WithField("StatusCode", responseStatus).Info("Pull Status: " + pullEntity.StatusDescription) if pullEntity.Status != "R" { printTransportLogs := true if serviceContainsNewLogEntities(connectionDetails, client) { PrintLogs(repositoryName, connectionDetails, client) printTransportLogs = false } if pullEntity.Status == "E" { log.SetErrorCategory(log.ErrorUndefined) PrintLegacyLogs(repositoryName, connectionDetails, client, true, printTransportLogs) } else { PrintLegacyLogs(repositoryName, connectionDetails, client, false, printTransportLogs) } break } time.Sleep(pollIntervall) } return status, nil } func serviceContainsNewLogEntities(connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) (newLogEntitiesAvailable bool) { newLogEntitiesAvailable = false details := connectionDetails details.URL = details.Host + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/" resp, err := GetHTTPResponse("GET", details, nil, client) if err != nil { return } defer resp.Body.Close() var entitySet EntitySetsForManageGitRepository // Parse response var abapResp map[string]*json.RawMessage bodyText, _ := ioutil.ReadAll(resp.Body) json.Unmarshal(bodyText, &abapResp) json.Unmarshal(*abapResp["d"], &entitySet) for _, entitySet := range entitySet.EntitySets { if entitySet == "LogOverviews" || entitySet == "LogProtocols" { return true } } return } func PrintLogs(repositoryName string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) { connectionDetails.URL = connectionDetails.URL + "?$expand=to_Log_Overview,to_Log_Overview/to_Log_Protocol" entity, _, err := GetStatus(failureMessageClonePull+repositoryName, connectionDetails, client) if err != nil { return } if len(entity.ToLogOverview.Results) == 0 { // return if no logs are available return } // Sort logs sort.SliceStable(entity.ToLogOverview.Results, func(i, j int) bool { return entity.ToLogOverview.Results[i].Index < entity.ToLogOverview.Results[j].Index }) // Get Lengths phaseLength := 22 // minimum default length for _, logEntry := range entity.ToLogOverview.Results { if l := len(logEntry.Name); l > phaseLength { phaseLength = l } } statusLength := 10 timestampLength := 29 // Dashed Line Length lineLength := 10 + phaseLength + statusLength + timestampLength // Print Overview log.Entry().Infof("\n") dashedLine(lineLength) log.Entry().Infof("| %-"+fmt.Sprint(phaseLength)+"s | %"+fmt.Sprint(statusLength)+"s | %-"+fmt.Sprint(timestampLength)+"s |", "Phase", "Status", "Timestamp") dashedLine(lineLength) for _, logEntry := range entity.ToLogOverview.Results { log.Entry().Infof("| %-"+fmt.Sprint(phaseLength)+"s | %"+fmt.Sprint(statusLength)+"s | %-"+fmt.Sprint(timestampLength)+"s |", logEntry.Name, logEntry.Status, ConvertTime(logEntry.Timestamp)) } dashedLine(lineLength) // Print Details for _, logEntryForDetails := range entity.ToLogOverview.Results { printLog(logEntryForDetails) } log.Entry().Infof("-------------------------") return } func dashedLine(i int) { log.Entry().Infof(strings.Repeat("-", i)) } func printLog(logEntry LogResultsV2) { sort.SliceStable(logEntry.ToLogProtocol.Results, func(i, j int) bool { return logEntry.ToLogProtocol.Results[i].ProtocolLine < logEntry.ToLogProtocol.Results[j].ProtocolLine }) if logEntry.Status != `Success` { log.Entry().Infof("\n") log.Entry().Infof("-------------------------") log.Entry().Infof("%s (%v)", logEntry.Name, ConvertTime(logEntry.Timestamp)) log.Entry().Infof("-------------------------") for _, entry := range logEntry.ToLogProtocol.Results { log.Entry().Info(entry.Description) } } else { log.Entry().Debugf("\n") log.Entry().Debugf("-------------------------") log.Entry().Debugf("%s (%v)", logEntry.Name, ConvertTime(logEntry.Timestamp)) log.Entry().Debugf("-------------------------") for _, entry := range logEntry.ToLogProtocol.Results { log.Entry().Debug(entry.Description) } } } // PrintLegacyLogs sorts and formats the received transport and execution log of an import; Deprecated with SAP BTP, ABAP Environment release 2205 func PrintLegacyLogs(repositoryName string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender, errorOnSystem bool, includeTransportLog bool) { connectionDetails.URL = connectionDetails.URL + "?$expand=to_Transport_log,to_Execution_log" entity, _, err := GetStatus(failureMessageClonePull+repositoryName, connectionDetails, client) if err != nil { return } // Sort logs sort.SliceStable(entity.ToExecutionLog.Results, func(i, j int) bool { return entity.ToExecutionLog.Results[i].Index < entity.ToExecutionLog.Results[j].Index }) sort.SliceStable(entity.ToTransportLog.Results, func(i, j int) bool { return entity.ToTransportLog.Results[i].Index < entity.ToTransportLog.Results[j].Index }) // Show transport and execution log if either the action was erroenous on the system or the log level is set to "debug" (verbose = true) if errorOnSystem { if includeTransportLog { log.Entry().Info("-------------------------") log.Entry().Info("Transport Log") log.Entry().Info("-------------------------") for _, logEntry := range entity.ToTransportLog.Results { log.Entry().WithField("Timestamp", ConvertTime(logEntry.Timestamp)).Info(logEntry.Description) } } log.Entry().Info("-------------------------") log.Entry().Info("Execution Log") log.Entry().Info("-------------------------") for _, logEntry := range entity.ToExecutionLog.Results { log.Entry().WithField("Timestamp", ConvertTime(logEntry.Timestamp)).Info(logEntry.Description) } log.Entry().Info("-------------------------") } else { if includeTransportLog { log.Entry().Debug("-------------------------") log.Entry().Debug("Transport Log") log.Entry().Debug("-------------------------") for _, logEntry := range entity.ToTransportLog.Results { log.Entry().WithField("Timestamp", ConvertTime(logEntry.Timestamp)).Debug(logEntry.Description) } } log.Entry().Debug("-------------------------") log.Entry().Debug("Execution Log") log.Entry().Debug("-------------------------") for _, logEntry := range entity.ToExecutionLog.Results { log.Entry().WithField("Timestamp", ConvertTime(logEntry.Timestamp)).Debug(logEntry.Description) } log.Entry().Debug("-------------------------") } } func GetStatus(failureMessage string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) (body PullEntity, status string, err error) { resp, err := GetHTTPResponse("GET", connectionDetails, nil, client) if err != nil { log.SetErrorCategory(log.ErrorInfrastructure) err = HandleHTTPError(resp, err, failureMessage, connectionDetails) if resp != nil { status = resp.Status } return body, status, err } defer resp.Body.Close() // Parse response var abapResp map[string]*json.RawMessage bodyText, _ := ioutil.ReadAll(resp.Body) json.Unmarshal(bodyText, &abapResp) if err != nil { return body, resp.Status, errors.Wrap(err, "Could not read response") } json.Unmarshal(*abapResp["d"], &body) if err != nil { return body, resp.Status, errors.Wrap(err, "Could not read response") } if reflect.DeepEqual(PullEntity{}, body) { log.Entry().WithField("StatusCode", resp.Status).Error(failureMessage) log.SetErrorCategory(log.ErrorInfrastructure) var err = errors.New("Request to ABAP System not successful") return body, resp.Status, err } return body, resp.Status, nil } //GetRepositories for parsing one or multiple branches and repositories from repositories file or branchName and repositoryName configuration func GetRepositories(config *RepositoriesConfig, branchRequired bool) ([]Repository, error) { var repositories = make([]Repository, 0) if reflect.DeepEqual(RepositoriesConfig{}, config) { log.SetErrorCategory(log.ErrorConfiguration) return repositories, fmt.Errorf("Failed to read repository configuration: %w", errors.New("Eror in configuration, most likely you have entered empty or wrong configuration values. Please make sure that you have correctly specified them. For more information please read the User documentation")) } if config.RepositoryName == "" && config.BranchName == "" && config.Repositories == "" && len(config.RepositoryNames) == 0 { log.SetErrorCategory(log.ErrorConfiguration) return repositories, fmt.Errorf("Failed to read repository configuration: %w", errors.New("You have not specified any repository configuration. Please make sure that you have correctly specified it. For more information please read the User documentation")) } if config.Repositories != "" { descriptor, err := ReadAddonDescriptor(config.Repositories) if err != nil { log.SetErrorCategory(log.ErrorConfiguration) return repositories, err } err = CheckAddonDescriptorForRepositories(descriptor) if err != nil { log.SetErrorCategory(log.ErrorConfiguration) return repositories, fmt.Errorf("Error in config file %v, %w", config.Repositories, err) } repositories = descriptor.Repositories } if config.RepositoryName != "" && config.BranchName != "" { repositories = append(repositories, Repository{Name: config.RepositoryName, Branch: config.BranchName}) } if config.RepositoryName != "" && !branchRequired { repositories = append(repositories, Repository{Name: config.RepositoryName, CommitID: config.CommitID}) } if len(config.RepositoryNames) > 0 { for _, repository := range config.RepositoryNames { repositories = append(repositories, Repository{Name: repository}) } } return repositories, nil } func (repo *Repository) GetRequestBodyForCommitOrTag() (requestBodyString string) { if repo.CommitID != "" { requestBodyString = `, "commit_id":"` + repo.CommitID + `"` } else if repo.Tag != "" { requestBodyString = `, "tag_name":"` + repo.Tag + `"` } return requestBodyString } func (repo *Repository) GetLogStringForCommitOrTag() (logString string) { if repo.CommitID != "" { logString = ", commit '" + repo.CommitID + "'" } else if repo.Tag != "" { logString = ", tag '" + repo.Tag + "'" } return logString } func (repo *Repository) GetCloneRequestBody() (body string) { if repo.CommitID != "" && repo.Tag != "" { log.Entry().WithField("Tag", repo.Tag).WithField("Commit ID", repo.CommitID).Info("The commit ID takes precedence over the tag") } requestBodyString := repo.GetRequestBodyForCommitOrTag() body = `{"sc_name":"` + repo.Name + `", "branch_name":"` + repo.Branch + `"` + requestBodyString + `}` return body } func (repo *Repository) GetCloneLogString() (logString string) { commitOrTag := repo.GetLogStringForCommitOrTag() logString = "repository / software component '" + repo.Name + "', branch '" + repo.Branch + "'" + commitOrTag return logString } func (repo *Repository) GetPullRequestBody() (body string) { if repo.CommitID != "" && repo.Tag != "" { log.Entry().WithField("Tag", repo.Tag).WithField("Commit ID", repo.CommitID).Info("The commit ID takes precedence over the tag") } requestBodyString := repo.GetRequestBodyForCommitOrTag() body = `{"sc_name":"` + repo.Name + `"` + requestBodyString + `}` return body } func (repo *Repository) GetPullLogString() (logString string) { commitOrTag := repo.GetLogStringForCommitOrTag() logString = "repository / software component '" + repo.Name + "'" + commitOrTag return logString } /**************************************** * Structs for the A4C_A2G_GHA service * ****************************************/ // PullEntity struct for the Pull/Import entity A4C_A2G_GHA_SC_IMP type PullEntity struct { Metadata AbapMetadata `json:"__metadata"` UUID string `json:"uuid"` Namespace string `json:"namepsace"` ScName string `json:"sc_name"` ImportType string `json:"import_type"` BranchName string `json:"branch_name"` StartedByUser string `json:"user_name"` Status string `json:"status"` StatusDescription string `json:"status_descr"` CommitID string `json:"commit_id"` StartTime string `json:"start_time"` ChangeTime string `json:"change_time"` ToExecutionLog AbapLogs `json:"to_Execution_log"` ToTransportLog AbapLogs `json:"to_Transport_log"` ToLogOverview AbapLogsV2 `json:"to_Log_Overview"` } // BranchEntity struct for the Branch entity A4C_A2G_GHA_SC_BRANCH type BranchEntity struct { Metadata AbapMetadata `json:"__metadata"` ScName string `json:"sc_name"` Namespace string `json:"namepsace"` BranchName string `json:"branch_name"` ParentBranch string `json:"derived_from"` CreatedBy string `json:"created_by"` CreatedOn string `json:"created_on"` IsActive bool `json:"is_active"` CommitID string `json:"commit_id"` CommitMessage string `json:"commit_message"` LastCommitBy string `json:"last_commit_by"` LastCommitOn string `json:"last_commit_on"` } // CloneEntity struct for the Clone entity A4C_A2G_GHA_SC_CLONE type CloneEntity struct { Metadata AbapMetadata `json:"__metadata"` UUID string `json:"uuid"` ScName string `json:"sc_name"` BranchName string `json:"branch_name"` ImportType string `json:"import_type"` Namespace string `json:"namepsace"` Status string `json:"status"` StatusDescription string `json:"status_descr"` StartedByUser string `json:"user_name"` StartTime string `json:"start_time"` ChangeTime string `json:"change_time"` } // AbapLogs struct for ABAP logs type AbapLogs struct { Results []LogResults `json:"results"` } type AbapLogsV2 struct { Results []LogResultsV2 `json:"results"` } type LogResultsV2 struct { Metadata AbapMetadata `json:"__metadata"` Index int `json:"log_index"` Name string `json:"log_name"` Status string `json:"type_of_found_issues"` Timestamp string `json:"timestamp"` ToLogProtocol LogProtocolResults `json:"to_Log_Protocol"` } type LogProtocolResults struct { Results []LogProtocol `json:"results"` } type LogProtocol struct { Metadata AbapMetadata `json:"__metadata"` OverviewIndex int `json:"log_index"` ProtocolLine int `json:"index_no"` Type string `json:"type"` Description string `json:"descr"` } // LogResults struct for Execution and Transport Log entities A4C_A2G_GHA_SC_LOG_EXE and A4C_A2G_GHA_SC_LOG_TP type LogResults struct { Index string `json:"index_no"` Type string `json:"type"` Description string `json:"descr"` Timestamp string `json:"timestamp"` } //RepositoriesConfig struct for parsing one or multiple branches and repositories configurations type RepositoriesConfig struct { BranchName string CommitID string RepositoryName string RepositoryNames []string Repositories string } type EntitySetsForManageGitRepository struct { EntitySets []string `json:"EntitySets"` }