1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-03-03 15:02:35 +02:00
Googlom ac5cf17317
refactor(orchestrator): Use singleton in orchestrator package and rename methods (#4639)
* rename interface, types and methods.
some type changes and refactor

* update dependent methods and variables

* fix unit tests

* a bit more refactor and fix

* concurrent safe singleton

* return old Options struct

* refactor creating config provider and fix nil pointer derefernce

* fix unit test and linter errors

* introduce resetting config provider (for unit tests)

* fix annoying error message when config provider is not configured

---------

Co-authored-by: Gulom Alimov <gulomjon.alimov@sap.com>
Co-authored-by: Muhammadali Nazarov <muhammadalinazarov@gmail.com>
2024-01-09 16:01:15 +05:00

290 lines
9.2 KiB
Go

package orchestrator
import (
"encoding/json"
"io"
"strings"
"time"
"github.com/Jeffail/gabs/v2"
piperHttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
)
type jenkinsConfigProvider struct {
client piperHttp.Client
apiInformation map[string]interface{}
}
func newJenkinsConfigProvider() *jenkinsConfigProvider {
return &jenkinsConfigProvider{}
}
// Configure initializes the Jenkins orchestrator with credentials
func (j *jenkinsConfigProvider) Configure(opts *Options) error {
j.client.SetOptions(piperHttp.ClientOptions{
Username: opts.JenkinsUser,
Password: opts.JenkinsToken,
MaxRetries: 3,
TransportTimeout: time.Second * 10,
})
log.Entry().Debug("Successfully initialized Jenkins config provider")
return nil
}
// OrchestratorVersion returns the orchestrator version currently running on
func (j *jenkinsConfigProvider) OrchestratorVersion() string {
return getEnv("JENKINS_VERSION", "n/a")
}
// OrchestratorType returns the orchestrator type Jenkins
func (j *jenkinsConfigProvider) OrchestratorType() string {
return "Jenkins"
}
func (j *jenkinsConfigProvider) fetchAPIInformation() {
if len(j.apiInformation) == 0 {
log.Entry().Debugf("apiInformation is empty, getting infos from API")
URL := j.BuildURL() + "api/json"
log.Entry().Debugf("API URL: %s", URL)
response, err := j.client.GetRequest(URL, nil, nil)
if err != nil {
log.Entry().WithError(err).Error("could not get API information from Jenkins")
j.apiInformation = map[string]interface{}{}
return
}
if response.StatusCode != 200 { //http.StatusNoContent
log.Entry().Errorf("Response-Code is %v, could not get timestamp from Jenkins. Setting timestamp to 1970.", response.StatusCode)
j.apiInformation = map[string]interface{}{}
return
}
err = piperHttp.ParseHTTPResponseBodyJSON(response, &j.apiInformation)
if err != nil {
log.Entry().WithError(err).Errorf("could not parse HTTP response body")
j.apiInformation = map[string]interface{}{}
return
}
log.Entry().Debugf("successfully retrieved apiInformation")
} else {
log.Entry().Debugf("apiInformation already set")
}
}
// BuildStatus returns build status of the current job
func (j *jenkinsConfigProvider) BuildStatus() string {
j.fetchAPIInformation()
if val, ok := j.apiInformation["result"]; ok {
// cases in ADO: succeeded, failed, canceled, none, partiallySucceeded
switch result := val; result {
case "SUCCESS":
return BuildStatusSuccess
case "ABORTED":
return BuildStatusAborted
default:
// FAILURE, NOT_BUILT
return BuildStatusFailure
}
}
return BuildStatusFailure
}
// ChangeSet returns the commitIds and timestamp of the changeSet of the current run
func (j *jenkinsConfigProvider) ChangeSets() []ChangeSet {
j.fetchAPIInformation()
marshal, err := json.Marshal(j.apiInformation)
if err != nil {
log.Entry().WithError(err).Debugf("could not marshal apiInformation")
return []ChangeSet{}
}
jsonParsed, err := gabs.ParseJSON(marshal)
if err != nil {
log.Entry().WithError(err).Debugf("could not parse apiInformation")
return []ChangeSet{}
}
var changeSetList []ChangeSet
for _, child := range jsonParsed.Path("changeSets").Children() {
if child.Path("kind").Data().(string) == "git" {
for _, item := range child.S("items").Children() {
tmpChangeSet := ChangeSet{
CommitId: item.Path("commitId").Data().(string),
Timestamp: item.Path("timestamp").String(),
}
changeSetList = append(changeSetList, tmpChangeSet)
}
}
}
return changeSetList
}
// FullLogs returns the logfile from the current job as byte object
func (j *jenkinsConfigProvider) FullLogs() ([]byte, error) {
URL := j.BuildURL() + "consoleText"
response, err := j.client.GetRequest(URL, nil, nil)
if err != nil {
return []byte{}, errors.Wrapf(err, "could not GET Jenkins log file %v", err)
} else if response.StatusCode != 200 {
log.Entry().Error("response code !=200 could not get log information from Jenkins, returning with empty log.")
return []byte{}, nil
}
logFile, err := io.ReadAll(response.Body)
if err != nil {
return []byte{}, errors.Wrapf(err, "could not read Jenkins log file from request %v", err)
}
defer response.Body.Close()
return logFile, nil
}
// PipelineStartTime returns the pipeline start time in UTC
func (j *jenkinsConfigProvider) PipelineStartTime() time.Time {
URL := j.BuildURL() + "api/json"
response, err := j.client.GetRequest(URL, nil, nil)
if err != nil {
log.Entry().WithError(err).Errorf("could not getRequest to URL %s", URL)
return time.Time{}.UTC()
}
if response.StatusCode != 200 { //http.StatusNoContent -> also empty log!
log.Entry().Errorf("response code is %v . \n Could not get timestamp from Jenkins. Setting timestamp to 1970.", response.StatusCode)
return time.Time{}.UTC()
}
var responseInterface map[string]interface{}
err = piperHttp.ParseHTTPResponseBodyJSON(response, &responseInterface)
if err != nil {
log.Entry().WithError(err).Infof("could not parse http response, returning 1970")
return time.Time{}.UTC()
}
rawTimeStamp := responseInterface["timestamp"].(float64)
timeStamp := time.Unix(int64(rawTimeStamp)/1000, 0)
log.Entry().Debugf("Pipeline start time: %v", timeStamp.String())
defer response.Body.Close()
return timeStamp.UTC()
}
// JobName returns the job name of the current job e.g. foo/bar/BRANCH
func (j *jenkinsConfigProvider) JobName() string {
return getEnv("JOB_NAME", "n/a")
}
// JobURL returns the current job URL e.g. https://jaas.url/job/foo/job/bar/job/main
func (j *jenkinsConfigProvider) JobURL() string {
return getEnv("JOB_URL", "n/a")
}
// getJenkinsHome returns the jenkins home e.g. /var/lib/jenkins
func (j *jenkinsConfigProvider) getJenkinsHome() string {
return getEnv("JENKINS_HOME", "n/a")
}
// BuildID returns the build ID of the current job, e.g. 1234
func (j *jenkinsConfigProvider) BuildID() string {
return getEnv("BUILD_ID", "n/a")
}
// StageName returns the stage name the job is currently in, e.g. Promote
func (j *jenkinsConfigProvider) StageName() string {
return getEnv("STAGE_NAME", "n/a")
}
// BuildReason returns the build reason of the current build
func (j *jenkinsConfigProvider) BuildReason() string {
// BuildReasons are unified with AzureDevOps build reasons,see
// https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
// ResourceTrigger, PullRequest, Manual, IndividualCI, Schedule
j.fetchAPIInformation()
marshal, err := json.Marshal(j.apiInformation)
if err != nil {
log.Entry().WithError(err).Debugf("could not marshal apiInformation")
return BuildReasonUnknown
}
jsonParsed, err := gabs.ParseJSON(marshal)
if err != nil {
log.Entry().WithError(err).Debugf("could not parse apiInformation")
return BuildReasonUnknown
}
for _, child := range jsonParsed.Path("actions").Children() {
class := child.S("_class")
if class == nil {
continue
}
if class.Data().(string) == "hudson.model.CauseAction" {
for _, val := range child.Path("causes").Children() {
subclass := val.S("_class")
if subclass.Data().(string) == "hudson.model.Cause$UserIdCause" {
return BuildReasonManual
} else if subclass.Data().(string) == "hudson.triggers.TimerTrigger$TimerTriggerCause" {
return BuildReasonSchedule
} else if subclass.Data().(string) == "jenkins.branch.BranchEventCause" {
return BuildReasonPullRequest
} else if subclass.Data().(string) == "org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause" {
return BuildReasonResourceTrigger
} else {
return BuildReasonUnknown
}
}
}
}
return BuildReasonUnknown
}
// Branch returns the branch name, only works with the git plugin enabled
func (j *jenkinsConfigProvider) Branch() string {
return getEnv("BRANCH_NAME", "n/a")
}
// GitReference returns the git reference, only works with the git plugin enabled
func (j *jenkinsConfigProvider) GitReference() string {
ref := getEnv("BRANCH_NAME", "n/a")
if ref == "n/a" {
return ref
} else if strings.Contains(ref, "PR") {
return "refs/pull/" + strings.Split(ref, "-")[1] + "/head"
} else {
return "refs/heads/" + ref
}
}
// BuildURL returns the build url, e.g. https://jaas.url/job/foo/job/bar/job/main/1234/
func (j *jenkinsConfigProvider) BuildURL() string {
return getEnv("BUILD_URL", "n/a")
}
// CommitSHA returns the commit SHA from the current build, only works with the git plugin enabled
func (j *jenkinsConfigProvider) CommitSHA() string {
return getEnv("GIT_COMMIT", "n/a")
}
// RepoURL returns the repo URL of the current build, only works with the git plugin enabled
func (j *jenkinsConfigProvider) RepoURL() string {
return getEnv("GIT_URL", "n/a")
}
// PullRequestConfig returns the pull request config
func (j *jenkinsConfigProvider) PullRequestConfig() PullRequestConfig {
return PullRequestConfig{
Branch: getEnv("CHANGE_BRANCH", "n/a"),
Base: getEnv("CHANGE_TARGET", "n/a"),
Key: getEnv("CHANGE_ID", "n/a"),
}
}
// IsPullRequest returns boolean indicating if current job is a PR
func (j *jenkinsConfigProvider) IsPullRequest() bool {
return envVarIsTrue("CHANGE_ID")
}
func isJenkins() bool {
envVars := []string{"JENKINS_HOME", "JENKINS_URL"}
return envVarsAreSet(envVars)
}