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 }