package orchestrator import ( "encoding/json" "io/ioutil" "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 options piperHttp.ClientOptions apiInformation map[string]interface{} } // InitOrchestratorProvider initializes the Jenkins orchestrator with credentials func (j *JenkinsConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) { j.client = piperHttp.Client{} j.options = piperHttp.ClientOptions{ Username: settings.JenkinsUser, Password: settings.JenkinsToken, MaxRetries: 3, TransportTimeout: time.Second * 10, } j.client.SetOptions(j.options) log.Entry().Debug("Successfully initialized Jenkins config provider") } // 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.GetBuildURL() + "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") } } // GetBuildStatus returns build status of the current job func (j *JenkinsConfigProvider) GetBuildStatus() 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 "SUCCESS" case "ABORTED": return "ABORTED" default: // FAILURE, NOT_BUILT return "FAILURE" } } return "FAILURE" } // GetChangeSet returns the commitIds and timestamp of the changeSet of the current run func (j *JenkinsConfigProvider) GetChangeSet() []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 } // GetLog returns the logfile from the current job as byte object func (j *JenkinsConfigProvider) GetLog() ([]byte, error) { URL := j.GetBuildURL() + "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 := ioutil.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 } // GetPipelineStartTime returns the pipeline start time in UTC func (j *JenkinsConfigProvider) GetPipelineStartTime() time.Time { URL := j.GetBuildURL() + "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() } // GetJobName returns the job name of the current job e.g. foo/bar/BRANCH func (j *JenkinsConfigProvider) GetJobName() string { return getEnv("JOB_NAME", "n/a") } // GetJobURL returns the current job URL e.g. https://jaas.url/job/foo/job/bar/job/main func (j *JenkinsConfigProvider) GetJobURL() 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") } // GetBuildID returns the build ID of the current job, e.g. 1234 func (j *JenkinsConfigProvider) GetBuildID() string { return getEnv("BUILD_ID", "n/a") } // GetStageName returns the stage name the job is currently in, e.g. Promote func (j *JenkinsConfigProvider) GetStageName() string { return getEnv("STAGE_NAME", "n/a") } // GetBuildReason returns the build reason of the current build func (j *JenkinsConfigProvider) GetBuildReason() 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 "Unknown" } jsonParsed, err := gabs.ParseJSON(marshal) if err != nil { log.Entry().WithError(err).Debugf("could not parse apiInformation") return "Unknown" } 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 "Manual" } else if subclass.Data().(string) == "hudson.triggers.TimerTrigger$TimerTriggerCause" { return "Schedule" } else if subclass.Data().(string) == "jenkins.branch.BranchEventCause" { return "PullRequest" } else if subclass.Data().(string) == "org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause" { return "ResourceTrigger" } else { return "Unknown" } } } } return "Unknown" } // GetBranch returns the branch name, only works with the git plugin enabled func (j *JenkinsConfigProvider) GetBranch() string { return getEnv("BRANCH_NAME", "n/a") } // GetReference returns the git reference, only works with the git plugin enabled func (j *JenkinsConfigProvider) GetReference() 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 } } // GetBuildURL returns the build url, e.g. https://jaas.url/job/foo/job/bar/job/main/1234/ func (j *JenkinsConfigProvider) GetBuildURL() string { return getEnv("BUILD_URL", "n/a") } // GetCommit returns the commit SHA from the current build, only works with the git plugin enabled func (j *JenkinsConfigProvider) GetCommit() string { return getEnv("GIT_COMMIT", "n/a") } // GetRepoURL returns the repo URL of the current build, only works with the git plugin enabled func (j *JenkinsConfigProvider) GetRepoURL() string { return getEnv("GIT_URL", "n/a") } // GetPullRequestConfig returns the pull request config func (j *JenkinsConfigProvider) GetPullRequestConfig() 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 truthy("CHANGE_ID") } func isJenkins() bool { envVars := []string{"JENKINS_HOME", "JENKINS_URL"} return areIndicatingEnvVarsSet(envVars) }