github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/abap/build/bfw.go (about)

     1  package build
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/url"
     7  	"path"
     8  	"path/filepath"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/SAP/jenkins-library/pkg/log"
    15  	"github.com/SAP/jenkins-library/pkg/piperutils"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  // RunState : Current Status of the Build
    20  type RunState string
    21  type resultState string
    22  type msgty string
    23  
    24  const (
    25  	successful resultState = "SUCCESSFUL"
    26  	warning    resultState = "WARNING"
    27  	erroneous  resultState = "ERRONEOUS"
    28  	aborted    resultState = "ABORTED"
    29  	// Initializing : Build Framework prepared
    30  	Initializing RunState = "INITIALIZING"
    31  	// Accepted : Build Framework triggered
    32  	Accepted RunState = "ACCEPTED"
    33  	// Running : Build Framework performs build
    34  	Running RunState = "RUNNING"
    35  	// Finished : Build Framework ended successful
    36  	Finished RunState = "FINISHED"
    37  	// Failed : Build Framework endded with error
    38  	Failed          RunState = "FAILED"
    39  	loginfo         msgty    = "I"
    40  	logwarning      msgty    = "W"
    41  	logerror        msgty    = "E"
    42  	logaborted      msgty    = "A"
    43  	dummyResultName string   = "Dummy"
    44  )
    45  
    46  // ******** structs needed for json convertion ********
    47  type jsonBuild struct {
    48  	Build struct {
    49  		BuildID     string      `json:"build_id"`
    50  		RunState    RunState    `json:"run_state"`
    51  		ResultState resultState `json:"result_state"`
    52  		Phase       string      `json:"phase"`
    53  		Entitytype  string      `json:"entitytype"`
    54  		Startedby   string      `json:"startedby"`
    55  		StartedAt   string      `json:"started_at"`
    56  		FinishedAt  string      `json:"finished_at"`
    57  	} `json:"d"`
    58  }
    59  
    60  type jsonTasks struct {
    61  	ResultTasks struct {
    62  		Tasks []jsonTask `json:"results"`
    63  	} `json:"d"`
    64  }
    65  
    66  type jsonTask struct {
    67  	BuildID     string      `json:"build_id"`
    68  	TaskID      int         `json:"task_id"`
    69  	LogID       string      `json:"log_id"`
    70  	PluginClass string      `json:"plugin_class"`
    71  	StartedAt   string      `json:"started_at"`
    72  	FinishedAt  string      `json:"finished_at"`
    73  	ResultState resultState `json:"result_state"`
    74  }
    75  
    76  type jsonLogs struct {
    77  	ResultLogs struct {
    78  		Logs []logStruct `json:"results"`
    79  	} `json:"d"`
    80  }
    81  
    82  type jsonResults struct {
    83  	ResultResults struct {
    84  		Results []Result `json:"results"`
    85  	} `json:"d"`
    86  }
    87  
    88  type jsonValues struct {
    89  	ResultValues struct {
    90  		Values []Value `json:"results"`
    91  	} `json:"d"`
    92  }
    93  
    94  // ******** resembling data model in backend ********
    95  
    96  // Build : Information for all data comming from Build Framework
    97  type Build struct {
    98  	Connector   Connector
    99  	BuildID     string      `json:"build_id"`
   100  	RunState    RunState    `json:"run_state"`
   101  	ResultState resultState `json:"result_state"`
   102  	Phase       string      `json:"phase"`
   103  	Entitytype  string      `json:"entitytype"`
   104  	Startedby   string      `json:"startedby"`
   105  	StartedAt   string      `json:"started_at"`
   106  	FinishedAt  string      `json:"finished_at"`
   107  	Tasks       []task
   108  	Values      []Value
   109  }
   110  
   111  type task struct {
   112  	connector   Connector
   113  	BuildID     string      `json:"build_id"`
   114  	TaskID      int         `json:"task_id"`
   115  	LogID       string      `json:"log_id"`
   116  	PluginClass string      `json:"plugin_class"`
   117  	StartedAt   string      `json:"started_at"`
   118  	FinishedAt  string      `json:"finished_at"`
   119  	ResultState resultState `json:"result_state"`
   120  	Logs        []logStruct
   121  	Results     []Result
   122  }
   123  
   124  type logStruct struct {
   125  	BuildID   string `json:"build_id"`
   126  	TaskID    int    `json:"task_id"`
   127  	LogID     string `json:"log_id"`
   128  	Msgty     msgty  `json:"msgty"`
   129  	Detlevel  string `json:"detlevel"`
   130  	Logline   string `json:"log_line"`
   131  	Timestamp string `json:"TIME_STMP"`
   132  }
   133  
   134  // Result : Artefact from Build Framework step
   135  type Result struct {
   136  	connector      Connector
   137  	BuildID        string `json:"build_id"`
   138  	TaskID         int    `json:"task_id"`
   139  	Name           string `json:"name"`
   140  	AdditionalInfo string `json:"additional_info"`
   141  	Mimetype       string `json:"mimetype"`
   142  	SavedFilename  string
   143  	DownloadPath   string
   144  }
   145  
   146  // Value : Returns Build Runtime Value
   147  type Value struct {
   148  	connector Connector
   149  	BuildID   string `json:"build_id,omitempty"`
   150  	ValueID   string `json:"value_id"`
   151  	Value     string `json:"value"`
   152  }
   153  
   154  // Values : Returns Build Runtime Values
   155  type Values struct {
   156  	Values []Value `json:"results"`
   157  }
   158  
   159  type InputForPost struct {
   160  	Phase  string  `json:"phase"`
   161  	Values []Value `json:"values"`
   162  }
   163  
   164  // *********************************************************************
   165  // ******************************* Funcs *******************************
   166  // *********************************************************************
   167  
   168  // Start : Starts the Build Framework
   169  func (b *Build) Start(phase string, inputValues Values) error {
   170  	if err := b.Connector.GetToken(""); err != nil {
   171  		return err
   172  	}
   173  	inputForPost := InputForPost{
   174  		Phase:  phase,
   175  		Values: inputValues.Values,
   176  	}
   177  	importBody, err := json.Marshal(inputForPost)
   178  	if err != nil {
   179  		return errors.Wrap(err, "Generating Post Request Body failed")
   180  	}
   181  
   182  	body, err := b.Connector.Post("/builds", string(importBody))
   183  	if err != nil {
   184  		return errors.Wrap(err, "Start of build failed: "+string(body))
   185  	}
   186  
   187  	var jBuild jsonBuild
   188  	if err := json.Unmarshal(body, &jBuild); err != nil {
   189  		return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
   190  	}
   191  	b.BuildID = jBuild.Build.BuildID
   192  	b.RunState = jBuild.Build.RunState
   193  	b.ResultState = jBuild.Build.ResultState
   194  	b.Phase = jBuild.Build.Phase
   195  	b.Entitytype = jBuild.Build.Entitytype
   196  	b.Startedby = jBuild.Build.Startedby
   197  	b.StartedAt = jBuild.Build.StartedAt
   198  	b.FinishedAt = jBuild.Build.FinishedAt
   199  	return nil
   200  }
   201  
   202  // Poll : waits for the build framework to be finished
   203  func (b *Build) Poll() error {
   204  	timeout := time.After(b.Connector.MaxRuntime)
   205  	ticker := time.Tick(b.Connector.PollingInterval)
   206  	for {
   207  		select {
   208  		case <-timeout:
   209  			return errors.Errorf("Timed out: (max Runtime %v reached)", b.Connector.MaxRuntime)
   210  		case <-ticker:
   211  			b.Get()
   212  			if !b.IsFinished() {
   213  				log.Entry().Infof("Build is not yet finished, check again in %s", b.Connector.PollingInterval)
   214  			} else {
   215  				return nil
   216  			}
   217  		}
   218  	}
   219  }
   220  
   221  // EvaluteIfBuildSuccessful : Checks the finale state of the build framework
   222  func (b *Build) EvaluteIfBuildSuccessful(treatWarningsAsError bool) error {
   223  	if b.RunState == Failed {
   224  		return errors.Errorf("Build ended with runState failed")
   225  	}
   226  	if treatWarningsAsError && b.ResultState == warning {
   227  		return errors.Errorf("Build ended with resultState warning, setting to failed as configured")
   228  	}
   229  	if (b.ResultState == aborted) || (b.ResultState == erroneous) {
   230  		return errors.Errorf("Build ended with resultState %s", b.ResultState)
   231  	}
   232  	return nil
   233  }
   234  
   235  // Get : Get all Build tasks
   236  func (b *Build) Get() error {
   237  	appendum := "/builds('" + url.QueryEscape(b.BuildID) + "')"
   238  	body, err := b.Connector.Get(appendum)
   239  	if err != nil {
   240  		return err
   241  	}
   242  	var jBuild jsonBuild
   243  	if err := json.Unmarshal(body, &jBuild); err != nil {
   244  		return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
   245  	}
   246  	b.RunState = jBuild.Build.RunState
   247  	b.ResultState = jBuild.Build.ResultState
   248  	b.Phase = jBuild.Build.Phase
   249  	b.Entitytype = jBuild.Build.Entitytype
   250  	b.Startedby = jBuild.Build.Startedby
   251  	b.StartedAt = jBuild.Build.StartedAt
   252  	b.FinishedAt = jBuild.Build.FinishedAt
   253  	return nil
   254  }
   255  
   256  func (b *Build) getTasks() error {
   257  	if len(b.Tasks) == 0 {
   258  		appendum := "/builds('" + url.QueryEscape(b.BuildID) + "')/tasks"
   259  		body, err := b.Connector.Get(appendum)
   260  		if err != nil {
   261  			return err
   262  		}
   263  		b.Tasks, err = unmarshalTasks(body, b.Connector)
   264  		if err != nil {
   265  			return err
   266  		}
   267  		sort.Slice(b.Tasks, func(i, j int) bool {
   268  			return b.Tasks[i].TaskID < b.Tasks[j].TaskID
   269  		})
   270  	}
   271  	return nil
   272  }
   273  
   274  // GetValues : Gets all Build values
   275  func (b *Build) GetValues() error {
   276  	if len(b.Values) == 0 {
   277  		appendum := "/builds('" + url.QueryEscape(b.BuildID) + "')/values"
   278  		body, err := b.Connector.Get(appendum)
   279  		if err != nil {
   280  			return err
   281  		}
   282  		var jValues jsonValues
   283  		if err := json.Unmarshal(body, &jValues); err != nil {
   284  			return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
   285  		}
   286  		b.Values = jValues.ResultValues.Values
   287  		for i := range b.Values {
   288  			b.Values[i].connector = b.Connector
   289  		}
   290  	}
   291  	return nil
   292  }
   293  
   294  func (b *Build) getLogs() error {
   295  	if err := b.getTasks(); err != nil {
   296  		return err
   297  	}
   298  	for i := range b.Tasks {
   299  		if err := b.Tasks[i].getLogs(); err != nil {
   300  			return err
   301  		}
   302  	}
   303  	return nil
   304  }
   305  
   306  // PrintLogs : Returns the Build logs
   307  func (b *Build) PrintLogs() error {
   308  	if err := b.getTasks(); err != nil {
   309  		return err
   310  	}
   311  	for i := range b.Tasks {
   312  		if err := b.Tasks[i].printLogs(); err != nil {
   313  			return err
   314  		}
   315  	}
   316  	return nil
   317  }
   318  
   319  // GetResults : Gets all Build results
   320  func (b *Build) GetResults() error {
   321  	if err := b.getTasks(); err != nil {
   322  		return err
   323  	}
   324  	for i := range b.Tasks {
   325  		if err := b.Tasks[i].getResults(); err != nil {
   326  			return err
   327  		}
   328  	}
   329  	return nil
   330  }
   331  
   332  func (t *task) printLogs() error {
   333  	if err := t.getLogs(); err != nil {
   334  		return err
   335  	}
   336  	for _, logs := range t.Logs {
   337  		logs.print()
   338  	}
   339  	return nil
   340  }
   341  
   342  // GetResult : Returns the last Build artefact created from build step
   343  func (b *Build) GetResult(name string) (*Result, error) {
   344  	var Results []*Result
   345  	var returnResult Result
   346  	if err := b.GetResults(); err != nil {
   347  		return &returnResult, err
   348  	}
   349  	for i_task := range b.Tasks {
   350  		for i_result := range b.Tasks[i_task].Results {
   351  			if b.Tasks[i_task].Results[i_result].Name == name {
   352  				Results = append(Results, &b.Tasks[i_task].Results[i_result])
   353  			}
   354  		}
   355  	}
   356  	switch len(Results) {
   357  	case 0:
   358  		return &returnResult, errors.New("No result named " + name + " was found")
   359  	case 1:
   360  		return Results[0], nil
   361  	default:
   362  		return &returnResult, errors.New("More than one result with the name " + name + " was found")
   363  	}
   364  }
   365  
   366  // IsFinished : Returns Build run state
   367  func (b *Build) IsFinished() bool {
   368  	if b.RunState == Finished || b.RunState == Failed {
   369  		return true
   370  	}
   371  	return false
   372  }
   373  
   374  func (t *task) getLogs() error {
   375  	if len(t.Logs) == 0 {
   376  		appendum := fmt.Sprint("/tasks(build_id='", url.QueryEscape(t.BuildID), "',task_id=", t.TaskID, ")/logs")
   377  		body, err := t.connector.Get(appendum)
   378  		if err != nil {
   379  			return err
   380  		}
   381  		var jLogs jsonLogs
   382  		if err := json.Unmarshal(body, &jLogs); err != nil {
   383  			return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
   384  		}
   385  		t.Logs = jLogs.ResultLogs.Logs
   386  	}
   387  	return nil
   388  }
   389  
   390  func (t *task) getResults() error {
   391  	if len(t.Results) == 0 {
   392  		appendum := fmt.Sprint("/tasks(build_id='", url.QueryEscape(t.BuildID), "',task_id=", t.TaskID, ")/results")
   393  		body, err := t.connector.Get(appendum)
   394  		if err != nil {
   395  			return err
   396  		}
   397  		var jResults jsonResults
   398  		if err := json.Unmarshal(body, &jResults); err != nil {
   399  			return errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
   400  		}
   401  		t.Results = jResults.ResultResults.Results
   402  		for i := range t.Results {
   403  			t.Results[i].connector = t.connector
   404  		}
   405  		if len(t.Results) == 0 {
   406  			//prevent 2nd GET request - no new results will occure...
   407  			t.Results = append(t.Results, Result{Name: dummyResultName})
   408  		}
   409  	}
   410  	return nil
   411  }
   412  
   413  // DownloadAllResults : Downloads all build artefacts, saves it to basePath and the filenames can be modified with the filenamePrefix
   414  func (b *Build) DownloadAllResults(basePath string, filenamePrefix string) error {
   415  	if err := b.GetResults(); err != nil {
   416  		return err
   417  	}
   418  	for i_task := range b.Tasks {
   419  		//in case there was no result, there is only one entry with dummyResultName, obviously we don't want to download this
   420  		if b.Tasks[i_task].Results[0].Name != dummyResultName {
   421  			for i_result := range b.Tasks[i_task].Results {
   422  				if err := b.Tasks[i_task].Results[i_result].DownloadWithFilenamePrefixAndTargetDirectory(basePath, filenamePrefix); err != nil {
   423  					return errors.Wrapf(err, "Error during the download of file %s", b.Tasks[i_task].Results[i_result].Name)
   424  				}
   425  			}
   426  		}
   427  	}
   428  	return nil
   429  }
   430  
   431  // DownloadResults : Download results which are specified in filenames
   432  func (b *Build) DownloadResults(filenames []string, basePath string, filenamePrefix string) error {
   433  	for _, name := range filenames {
   434  		result, err := b.GetResult(name)
   435  		if err != nil {
   436  			log.SetErrorCategory(log.ErrorConfiguration)
   437  			return errors.Wrapf(err, "Problems finding the file %s, please check your config whether this file is really a result file", name)
   438  		}
   439  		if err := result.DownloadWithFilenamePrefixAndTargetDirectory(basePath, filenamePrefix); err != nil {
   440  			return errors.Wrapf(err, "Error during the download of file %s", name)
   441  		}
   442  	}
   443  	return nil
   444  }
   445  
   446  // PublishAllDownloadedResults : publishes all build artefacts which were downloaded before
   447  func (b *Build) PublishAllDownloadedResults(stepname string, utils piperutils.FileUtils) {
   448  	var filesToPublish []piperutils.Path
   449  	for i_task := range b.Tasks {
   450  		for i_result := range b.Tasks[i_task].Results {
   451  			if b.Tasks[i_task].Results[i_result].wasDownloaded() {
   452  				filesToPublish = append(filesToPublish, piperutils.Path{Target: b.Tasks[i_task].Results[i_result].DownloadPath,
   453  					Name: b.Tasks[i_task].Results[i_result].SavedFilename, Mandatory: true})
   454  			}
   455  		}
   456  	}
   457  	if len(filesToPublish) > 0 {
   458  		if err := piperutils.PersistReportsAndLinks(stepname, "", utils, filesToPublish, nil); err != nil {
   459  			log.Entry().WithError(err).Error("failed to persist reports")
   460  		}
   461  	}
   462  }
   463  
   464  // PublishDownloadedResults : Publishes build artefacts specified in filenames
   465  func (b *Build) PublishDownloadedResults(stepname string, filenames []string, utils piperutils.FileUtils) error {
   466  	var filesToPublish []piperutils.Path
   467  	for i := range filenames {
   468  		result, err := b.GetResult(filenames[i])
   469  		if err != nil {
   470  			log.SetErrorCategory(log.ErrorConfiguration)
   471  			return errors.Wrapf(err, "Problems finding the file %s, please check your config whether this file is really a result file", filenames[i])
   472  		}
   473  		if result.wasDownloaded() {
   474  			filesToPublish = append(filesToPublish, piperutils.Path{Target: result.DownloadPath, Name: result.SavedFilename, Mandatory: true})
   475  		} else {
   476  			log.SetErrorCategory(log.ErrorConfiguration)
   477  			return errors.Errorf("Trying to publish the file %s which was not downloaded", result.Name)
   478  		}
   479  	}
   480  	if len(filesToPublish) > 0 {
   481  		if err := piperutils.PersistReportsAndLinks(stepname, "", utils, filesToPublish, nil); err != nil {
   482  			log.Entry().WithError(err).Error("failed to persist reports")
   483  		}
   484  	}
   485  	return nil
   486  }
   487  
   488  // Download : Provides the atrefact of build step
   489  func (result *Result) Download(downloadPath string) error {
   490  	appendum := fmt.Sprint("/results(build_id='", url.QueryEscape(result.BuildID), "',task_id=", result.TaskID, ",name='", url.QueryEscape(result.Name), "')/$value")
   491  	err := result.connector.Download(appendum, downloadPath)
   492  	return err
   493  }
   494  
   495  // DownloadWithFilenamePrefixAndTargetDirectory : downloads build artefact, saves it to basePath and the filename can be modified with the filenamePrefix
   496  func (result *Result) DownloadWithFilenamePrefixAndTargetDirectory(basePath string, filenamePrefix string) error {
   497  	basePath, err := result.resolveParamter(basePath)
   498  	if err != nil {
   499  		return errors.Wrapf(err, "Could not resolve parameter %s for the target directory", basePath)
   500  	}
   501  	filenamePrefix, err = result.resolveParamter(filenamePrefix)
   502  	if err != nil {
   503  		return errors.Wrapf(err, "Could not resolve parameter %s for the filename prefix", filenamePrefix)
   504  	}
   505  	appendum := fmt.Sprint("/results(build_id='", result.BuildID, "',task_id=", result.TaskID, ",name='", result.Name, "')/$value")
   506  	filename := filenamePrefix + result.Name
   507  	downloadPath := filepath.Join(path.Base(basePath), path.Base(filename))
   508  	if err := result.connector.Download(appendum, downloadPath); err != nil {
   509  		log.SetErrorCategory(log.ErrorInfrastructure)
   510  		return errors.Wrapf(err, "Could not download %s", result.Name)
   511  	}
   512  	result.SavedFilename = filename
   513  	result.DownloadPath = downloadPath
   514  	log.Entry().Infof("Saved file %s as %s to %s", result.Name, result.SavedFilename, result.DownloadPath)
   515  	return nil
   516  }
   517  
   518  func (result *Result) resolveParamter(parameter string) (string, error) {
   519  	if len(parameter) == 0 {
   520  		return parameter, nil
   521  	}
   522  	if (string(parameter[0]) == "{") && string(parameter[len(parameter)-1]) == "}" {
   523  		trimmedParam := strings.ToLower(parameter[1 : len(parameter)-1])
   524  		switch trimmedParam {
   525  		case "buildid":
   526  			return result.BuildID, nil
   527  		case "taskid":
   528  			return strconv.Itoa(result.TaskID), nil
   529  		default:
   530  			log.SetErrorCategory(log.ErrorConfiguration)
   531  			return "", errors.Errorf("Unknown parameter %s", parameter)
   532  		}
   533  	} else {
   534  		return parameter, nil
   535  	}
   536  }
   537  
   538  func (result *Result) wasDownloaded() bool {
   539  	if len(result.DownloadPath) > 0 && len(result.SavedFilename) > 0 {
   540  		return true
   541  	} else {
   542  		return false
   543  	}
   544  }
   545  
   546  func (logging *logStruct) print() {
   547  	switch logging.Msgty {
   548  	case loginfo:
   549  		log.Entry().WithField("Timestamp", logging.Timestamp).Info(logging.Logline)
   550  	case logwarning:
   551  		log.Entry().WithField("Timestamp", logging.Timestamp).Warn(logging.Logline)
   552  	case logerror:
   553  		log.Entry().WithField("Timestamp", logging.Timestamp).Error(logging.Logline)
   554  	case logaborted:
   555  		log.Entry().WithField("Timestamp", logging.Timestamp).Error(logging.Logline)
   556  	default:
   557  	}
   558  }
   559  
   560  // ******** unmarshal function  ************
   561  func unmarshalTasks(body []byte, connector Connector) ([]task, error) {
   562  
   563  	var tasks []task
   564  	var append_task task
   565  	var jTasks jsonTasks
   566  	if err := json.Unmarshal(body, &jTasks); err != nil {
   567  		return tasks, errors.Wrap(err, "Unexpected buildFrameWork response: "+string(body))
   568  	}
   569  	for _, jTask := range jTasks.ResultTasks.Tasks {
   570  		append_task.connector = connector
   571  		append_task.BuildID = jTask.BuildID
   572  		append_task.TaskID = jTask.TaskID
   573  		append_task.LogID = jTask.LogID
   574  		append_task.PluginClass = jTask.PluginClass
   575  		append_task.StartedAt = jTask.StartedAt
   576  		append_task.FinishedAt = jTask.FinishedAt
   577  		append_task.ResultState = jTask.ResultState
   578  		tasks = append(tasks, append_task)
   579  	}
   580  	return tasks, nil
   581  }