1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/pkg/abap/build/bfw.go
2022-04-26 17:14:55 +02:00

606 lines
18 KiB
Go

package build
import (
"encoding/json"
"fmt"
"net/url"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/pkg/errors"
)
// RunState : Current Status of the Build
type RunState string
type resultState string
type msgty string
const (
successful resultState = "SUCCESSFUL"
warning resultState = "WARNING"
erroneous resultState = "ERRONEOUS"
aborted resultState = "ABORTED"
// Initializing : Build Framework prepared
Initializing RunState = "INITIALIZING"
// Accepted : Build Framework triggered
Accepted RunState = "ACCEPTED"
// Running : Build Framework performs build
Running RunState = "RUNNING"
// Finished : Build Framework ended successful
Finished RunState = "FINISHED"
// Failed : Build Framework endded with error
Failed RunState = "FAILED"
loginfo msgty = "I"
logwarning msgty = "W"
logerror msgty = "E"
logaborted msgty = "A"
dummyResultName string = "Dummy"
)
//******** structs needed for json convertion ********
type jsonBuild struct {
Build struct {
BuildID string `json:"build_id"`
RunState RunState `json:"run_state"`
ResultState resultState `json:"result_state"`
Phase string `json:"phase"`
Entitytype string `json:"entitytype"`
Startedby string `json:"startedby"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at"`
} `json:"d"`
}
type jsonTasks struct {
ResultTasks struct {
Tasks []jsonTask `json:"results"`
} `json:"d"`
}
type jsonTask struct {
BuildID string `json:"build_id"`
TaskID int `json:"task_id"`
LogID string `json:"log_id"`
PluginClass string `json:"plugin_class"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at"`
ResultState resultState `json:"result_state"`
}
type jsonLogs struct {
ResultLogs struct {
Logs []logStruct `json:"results"`
} `json:"d"`
}
type jsonResults struct {
ResultResults struct {
Results []Result `json:"results"`
} `json:"d"`
}
type jsonValues struct {
ResultValues struct {
Values []Value `json:"results"`
} `json:"d"`
}
// ******** resembling data model in backend ********
// Build : Information for all data comming from Build Framework
type Build struct {
Connector Connector
BuildID string `json:"build_id"`
RunState RunState `json:"run_state"`
ResultState resultState `json:"result_state"`
Phase string `json:"phase"`
Entitytype string `json:"entitytype"`
Startedby string `json:"startedby"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at"`
Tasks []task
Values []Value
}
type task struct {
connector Connector
BuildID string `json:"build_id"`
TaskID int `json:"task_id"`
LogID string `json:"log_id"`
PluginClass string `json:"plugin_class"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at"`
ResultState resultState `json:"result_state"`
Logs []logStruct
Results []Result
}
type logStruct struct {
BuildID string `json:"build_id"`
TaskID int `json:"task_id"`
LogID string `json:"log_id"`
Msgty msgty `json:"msgty"`
Detlevel string `json:"detlevel"`
Logline string `json:"log_line"`
Timestamp string `json:"TIME_STMP"`
}
// Result : Artefact from Build Framework step
type Result struct {
connector Connector
BuildID string `json:"build_id"`
TaskID int `json:"task_id"`
Name string `json:"name"`
AdditionalInfo string `json:"additional_info"`
Mimetype string `json:"mimetype"`
SavedFilename string
DownloadPath string
}
// Value : Returns Build Runtime Value
type Value struct {
connector Connector
BuildID string `json:"build_id"`
ValueID string `json:"value_id"`
Value string `json:"value"`
}
// Values : Returns Build Runtime Values
type Values struct {
Values []Value `json:"results"`
}
type inputForPost struct {
phase string
values Values
}
// *********************************************************************
// ******************************* Funcs *******************************
// *********************************************************************
// Start : Starts the Build Framework
func (b *Build) Start(phase string, inputValues Values) error {
if err := b.Connector.GetToken(""); err != nil {
return err
}
importBody := inputForPost{
phase: phase,
values: inputValues,
}.String()
body, err := b.Connector.Post("/builds", importBody)
if err != nil {
return errors.Wrap(err, "Start of build failed: "+string(body))
}
var jBuild jsonBuild
if err := json.Unmarshal(body, &jBuild); err != nil {
return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
}
b.BuildID = jBuild.Build.BuildID
b.RunState = jBuild.Build.RunState
b.ResultState = jBuild.Build.ResultState
b.Phase = jBuild.Build.Phase
b.Entitytype = jBuild.Build.Entitytype
b.Startedby = jBuild.Build.Startedby
b.StartedAt = jBuild.Build.StartedAt
b.FinishedAt = jBuild.Build.FinishedAt
return nil
}
// Poll : waits for the build framework to be finished
func (b *Build) Poll() error {
timeout := time.After(b.Connector.MaxRuntime)
ticker := time.Tick(b.Connector.PollingInterval)
for {
select {
case <-timeout:
return errors.Errorf("Timed out: (max Runtime %v reached)", b.Connector.MaxRuntime)
case <-ticker:
b.Get()
if !b.IsFinished() {
log.Entry().Infof("Build is not yet finished, check again in %s", b.Connector.PollingInterval)
} else {
return nil
}
}
}
}
// EvaluteIfBuildSuccessful : Checks the finale state of the build framework
func (b *Build) EvaluteIfBuildSuccessful(treatWarningsAsError bool) error {
if b.RunState == Failed {
return errors.Errorf("Build ended with runState failed")
}
if treatWarningsAsError && b.ResultState == warning {
return errors.Errorf("Build ended with resultState warning, setting to failed as configured")
}
if (b.ResultState == aborted) || (b.ResultState == erroneous) {
return errors.Errorf("Build ended with resultState %s", b.ResultState)
}
return nil
}
// Get : Get all Build tasks
func (b *Build) Get() error {
appendum := "/builds('" + url.QueryEscape(b.BuildID) + "')"
body, err := b.Connector.Get(appendum)
if err != nil {
return err
}
var jBuild jsonBuild
if err := json.Unmarshal(body, &jBuild); err != nil {
return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
}
b.RunState = jBuild.Build.RunState
b.ResultState = jBuild.Build.ResultState
b.Phase = jBuild.Build.Phase
b.Entitytype = jBuild.Build.Entitytype
b.Startedby = jBuild.Build.Startedby
b.StartedAt = jBuild.Build.StartedAt
b.FinishedAt = jBuild.Build.FinishedAt
return nil
}
func (b *Build) getTasks() error {
if len(b.Tasks) == 0 {
appendum := "/builds('" + url.QueryEscape(b.BuildID) + "')/tasks"
body, err := b.Connector.Get(appendum)
if err != nil {
return err
}
b.Tasks, err = unmarshalTasks(body, b.Connector)
if err != nil {
return err
}
sort.Slice(b.Tasks, func(i, j int) bool {
return b.Tasks[i].TaskID < b.Tasks[j].TaskID
})
}
return nil
}
// GetValues : Gets all Build values
func (b *Build) GetValues() error {
if len(b.Values) == 0 {
appendum := "/builds('" + url.QueryEscape(b.BuildID) + "')/values"
body, err := b.Connector.Get(appendum)
if err != nil {
return err
}
var jValues jsonValues
if err := json.Unmarshal(body, &jValues); err != nil {
return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
}
b.Values = jValues.ResultValues.Values
for i := range b.Values {
b.Values[i].connector = b.Connector
}
}
return nil
}
func (b *Build) getLogs() error {
if err := b.getTasks(); err != nil {
return err
}
for i := range b.Tasks {
if err := b.Tasks[i].getLogs(); err != nil {
return err
}
}
return nil
}
// PrintLogs : Returns the Build logs
func (b *Build) PrintLogs() error {
if err := b.getTasks(); err != nil {
return err
}
for i := range b.Tasks {
if err := b.Tasks[i].printLogs(); err != nil {
return err
}
}
return nil
}
// GetResults : Gets all Build results
func (b *Build) GetResults() error {
if err := b.getTasks(); err != nil {
return err
}
for i := range b.Tasks {
if err := b.Tasks[i].getResults(); err != nil {
return err
}
}
return nil
}
func (t *task) printLogs() error {
if err := t.getLogs(); err != nil {
return err
}
for _, logs := range t.Logs {
logs.print()
}
return nil
}
// GetResult : Returns the last Build artefact created from build step
func (b *Build) GetResult(name string) (*Result, error) {
var Results []*Result
var returnResult Result
if err := b.GetResults(); err != nil {
return &returnResult, err
}
for i_task := range b.Tasks {
for i_result := range b.Tasks[i_task].Results {
if b.Tasks[i_task].Results[i_result].Name == name {
Results = append(Results, &b.Tasks[i_task].Results[i_result])
}
}
}
switch len(Results) {
case 0:
return &returnResult, errors.New("No result named " + name + " was found")
case 1:
return Results[0], nil
default:
return &returnResult, errors.New("More than one result with the name " + name + " was found")
}
}
// IsFinished : Returns Build run state
func (b *Build) IsFinished() bool {
if b.RunState == Finished || b.RunState == Failed {
return true
}
return false
}
func (t *task) getLogs() error {
if len(t.Logs) == 0 {
appendum := fmt.Sprint("/tasks(build_id='", url.QueryEscape(t.BuildID), "',task_id=", t.TaskID, ")/logs")
body, err := t.connector.Get(appendum)
if err != nil {
return err
}
var jLogs jsonLogs
if err := json.Unmarshal(body, &jLogs); err != nil {
return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
}
t.Logs = jLogs.ResultLogs.Logs
}
return nil
}
func (t *task) getResults() error {
if len(t.Results) == 0 {
appendum := fmt.Sprint("/tasks(build_id='", url.QueryEscape(t.BuildID), "',task_id=", t.TaskID, ")/results")
body, err := t.connector.Get(appendum)
if err != nil {
return err
}
var jResults jsonResults
if err := json.Unmarshal(body, &jResults); err != nil {
return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
}
t.Results = jResults.ResultResults.Results
for i := range t.Results {
t.Results[i].connector = t.connector
}
if len(t.Results) == 0 {
//prevent 2nd GET request - no new results will occure...
t.Results = append(t.Results, Result{Name: dummyResultName})
}
}
return nil
}
// DownloadAllResults : Downloads all build artefacts, saves it to basePath and the filenames can be modified with the filenamePrefix
func (b *Build) DownloadAllResults(basePath string, filenamePrefix string) error {
if err := b.GetResults(); err != nil {
return err
}
for i_task := range b.Tasks {
//in case there was no result, there is only one entry with dummyResultName, obviously we don't want to download this
if b.Tasks[i_task].Results[0].Name != dummyResultName {
for i_result := range b.Tasks[i_task].Results {
if err := b.Tasks[i_task].Results[i_result].DownloadWithFilenamePrefixAndTargetDirectory(basePath, filenamePrefix); err != nil {
return errors.Wrapf(err, "Error during the download of file %s", b.Tasks[i_task].Results[i_result].Name)
}
}
}
}
return nil
}
// DownloadResults : Download results which are specified in filenames
func (b *Build) DownloadResults(filenames []string, basePath string, filenamePrefix string) error {
for _, name := range filenames {
result, err := b.GetResult(name)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "Problems finding the file %s, please check your config whether this file is really a result file", name)
}
if err := result.DownloadWithFilenamePrefixAndTargetDirectory(basePath, filenamePrefix); err != nil {
return errors.Wrapf(err, "Error during the download of file %s", name)
}
}
return nil
}
// PublishAllDownloadedResults : publishes all build artefacts which were downloaded before
func (b *Build) PublishAllDownloadedResults(stepname string, publish Publish) {
var filesToPublish []piperutils.Path
for i_task := range b.Tasks {
for i_result := range b.Tasks[i_task].Results {
if b.Tasks[i_task].Results[i_result].wasDownloaded() {
filesToPublish = append(filesToPublish, piperutils.Path{Target: b.Tasks[i_task].Results[i_result].DownloadPath,
Name: b.Tasks[i_task].Results[i_result].SavedFilename, Mandatory: true})
}
}
}
if len(filesToPublish) > 0 {
publish.PersistReportsAndLinks(stepname, "", filesToPublish, nil)
}
}
// PublishDownloadedResults : Publishes build artefacts specified in filenames
func (b *Build) PublishDownloadedResults(stepname string, filenames []string, publish Publish) error {
var filesToPublish []piperutils.Path
for i := range filenames {
result, err := b.GetResult(filenames[i])
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "Problems finding the file %s, please check your config whether this file is really a result file", filenames[i])
}
if result.wasDownloaded() {
filesToPublish = append(filesToPublish, piperutils.Path{Target: result.DownloadPath, Name: result.SavedFilename, Mandatory: true})
} else {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Errorf("Trying to publish the file %s which was not downloaded", result.Name)
}
}
if len(filesToPublish) > 0 {
publish.PersistReportsAndLinks(stepname, "", filesToPublish, nil)
}
return nil
}
// Download : Provides the atrefact of build step
func (result *Result) Download(downloadPath string) error {
appendum := fmt.Sprint("/results(build_id='", url.QueryEscape(result.BuildID), "',task_id=", result.TaskID, ",name='", url.QueryEscape(result.Name), "')/$value")
err := result.connector.Download(appendum, downloadPath)
return err
}
// DownloadWithFilenamePrefixAndTargetDirectory : downloads build artefact, saves it to basePath and the filename can be modified with the filenamePrefix
func (result *Result) DownloadWithFilenamePrefixAndTargetDirectory(basePath string, filenamePrefix string) error {
basePath, err := result.resolveParamter(basePath)
if err != nil {
return errors.Wrapf(err, "Could not resolve parameter %s for the target directory", basePath)
}
filenamePrefix, err = result.resolveParamter(filenamePrefix)
if err != nil {
return errors.Wrapf(err, "Could not resolve parameter %s for the filename prefix", filenamePrefix)
}
appendum := fmt.Sprint("/results(build_id='", result.BuildID, "',task_id=", result.TaskID, ",name='", result.Name, "')/$value")
filename := filenamePrefix + result.Name
downloadPath := filepath.Join(path.Base(basePath), path.Base(filename))
if err := result.connector.Download(appendum, downloadPath); err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
return errors.Wrapf(err, "Could not download %s", result.Name)
}
result.SavedFilename = filename
result.DownloadPath = downloadPath
log.Entry().Infof("Saved file %s as %s to %s", result.Name, result.SavedFilename, result.DownloadPath)
return nil
}
func (result *Result) resolveParamter(parameter string) (string, error) {
if len(parameter) == 0 {
return parameter, nil
}
if (string(parameter[0]) == "{") && string(parameter[len(parameter)-1]) == "}" {
trimmedParam := strings.ToLower(parameter[1 : len(parameter)-1])
switch trimmedParam {
case "buildid":
return result.BuildID, nil
case "taskid":
return strconv.Itoa(result.TaskID), nil
default:
log.SetErrorCategory(log.ErrorConfiguration)
return "", errors.Errorf("Unknown parameter %s", parameter)
}
} else {
return parameter, nil
}
}
func (result *Result) wasDownloaded() bool {
if len(result.DownloadPath) > 0 && len(result.SavedFilename) > 0 {
return true
} else {
return false
}
}
func (logging *logStruct) print() {
switch logging.Msgty {
case loginfo:
log.Entry().WithField("Timestamp", logging.Timestamp).Info(logging.Logline)
case logwarning:
log.Entry().WithField("Timestamp", logging.Timestamp).Warn(logging.Logline)
case logerror:
log.Entry().WithField("Timestamp", logging.Timestamp).Error(logging.Logline)
case logaborted:
log.Entry().WithField("Timestamp", logging.Timestamp).Error(logging.Logline)
default:
}
}
// ******** parsing ********
func (v Value) String() string {
return fmt.Sprintf(
`{ "value_id": "%s", "value": "%s" }`,
v.ValueID,
v.Value)
}
func (vs Values) String() string {
returnString := ""
for _, value := range vs.Values {
returnString = returnString + value.String() + ",\n"
}
if len(returnString) > 0 {
returnString = returnString[:len(returnString)-2] //removes last ,
}
return returnString
}
func (in inputForPost) String() string {
return fmt.Sprintf(`{ "phase": "%s", "values": [%s]}`, in.phase, in.values.String())
}
//******** unmarshal function ************
func unmarshalTasks(body []byte, connector Connector) ([]task, error) {
var tasks []task
var append_task task
var jTasks jsonTasks
if err := json.Unmarshal(body, &jTasks); err != nil {
return tasks, errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
}
for _, jTask := range jTasks.ResultTasks.Tasks {
append_task.connector = connector
append_task.BuildID = jTask.BuildID
append_task.TaskID = jTask.TaskID
append_task.LogID = jTask.LogID
append_task.PluginClass = jTask.PluginClass
append_task.StartedAt = jTask.StartedAt
append_task.FinishedAt = jTask.FinishedAt
append_task.ResultState = jTask.ResultState
tasks = append(tasks, append_task)
}
return tasks, nil
}
// *****************publish *******************************
type Publish interface {
PersistReportsAndLinks(stepName, workspace string, reports, links []piperutils.Path)
}
func PersistReportsAndLinks(stepName, workspace string, reports, links []piperutils.Path) {
piperutils.PersistReportsAndLinks(stepName, workspace, reports, links)
}