You've already forked sap-jenkins-library
							
							
				mirror of
				https://github.com/SAP/jenkins-library.git
				synced 2025-10-30 23:57:50 +02:00 
			
		
		
		
	fix(orchestrator) usage of correct env variables (#3650)
* Reorders getApiInformation, changes variables to get start time, adjusts and adds test cases * Changes the way to get apiInformation and reduces number of requests * Changes getting pipeline start time from correct env variable * Refactors getApiInformation functionality * Adds GetBuildReason() for Azure and Jenkins * Updates JobURL for ADO
This commit is contained in:
		| @@ -202,7 +202,7 @@ func addRootFlags(rootCmd *cobra.Command) { | ||||
| 		provider = &orchestrator.UnknownOrchestratorConfigProvider{} | ||||
| 	} | ||||
|  | ||||
| 	rootCmd.PersistentFlags().StringVar(&GeneralConfig.CorrelationID, "correlationID", provider.GetBuildUrl(), "ID for unique identification of a pipeline run") | ||||
| 	rootCmd.PersistentFlags().StringVar(&GeneralConfig.CorrelationID, "correlationID", provider.GetBuildURL(), "ID for unique identification of a pipeline run") | ||||
| 	rootCmd.PersistentFlags().StringVar(&GeneralConfig.CustomConfig, "customConfig", ".pipeline/config.yml", "Path to the pipeline configuration file") | ||||
| 	rootCmd.PersistentFlags().StringSliceVar(&GeneralConfig.GitHubTokens, "gitHubTokens", AccessTokensFromEnvJSON(os.Getenv("PIPER_gitHubTokens")), "List of entries in form of <hostname>:<token> to allow GitHub token authentication for downloading config / defaults") | ||||
| 	rootCmd.PersistentFlags().StringSliceVar(&GeneralConfig.DefaultConfig, "defaultConfig", []string{".pipeline/defaults.yaml"}, "Default configurations, passed as path to yaml file") | ||||
|   | ||||
| @@ -67,7 +67,7 @@ type ClientOptions struct { | ||||
| 	TrustedCerts              []string | ||||
| } | ||||
|  | ||||
| // TransportWrapper is a wrapper for central roundtrip capabilities | ||||
| // TransportWrapper is a wrapper for central round trip capabilities | ||||
| type TransportWrapper struct { | ||||
| 	Transport                http.RoundTripper | ||||
| 	doLogRequestBodyOnDebug  bool | ||||
| @@ -206,7 +206,7 @@ func (c *Client) Upload(data UploadRequestData) (*http.Response, error) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SendRequest sends an http request with a defined method | ||||
| // SendRequest sends a http request with a defined method | ||||
| // | ||||
| // On error, any Response can be ignored and the Response.Body | ||||
| // does not need to be closed. | ||||
| @@ -219,7 +219,7 @@ func (c *Client) SendRequest(method, url string, body io.Reader, header http.Hea | ||||
| 	return c.Send(request) | ||||
| } | ||||
|  | ||||
| // Send sends an http request | ||||
| // Send sends a http request | ||||
| func (c *Client) Send(request *http.Request) (*http.Response, error) { | ||||
| 	httpClient := c.initialize() | ||||
| 	response, err := httpClient.Do(request) | ||||
| @@ -355,7 +355,7 @@ var contextKeyRequestStart = &contextKey{"RequestStart"} | ||||
| var authHeaderKey = "Authorization" | ||||
|  | ||||
| // RoundTrip is the core part of this module and implements http.RoundTripper. | ||||
| // Executes HTTP request with request/response logging. | ||||
| // Executes HTTP requests with request/response logging. | ||||
| func (t *TransportWrapper) RoundTrip(req *http.Request) (*http.Response, error) { | ||||
| 	ctx := context.WithValue(req.Context(), contextKeyRequestStart, time.Now()) | ||||
| 	req = req.WithContext(ctx) | ||||
| @@ -372,7 +372,7 @@ func (t *TransportWrapper) RoundTrip(req *http.Request) (*http.Response, error) | ||||
| } | ||||
|  | ||||
| func handleAuthentication(req *http.Request, username, password, token string) { | ||||
| 	// Handle authenticaion if not done already | ||||
| 	// Handle authentication if not done already | ||||
| 	if (len(username) > 0 || len(password) > 0) && len(req.Header.Get(authHeaderKey)) == 0 { | ||||
| 		req.SetBasicAuth(username, password) | ||||
| 		log.Entry().Debug("Using Basic Authentication ****/****") | ||||
| @@ -432,9 +432,9 @@ func transformHeaders(header http.Header) http.Header { | ||||
| 			// Since | ||||
| 			//   1.) The auth header type itself might serve as a vector for an | ||||
| 			//       intrusion | ||||
| 			//   2.) We cannot make assumtions about the structure of the auth | ||||
| 			//   2.) We cannot make assumptions about the structure of the auth | ||||
| 			//       header value since that depends on the type, e.g. several tokens | ||||
| 			//       where only some of the tokens define the secret | ||||
| 			//       where only some tokens define the secret | ||||
| 			// we hide the full auth header value anyway in order to be on the | ||||
| 			// save side. | ||||
| 			value = []string{"<set>"} | ||||
| @@ -623,7 +623,7 @@ func (c *Client) configureTLSToTrustCertificates(transport *TransportWrapper) er | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // default truststore location | ||||
| // TrustStoreDirectory default truststore location | ||||
| const TrustStoreDirectory = ".pipeline/trustStore" | ||||
|  | ||||
| func getWorkingDirForTrustStore() (string, error) { | ||||
| @@ -637,7 +637,7 @@ func getWorkingDirForTrustStore() (string, error) { | ||||
| 	return TrustStoreDirectory, nil | ||||
| } | ||||
|  | ||||
| // ParseHTTPResponseBodyXML parses a XML http response into a given interface | ||||
| // ParseHTTPResponseBodyXML parses an XML http response into a given interface | ||||
| func ParseHTTPResponseBodyXML(resp *http.Response, response interface{}) error { | ||||
| 	if resp == nil { | ||||
| 		return errors.Errorf("cannot parse HTTP response with value <nil>") | ||||
|   | ||||
| @@ -1,22 +1,21 @@ | ||||
| package orchestrator | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	piperHttp "github.com/SAP/jenkins-library/pkg/http" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type AzureDevOpsConfigProvider struct { | ||||
| 	client  piperHttp.Client | ||||
| 	options piperHttp.ClientOptions | ||||
| 	client         piperHttp.Client | ||||
| 	options        piperHttp.ClientOptions | ||||
| 	apiInformation map[string]interface{} | ||||
| } | ||||
|  | ||||
| //InitOrchestratorProvider initializes http client for AzureDevopsConfigProvider | ||||
| // InitOrchestratorProvider initializes http client for AzureDevopsConfigProvider | ||||
| func (a *AzureDevOpsConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) { | ||||
| 	a.client = piperHttp.Client{} | ||||
| 	a.options = piperHttp.ClientOptions{ | ||||
| @@ -29,6 +28,58 @@ func (a *AzureDevOpsConfigProvider) InitOrchestratorProvider(settings *Orchestra | ||||
| 	log.Entry().Debug("Successfully initialized Azure config provider") | ||||
| } | ||||
|  | ||||
| // fetchAPIInformation fetches Azure API information of current build | ||||
| func (a *AzureDevOpsConfigProvider) fetchAPIInformation() { | ||||
| 	// if apiInformation is empty fill it otherwise do nothing | ||||
| 	if len(a.apiInformation) == 0 { | ||||
| 		log.Entry().Debugf("apiInformation is empty, getting infos from API") | ||||
| 		URL := a.getSystemCollectionURI() + a.getTeamProjectID() + "/_apis/build/builds/" + a.getAzureBuildID() + "/" | ||||
| 		log.Entry().Debugf("API URL: %s", URL) | ||||
| 		response, err := a.client.GetRequest(URL, nil, nil) | ||||
| 		if err != nil { | ||||
| 			log.Entry().Error("failed to get HTTP response, returning empty API information", err) | ||||
| 			a.apiInformation = map[string]interface{}{} | ||||
| 			return | ||||
| 		} else if response.StatusCode != 200 { //http.StatusNoContent | ||||
| 			log.Entry().Errorf("response code is %v, could not get API information from AzureDevOps. Returning with empty interface.", response.StatusCode) | ||||
| 			a.apiInformation = map[string]interface{}{} | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		err = piperHttp.ParseHTTPResponseBodyJSON(response, &a.apiInformation) | ||||
| 		if err != nil { | ||||
| 			log.Entry().Error("failed to parse HTTP response, returning with empty interface", err) | ||||
| 			a.apiInformation = map[string]interface{}{} | ||||
| 			return | ||||
| 		} | ||||
| 		log.Entry().Debugf("successfully retrieved apiInformation") | ||||
| 	} else { | ||||
| 		log.Entry().Debugf("apiInformation already set") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getSystemCollectionURI returns the URI of the TFS collection or Azure DevOps organization e.g. https://dev.azure.com/fabrikamfiber/ | ||||
| func (a *AzureDevOpsConfigProvider) getSystemCollectionURI() string { | ||||
| 	return getEnv("SYSTEM_COLLECTIONURI", "n/a") | ||||
| } | ||||
|  | ||||
| // getTeamProjectID is the name of the project that contains this build e.g. 123a4567-ab1c-12a1-1234-123456ab7890 | ||||
| func (a *AzureDevOpsConfigProvider) getTeamProjectID() string { | ||||
| 	return getEnv("SYSTEM_TEAMPROJECTID", "n/a") | ||||
| } | ||||
|  | ||||
| // getAzureBuildID returns the id of the build, e.g. 1234 | ||||
| func (a *AzureDevOpsConfigProvider) getAzureBuildID() string { | ||||
| 	// INFO: Private function only used for API requests, buildId for e.g. reporting | ||||
| 	// is GetBuildNumber to align with the UI of ADO | ||||
| 	return getEnv("BUILD_BUILDID", "n/a") | ||||
| } | ||||
|  | ||||
| // GetJobName returns the pipeline job name, currently org/repo | ||||
| func (a *AzureDevOpsConfigProvider) GetJobName() string { | ||||
| 	return getEnv("BUILD_REPOSITORY_NAME", "n/a") | ||||
| } | ||||
|  | ||||
| // OrchestratorVersion returns the agent version on ADO | ||||
| func (a *AzureDevOpsConfigProvider) OrchestratorVersion() string { | ||||
| 	return getEnv("AGENT_VERSION", "n/a") | ||||
| @@ -39,145 +90,134 @@ func (a *AzureDevOpsConfigProvider) OrchestratorType() string { | ||||
| 	return "Azure" | ||||
| } | ||||
|  | ||||
| //GetBuildStatus returns status of the build. Return variables are aligned with Jenkins build statuses. | ||||
| func (a *AzureDevOpsConfigProvider) GetBuildStatus() string { | ||||
| 	responseInterface := a.getAPIInformation() | ||||
| 	if _, ok := responseInterface["result"]; ok { | ||||
| 		// cases in Jenkins: SUCCESS, FAILURE, NOT_BUILD, ABORTED | ||||
| 		switch result := responseInterface["result"]; result { | ||||
| 		case "SUCCESS": | ||||
| 			return "SUCCESS" | ||||
| 		case "ABORTED": | ||||
| 			return "ABORTED" | ||||
| 		default: | ||||
| 			// FAILURE, NOT_BUILT | ||||
| 			return "FAILURE" | ||||
| 		} | ||||
| 	// cases to align with Jenkins: SUCCESS, FAILURE, NOT_BUILD, ABORTED | ||||
| 	switch buildStatus := getEnv("AGENT_JOBSTATUS", "FAILURE"); buildStatus { | ||||
| 	case "Succeeded": | ||||
| 		return "SUCCESS" | ||||
| 	case "Canceled": | ||||
| 		return "ABORTED" | ||||
| 	default: | ||||
| 		// Failed, SucceededWithIssues | ||||
| 		return "FAILURE" | ||||
| 	} | ||||
| 	return "FAILURE" | ||||
| } | ||||
|  | ||||
| func (a *AzureDevOpsConfigProvider) getAPIInformation() map[string]interface{} { | ||||
| 	URL := a.GetSystemCollectionURI() + a.GetTeamProjectId() + "/_apis/build/builds/" + a.GetBuildId() + "/" | ||||
| 	response, err := a.client.GetRequest(URL, nil, nil) | ||||
| 	if err != nil { | ||||
| 		log.Entry().Error("failed to get http response, using default values", err) | ||||
| 		return map[string]interface{}{} | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != 200 { //http.StatusNoContent | ||||
| 		log.Entry().Errorf("Response-Code is %v . \n Could not get API information from AzureDevOps. Returning with empty interface.", response.StatusCode) | ||||
| 		return map[string]interface{}{} | ||||
| 	} | ||||
| 	var responseInterface map[string]interface{} | ||||
| 	err = piperHttp.ParseHTTPResponseBodyJSON(response, &responseInterface) | ||||
| 	if err != nil { | ||||
| 		log.Entry().Error("failed to parse http response, returning with empty interface", err) | ||||
| 		return map[string]interface{}{} | ||||
| 	} | ||||
| 	return responseInterface | ||||
| } | ||||
|  | ||||
| // GetJobName returns the pipeline job name | ||||
| func (a *AzureDevOpsConfigProvider) GetJobName() string { | ||||
| 	responseInterface := a.getAPIInformation() | ||||
| 	if val, ok := responseInterface["repository"]; ok { | ||||
| 		return val.(map[string]interface{})["id"].(string) | ||||
| 	} | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| // GetLog returns the logfile of the pipeline run so far | ||||
| // GetLog returns the whole logfile for the current pipeline run | ||||
| func (a *AzureDevOpsConfigProvider) GetLog() ([]byte, error) { | ||||
| 	// ToDo: How to get step specific logs, not only whole log? | ||||
| 	URL := a.GetSystemCollectionURI() + a.GetTeamProjectId() + "/_apis/build/builds/" + a.GetBuildId() + "/logs" | ||||
| 	URL := a.getSystemCollectionURI() + a.getTeamProjectID() + "/_apis/build/builds/" + a.getAzureBuildID() + "/logs" | ||||
|  | ||||
| 	response, err := a.client.GetRequest(URL, nil, nil) | ||||
| 	logs := []byte{} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		log.Entry().Error("failed to get http response", err) | ||||
| 		return logs, nil | ||||
| 		log.Entry().Error("failed to get HTTP response: ", err) | ||||
| 		return []byte{}, err | ||||
| 	} | ||||
| 	if response.StatusCode != 200 { //http.StatusNoContent -> also empty log! | ||||
| 		log.Entry().Errorf("Response-Code is %v . \n Could not get log information from AzureDevOps. Returning with empty log.", response.StatusCode) | ||||
| 		return logs, nil | ||||
| 		log.Entry().Errorf("response-Code is %v, could not get log information from AzureDevOps, returning with empty log.", response.StatusCode) | ||||
| 		return []byte{}, nil | ||||
| 	} | ||||
| 	var responseInterface map[string]interface{} | ||||
| 	err = piperHttp.ParseHTTPResponseBodyJSON(response, &responseInterface) | ||||
| 	if err != nil { | ||||
| 		log.Entry().Error("failed to parse http response", err) | ||||
| 		return logs, nil | ||||
| 		log.Entry().Error("failed to parse http response: ", err) | ||||
| 		return []byte{}, err | ||||
| 	} | ||||
| 	// check if response interface is empty or non-existent | ||||
| 	logCount := int(responseInterface["count"].(float64)) | ||||
|  | ||||
| 	var logCount int | ||||
| 	if val, ok := responseInterface["count"]; ok { | ||||
| 		logCount = int(val.(float64)) | ||||
| 	} else { | ||||
| 		log.Entry().Error("log count variable not found, returning empty log") | ||||
| 		return []byte{}, err | ||||
| 	} | ||||
| 	var logs []byte | ||||
| 	for i := 1; i <= logCount; i++ { | ||||
| 		counter := strconv.Itoa(i) | ||||
| 		logURL := URL + "/" + counter | ||||
| 		fmt.Println("logURL: ", logURL) | ||||
| 		log.Entry().Debugf("Getting log no.: %d  from %v", i, logURL) | ||||
| 		response, err := a.client.GetRequest(logURL, nil, nil) | ||||
| 		if err != nil { | ||||
| 			fmt.Println(err) | ||||
| 			log.Entry().Error("failed to get log", err) | ||||
| 			return []byte{}, err | ||||
| 		} | ||||
| 		if response.StatusCode != 200 { //http.StatusNoContent -> also empty log! | ||||
| 			log.Entry().Errorf("response code is %v, could not get log information from AzureDevOps ", response.StatusCode) | ||||
| 			return []byte{}, err | ||||
| 		} | ||||
| 		content, err := ioutil.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			log.Entry().Error("failed to parse http response", err) | ||||
| 			return []byte{}, err | ||||
| 		} | ||||
| 		logs = append(logs, content...) | ||||
| 	} | ||||
|  | ||||
| 	return logs, nil | ||||
| } | ||||
|  | ||||
| // GetPipelineStartTime returns the pipeline start time | ||||
| // GetPipelineStartTime returns the pipeline start time in UTC | ||||
| func (a *AzureDevOpsConfigProvider) GetPipelineStartTime() time.Time { | ||||
| 	// "2021-10-11 13:49:09+00:00" | ||||
| 	timestamp := getEnv("SYSTEM_PIPELINESTARTTIME", "n/a") | ||||
| 	replaced := strings.Replace(timestamp, " ", "T", 1) | ||||
| 	parsed, err := time.Parse(time.RFC3339, replaced) | ||||
| 	if err != nil { | ||||
| 		log.Entry().Errorf("Could not parse timestamp. %v", err) | ||||
| 		// Return 1970 in case parsing goes wrong | ||||
| 		parsed = time.Date(1970, time.January, 01, 0, 0, 0, 0, time.UTC) | ||||
| 	//"2022-03-18T07:30:31.1915758Z" | ||||
| 	a.fetchAPIInformation() | ||||
| 	if val, ok := a.apiInformation["startTime"]; ok { | ||||
| 		parsed, err := time.Parse(time.RFC3339, val.(string)) | ||||
| 		if err != nil { | ||||
| 			log.Entry().Errorf("could not parse timestamp, %v", err) | ||||
| 			parsed = time.Time{} | ||||
| 		} | ||||
| 		return parsed.UTC() | ||||
| 	} | ||||
| 	return parsed | ||||
| 	return time.Time{}.UTC() | ||||
| } | ||||
|  | ||||
| func (a *AzureDevOpsConfigProvider) GetSystemCollectionURI() string { | ||||
| 	return getEnv("SYSTEM_COLLECTIONURI", "n/a") | ||||
| } | ||||
|  | ||||
| func (a *AzureDevOpsConfigProvider) GetTeamProjectId() string { | ||||
| 	return getEnv("SYSTEM_TEAMPROJECTID", "n/a") | ||||
| } | ||||
|  | ||||
| func (a *AzureDevOpsConfigProvider) GetBuildId() string { | ||||
| 	return getEnv("BUILD_BUILDID", "n/a") | ||||
| // GetBuildID returns the BuildNumber displayed in the ADO UI | ||||
| func (a *AzureDevOpsConfigProvider) GetBuildID() string { | ||||
| 	// INFO: ADO has BUILD_ID and buildNumber, as buildNumber is used in the UI we return this value | ||||
| 	// for the buildID used only for API requests we have a private method getAzureBuildID | ||||
| 	// example: buildNumber: 20220318.16 buildId: 76443 | ||||
| 	return getEnv("BUILD_BUILDNUMBER", "n/a") | ||||
| } | ||||
|  | ||||
| // GetStageName returns the human-readable name given to a stage. e.g. "Promote" or "Init" | ||||
| func (a *AzureDevOpsConfigProvider) GetStageName() string { | ||||
| 	return getEnv("SYSTEM_STAGEDISPLAYNAME", "n/a") | ||||
| } | ||||
|  | ||||
| // GetBranch returns the source branch name, e.g. main | ||||
| func (a *AzureDevOpsConfigProvider) GetBranch() string { | ||||
| 	tmp := getEnv("BUILD_SOURCEBRANCH", "n/a") | ||||
| 	return strings.TrimPrefix(tmp, "refs/heads/") | ||||
| 	return getEnv("BUILD_SOURCEBRANCHNAME", "n/a") | ||||
| } | ||||
|  | ||||
| func (a *AzureDevOpsConfigProvider) GetBuildUrl() string { | ||||
| 	return os.Getenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI") + os.Getenv("SYSTEM_TEAMPROJECT") + "/_build/results?buildId=" + os.Getenv("BUILD_BUILDID") | ||||
| // GetBuildURL returns the builds URL e.g. https://dev.azure.com/fabrikamfiber/your-repo-name/_build/results?buildId=1234 | ||||
| func (a *AzureDevOpsConfigProvider) GetBuildURL() string { | ||||
| 	return os.Getenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI") + os.Getenv("SYSTEM_TEAMPROJECT") + "/" + os.Getenv("SYSTEM_DEFINITIONNAME") + "/_build/results?buildId=" + a.getAzureBuildID() | ||||
| } | ||||
|  | ||||
| func (a *AzureDevOpsConfigProvider) GetJobUrl() string { | ||||
| 	// TODO: Check if thi is the correct URL | ||||
| 	return os.Getenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI") + os.Getenv("SYSTEM_TEAMPROJECT") | ||||
| // GetJobURL returns tje current job url e.g. https://dev.azure.com/fabrikamfiber/your-repo-name/_build?definitionId=1234 | ||||
| func (a *AzureDevOpsConfigProvider) GetJobURL() string { | ||||
| 	// TODO: Check if this is the correct URL | ||||
| 	return os.Getenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI") + os.Getenv("SYSTEM_TEAMPROJECT") + "/" + os.Getenv("SYSTEM_DEFINITIONNAME") + "/_build?definitionId=" + os.Getenv("SYSTEM_DEFINITIONID") | ||||
| } | ||||
|  | ||||
| // GetCommit returns commit SHA of current build | ||||
| func (a *AzureDevOpsConfigProvider) GetCommit() string { | ||||
| 	return getEnv("BUILD_SOURCEVERSION", "n/a") | ||||
| } | ||||
|  | ||||
| func (a *AzureDevOpsConfigProvider) GetRepoUrl() string { | ||||
| // GetRepoURL returns current repo URL e.g. https://github.com/SAP/jenkins-library | ||||
| func (a *AzureDevOpsConfigProvider) GetRepoURL() string { | ||||
| 	return getEnv("BUILD_REPOSITORY_URI", "n/a") | ||||
| } | ||||
|  | ||||
| // GetBuildReason returns the build reason | ||||
| func (a *AzureDevOpsConfigProvider) GetBuildReason() string { | ||||
| 	// https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services | ||||
| 	return getEnv("BUILD_REASON", "n/a") | ||||
| } | ||||
|  | ||||
| // GetPullRequestConfig returns pull request configuration | ||||
| func (a *AzureDevOpsConfigProvider) GetPullRequestConfig() PullRequestConfig { | ||||
| 	prKey := getEnv("SYSTEM_PULLREQUEST_PULLREQUESTID", "n/a") | ||||
|  | ||||
| @@ -196,6 +236,7 @@ func (a *AzureDevOpsConfigProvider) GetPullRequestConfig() PullRequestConfig { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // IsPullRequest indicates whether the current build is a PR | ||||
| func (a *AzureDevOpsConfigProvider) IsPullRequest() bool { | ||||
| 	return getEnv("BUILD_REASON", "n/a") == "PullRequest" | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,14 @@ | ||||
| package orchestrator | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	piperhttp "github.com/SAP/jenkins-library/pkg/http" | ||||
| 	"github.com/jarcoal/httpmock" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| @@ -12,21 +18,23 @@ func TestAzure(t *testing.T) { | ||||
| 		defer resetEnv(os.Environ()) | ||||
| 		os.Clearenv() | ||||
| 		os.Setenv("AZURE_HTTP_USER_AGENT", "FOO BAR BAZ") | ||||
| 		os.Setenv("BUILD_SOURCEBRANCH", "refs/heads/feat/test-azure") | ||||
| 		os.Setenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", "https://pogo.foo") | ||||
| 		os.Setenv("SYSTEM_TEAMPROJECT", "bar") | ||||
| 		os.Setenv("BUILD_SOURCEBRANCHNAME", "feat/test-azure") | ||||
| 		os.Setenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", "https://pogo.sap/") | ||||
| 		os.Setenv("SYSTEM_TEAMPROJECT", "foo") | ||||
| 		os.Setenv("BUILD_BUILDID", "42") | ||||
| 		os.Setenv("BUILD_SOURCEVERSION", "abcdef42713") | ||||
| 		os.Setenv("BUILD_REPOSITORY_URI", "github.com/foo/bar") | ||||
|  | ||||
| 		os.Setenv("SYSTEM_DEFINITIONNAME", "bar") | ||||
| 		os.Setenv("SYSTEM_DEFINITIONID", "1234") | ||||
| 		p, _ := NewOrchestratorSpecificConfigProvider() | ||||
|  | ||||
| 		assert.False(t, p.IsPullRequest()) | ||||
| 		assert.Equal(t, "feat/test-azure", p.GetBranch()) | ||||
| 		assert.Equal(t, "https://pogo.foobar/_build/results?buildId=42", p.GetBuildUrl()) | ||||
| 		assert.Equal(t, "https://pogo.sap/foo/bar/_build/results?buildId=42", p.GetBuildURL()) | ||||
| 		assert.Equal(t, "abcdef42713", p.GetCommit()) | ||||
| 		assert.Equal(t, "github.com/foo/bar", p.GetRepoUrl()) | ||||
| 		assert.Equal(t, "github.com/foo/bar", p.GetRepoURL()) | ||||
| 		assert.Equal(t, "Azure", p.OrchestratorType()) | ||||
| 		assert.Equal(t, "https://pogo.sap/foo/bar/_build?definitionId=1234", p.GetJobURL()) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("PR", func(t *testing.T) { | ||||
| @@ -74,4 +82,279 @@ func TestAzure(t *testing.T) { | ||||
|  | ||||
| 		assert.Equal(t, Orchestrator(Unknown), o) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("env variables", func(t *testing.T) { | ||||
| 		defer resetEnv(os.Environ()) | ||||
| 		os.Clearenv() | ||||
| 		os.Setenv("SYSTEM_COLLECTIONURI", "https://dev.azure.com/fabrikamfiber/") | ||||
| 		os.Setenv("SYSTEM_TEAMPROJECTID", "123a4567-ab1c-12a1-1234-123456ab7890") | ||||
| 		os.Setenv("BUILD_BUILDID", "42") | ||||
| 		os.Setenv("AGENT_VERSION", "2.193.0") | ||||
| 		os.Setenv("BUILD_BUILDNUMBER", "20220318.16") | ||||
| 		os.Setenv("BUILD_REPOSITORY_NAME", "repo-org/repo-name") | ||||
|  | ||||
| 		p := AzureDevOpsConfigProvider{} | ||||
|  | ||||
| 		assert.Equal(t, "https://dev.azure.com/fabrikamfiber/", p.getSystemCollectionURI()) | ||||
| 		assert.Equal(t, "123a4567-ab1c-12a1-1234-123456ab7890", p.getTeamProjectID()) | ||||
| 		assert.Equal(t, "42", p.getAzureBuildID())     // Don't confuse getAzureBuildID and GetBuildID! | ||||
| 		assert.Equal(t, "20220318.16", p.GetBuildID()) // buildNumber is used in the UI | ||||
| 		assert.Equal(t, "2.193.0", p.OrchestratorVersion()) | ||||
| 		assert.Equal(t, "repo-org/repo-name", p.GetJobName()) | ||||
|  | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestAzureDevOpsConfigProvider_GetPipelineStartTime(t *testing.T) { | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		apiInformation map[string]interface{} | ||||
| 		want           time.Time | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Retrieve correct time", | ||||
| 			apiInformation: map[string]interface{}{"startTime": "2022-03-18T12:30:42.0Z"}, | ||||
| 			want:           time.Date(2022, time.March, 18, 12, 30, 42, 0, time.UTC), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty apiInformation", | ||||
| 			apiInformation: map[string]interface{}{}, | ||||
| 			want:           time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "apiInformation does not contain key", | ||||
| 			apiInformation: map[string]interface{}{"someKey": "someValue"}, | ||||
| 			want:           time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "apiInformation contains malformed date", | ||||
| 			apiInformation: map[string]interface{}{"startTime": "2022-03/18 12:30:42.0Z"}, | ||||
| 			want:           time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			a := &AzureDevOpsConfigProvider{} | ||||
| 			a.apiInformation = tt.apiInformation | ||||
| 			pipelineStartTime := a.GetPipelineStartTime() | ||||
| 			assert.Equalf(t, tt.want, pipelineStartTime, "GetPipelineStartTime()") | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestAzureDevOpsConfigProvider_GetBuildStatus(t *testing.T) { | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name   string | ||||
| 		want   string | ||||
| 		envVar string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:   "Success", | ||||
| 			envVar: "Succeeded", | ||||
| 			want:   "SUCCESS", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "aborted", | ||||
| 			envVar: "Canceled", | ||||
| 			want:   "ABORTED", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "failure", | ||||
| 			envVar: "failed", | ||||
| 			want:   "FAILURE", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "other", | ||||
| 			envVar: "some other status", | ||||
| 			want:   "FAILURE", | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			defer resetEnv(os.Environ()) | ||||
| 			os.Clearenv() | ||||
| 			os.Setenv("AGENT_JOBSTATUS", tt.envVar) | ||||
| 			a := &AzureDevOpsConfigProvider{} | ||||
|  | ||||
| 			assert.Equalf(t, tt.want, a.GetBuildStatus(), "GetBuildStatus()") | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestAzureDevOpsConfigProvider_getAPIInformation(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name                    string | ||||
| 		wantHTTPErr             bool | ||||
| 		wantHTTPStatusCodeError bool | ||||
| 		wantHTTPJSONParseError  bool | ||||
| 		apiInformation          map[string]interface{} | ||||
| 		wantAPIInformation      map[string]interface{} | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:               "success case", | ||||
| 			apiInformation:     map[string]interface{}{}, | ||||
| 			wantAPIInformation: map[string]interface{}{"Success": "Case"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:               "apiInformation already set", | ||||
| 			apiInformation:     map[string]interface{}{"API info": "set"}, | ||||
| 			wantAPIInformation: map[string]interface{}{"API info": "set"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:               "failed to get response", | ||||
| 			apiInformation:     map[string]interface{}{}, | ||||
| 			wantHTTPErr:        true, | ||||
| 			wantAPIInformation: map[string]interface{}{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "response code != 200 http.StatusNoContent", | ||||
| 			wantHTTPStatusCodeError: true, | ||||
| 			apiInformation:          map[string]interface{}{}, | ||||
| 			wantAPIInformation:      map[string]interface{}{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                   "parseResponseBodyJson fails", | ||||
| 			wantHTTPJSONParseError: true, | ||||
| 			apiInformation:         map[string]interface{}{}, | ||||
| 			wantAPIInformation:     map[string]interface{}{}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			a := &AzureDevOpsConfigProvider{ | ||||
| 				apiInformation: tt.apiInformation, | ||||
| 			} | ||||
|  | ||||
| 			a.client.SetOptions(piperhttp.ClientOptions{ | ||||
| 				MaxRequestDuration:        5 * time.Second, | ||||
| 				Token:                     "TOKEN", | ||||
| 				TransportSkipVerification: true, | ||||
| 				UseDefaultTransport:       true, // need to use default transport for http mock | ||||
| 				MaxRetries:                -1, | ||||
| 			}) | ||||
|  | ||||
| 			defer resetEnv(os.Environ()) | ||||
| 			os.Clearenv() | ||||
| 			os.Setenv("SYSTEM_COLLECTIONURI", "https://dev.azure.com/fabrikamfiber/") | ||||
| 			os.Setenv("SYSTEM_TEAMPROJECTID", "123a4567-ab1c-12a1-1234-123456ab7890") | ||||
| 			os.Setenv("BUILD_BUILDID", "1234") | ||||
|  | ||||
| 			fakeUrl := "https://dev.azure.com/fabrikamfiber/123a4567-ab1c-12a1-1234-123456ab7890/_apis/build/builds/1234/" | ||||
| 			httpmock.Activate() | ||||
| 			defer httpmock.DeactivateAndReset() | ||||
| 			httpmock.RegisterResponder("GET", fakeUrl, | ||||
| 				func(req *http.Request) (*http.Response, error) { | ||||
| 					if tt.wantHTTPErr { | ||||
| 						return nil, errors.New("this error shows up") | ||||
| 					} | ||||
| 					if tt.wantHTTPStatusCodeError { | ||||
| 						return &http.Response{ | ||||
| 							Status:     "204", | ||||
| 							StatusCode: http.StatusNoContent, | ||||
| 							Request:    req, | ||||
| 						}, nil | ||||
| 					} | ||||
| 					if tt.wantHTTPJSONParseError { | ||||
| 						// Intentionally malformed JSON response | ||||
| 						return httpmock.NewJsonResponse(200, "timestamp:broken") | ||||
| 					} | ||||
| 					return httpmock.NewStringResponse(200, "{\"Success\":\"Case\"}"), nil | ||||
| 				}, | ||||
| 			) | ||||
|  | ||||
| 			a.fetchAPIInformation() | ||||
| 			assert.Equal(t, tt.wantAPIInformation, a.apiInformation) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestAzureDevOpsConfigProvider_GetLog(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name                    string | ||||
| 		want                    []byte | ||||
| 		wantErr                 assert.ErrorAssertionFunc | ||||
| 		wantHTTPErr             bool | ||||
| 		wantHTTPStatusCodeError bool | ||||
| 		wantLogCountError       bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:    "Successfully got log file", | ||||
| 			want:    []byte("Success"), | ||||
| 			wantErr: assert.NoError, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:              "Log count variable not available", | ||||
| 			want:              []byte(""), | ||||
| 			wantErr:           assert.NoError, | ||||
| 			wantLogCountError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "HTTP error", | ||||
| 			want:        []byte(""), | ||||
| 			wantErr:     assert.Error, | ||||
| 			wantHTTPErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "Status code error", | ||||
| 			want:                    []byte(""), | ||||
| 			wantErr:                 assert.NoError, | ||||
| 			wantHTTPStatusCodeError: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			a := &AzureDevOpsConfigProvider{} | ||||
| 			a.client.SetOptions(piperhttp.ClientOptions{ | ||||
| 				MaxRequestDuration:        5 * time.Second, | ||||
| 				Token:                     "TOKEN", | ||||
| 				TransportSkipVerification: true, | ||||
| 				UseDefaultTransport:       true, // need to use default transport for http mock | ||||
| 				MaxRetries:                -1, | ||||
| 			}) | ||||
|  | ||||
| 			defer resetEnv(os.Environ()) | ||||
| 			os.Clearenv() | ||||
| 			os.Setenv("SYSTEM_COLLECTIONURI", "https://dev.azure.com/fabrikamfiber/") | ||||
| 			os.Setenv("SYSTEM_TEAMPROJECTID", "123a4567-ab1c-12a1-1234-123456ab7890") | ||||
| 			os.Setenv("BUILD_BUILDID", "1234") | ||||
|  | ||||
| 			fakeUrl := "https://dev.azure.com/fabrikamfiber/123a4567-ab1c-12a1-1234-123456ab7890/_apis/build/builds/1234/logs" | ||||
| 			httpmock.Activate() | ||||
| 			defer httpmock.DeactivateAndReset() | ||||
| 			httpmock.RegisterResponder("GET", fakeUrl+"/1", | ||||
| 				func(req *http.Request) (*http.Response, error) { | ||||
| 					return httpmock.NewStringResponse(200, "Success"), nil | ||||
| 				}) | ||||
| 			httpmock.RegisterResponder("GET", fakeUrl, | ||||
| 				func(req *http.Request) (*http.Response, error) { | ||||
| 					if tt.wantHTTPErr { | ||||
| 						return nil, errors.New("this error shows up") | ||||
| 					} | ||||
| 					if tt.wantHTTPStatusCodeError { | ||||
| 						return &http.Response{ | ||||
| 							Status:     "204", | ||||
| 							StatusCode: http.StatusNoContent, | ||||
| 							Request:    req, | ||||
| 						}, nil | ||||
| 					} | ||||
| 					if tt.wantLogCountError { | ||||
| 						return httpmock.NewJsonResponse(200, map[string]interface{}{ | ||||
| 							"some": "value", | ||||
| 						}) | ||||
| 					} | ||||
| 					return httpmock.NewJsonResponse(200, map[string]interface{}{ | ||||
| 						"count": 1, | ||||
| 					}) | ||||
| 				}, | ||||
| 			) | ||||
| 			got, err := a.GetLog() | ||||
| 			if !tt.wantErr(t, err, fmt.Sprintf("GetLog()")) { | ||||
| 				return | ||||
| 			} | ||||
| 			assert.Equalf(t, tt.want, got, "GetLog()") | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import ( | ||||
| type GitHubActionsConfigProvider struct{} | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) { | ||||
| 	log.Entry().Debug("Successfully initalized GitHubActions config provider") | ||||
| 	log.Entry().Debug("Successfully initialized GitHubActions config provider") | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) OrchestratorVersion() string { | ||||
| @@ -23,51 +23,55 @@ func (g *GitHubActionsConfigProvider) OrchestratorType() string { | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetBuildStatus() string { | ||||
| 	log.Entry().Infof("GetBuildStatus() for GitHub Actions not yet implemented.") | ||||
| 	return "SUCCESS" | ||||
| 	return "FAILURE" | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetLog() ([]byte, error) { | ||||
| 	log.Entry().Infof("GetLog() for GitHub Actions not yet implemented.") | ||||
| 	return nil, nil | ||||
| 	return []byte{}, nil | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetBuildId() string { | ||||
| 	log.Entry().Infof("GetBuildId() for GitHub Actions not yet implemented.") | ||||
| func (g *GitHubActionsConfigProvider) GetBuildID() string { | ||||
| 	log.Entry().Infof("GetBuildID() for GitHub Actions not yet implemented.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetPipelineStartTime() time.Time { | ||||
| 	log.Entry().Infof("GetPipelineStartTime() for GitHub Actions not yet implemented.") | ||||
| 	timestamp, _ := time.Parse(time.UnixDate, "Wed Feb 25 11:06:39 PST 1970") | ||||
| 	return timestamp | ||||
| 	return time.Time{}.UTC() | ||||
| } | ||||
| func (g *GitHubActionsConfigProvider) GetStageName() string { | ||||
| 	return "GITHUB_WORKFLOW" //TODO: is there something like is "stage" in GH Actions? | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetBuildReason() string { | ||||
| 	log.Entry().Infof("GetBuildReason() for GitHub Actions not yet implemented.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetBranch() string { | ||||
| 	return strings.TrimPrefix(getEnv("GITHUB_REF", "n/a"), "refs/heads/") | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetBuildUrl() string { | ||||
| 	return g.GetRepoUrl() + "/actions/runs/" + getEnv("GITHUB_RUN_ID", "n/a") | ||||
| func (g *GitHubActionsConfigProvider) GetBuildURL() string { | ||||
| 	return g.GetRepoURL() + "/actions/runs/" + getEnv("GITHUB_RUN_ID", "n/a") | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetJobUrl() string { | ||||
| func (g *GitHubActionsConfigProvider) GetJobURL() string { | ||||
| 	log.Entry().Debugf("Not yet implemented.") | ||||
| 	return g.GetRepoUrl() + "/actions/runs/" + getEnv("GITHUB_RUN_ID", "n/a") | ||||
| 	return g.GetRepoURL() + "/actions/runs/" + getEnv("GITHUB_RUN_ID", "n/a") | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetJobName() string { | ||||
| 	log.Entry().Debugf("GetJobName() for GitHubActions not yet implemented.") | ||||
| 	return "N/A" | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetCommit() string { | ||||
| 	return getEnv("GITHUB_SHA", "n/a") | ||||
| } | ||||
|  | ||||
| func (g *GitHubActionsConfigProvider) GetRepoUrl() string { | ||||
| func (g *GitHubActionsConfigProvider) GetRepoURL() string { | ||||
| 	return getEnv("GITHUB_SERVER_URL", "n/a") + getEnv("GITHUB_REPOSITORY", "n/a") | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -22,10 +22,10 @@ func TestGitHubActions(t *testing.T) { | ||||
| 		p, _ := NewOrchestratorSpecificConfigProvider() | ||||
|  | ||||
| 		assert.False(t, p.IsPullRequest()) | ||||
| 		assert.Equal(t, "github.com/foo/bar/actions/runs/42", p.GetBuildUrl()) | ||||
| 		assert.Equal(t, "github.com/foo/bar/actions/runs/42", p.GetBuildURL()) | ||||
| 		assert.Equal(t, "feat/test-gh-actions", p.GetBranch()) | ||||
| 		assert.Equal(t, "abcdef42713", p.GetCommit()) | ||||
| 		assert.Equal(t, "github.com/foo/bar", p.GetRepoUrl()) | ||||
| 		assert.Equal(t, "github.com/foo/bar", p.GetRepoURL()) | ||||
| 		assert.Equal(t, "GitHubActions", p.OrchestratorType()) | ||||
| 	}) | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| package orchestrator | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"github.com/Jeffail/gabs/v2" | ||||
| 	piperHttp "github.com/SAP/jenkins-library/pkg/http" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/pkg/errors" | ||||
| @@ -9,10 +11,12 @@ import ( | ||||
| ) | ||||
|  | ||||
| type JenkinsConfigProvider struct { | ||||
| 	client  piperHttp.Client | ||||
| 	options piperHttp.ClientOptions | ||||
| 	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{ | ||||
| @@ -25,43 +29,51 @@ func (j *JenkinsConfigProvider) InitOrchestratorProvider(settings *OrchestratorS | ||||
| 	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) getAPIInformation() map[string]interface{} { | ||||
| 	URL := j.GetBuildUrl() + "api/json" | ||||
| 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 | ||||
| 		} | ||||
|  | ||||
| 	response, err := j.client.GetRequest(URL, nil, nil) | ||||
| 	if err != nil { | ||||
| 		log.Entry().WithError(err).Error("could not get api information from Jenkins") | ||||
| 		return map[string]interface{}{} | ||||
| 		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") | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != 200 { //http.StatusNoContent | ||||
| 		log.Entry().Errorf("Response-Code is %v . \n Could not get timestamp from Jenkins. Setting timestamp to 1970.", response.StatusCode) | ||||
| 		return map[string]interface{}{} | ||||
| 	} | ||||
| 	var responseInterface map[string]interface{} | ||||
| 	err = piperHttp.ParseHTTPResponseBodyJSON(response, &responseInterface) | ||||
| 	if err != nil { | ||||
| 		log.Entry().Error(err) | ||||
| 		return map[string]interface{}{} | ||||
| 	} | ||||
| 	return responseInterface | ||||
| } | ||||
|  | ||||
| // GetBuildInformation | ||||
| // GetBuildStatus returns build status of the current job | ||||
| func (j *JenkinsConfigProvider) GetBuildStatus() string { | ||||
| 	responseInterface := j.getAPIInformation() | ||||
|  | ||||
| 	if val, ok := responseInterface["result"]; ok { | ||||
| 	j.fetchAPIInformation() | ||||
| 	if val, ok := j.apiInformation["result"]; ok { | ||||
| 		// cases in ADO: succeeded, failed, canceled, none, partiallySucceeded | ||||
| 		switch result := responseInterface["result"]; result { | ||||
| 		switch result := val; result { | ||||
| 		case "SUCCESS": | ||||
| 			return "SUCCESS" | ||||
| 		case "ABORTED": | ||||
| @@ -70,50 +82,47 @@ func (j *JenkinsConfigProvider) GetBuildStatus() string { | ||||
| 			// FAILURE, NOT_BUILT | ||||
| 			return "FAILURE" | ||||
| 		} | ||||
| 		return val.(string) | ||||
| 	} | ||||
|  | ||||
| 	return "FAILURE" | ||||
| } | ||||
|  | ||||
| // GetLog returns the logfile from the current job as byte object | ||||
| func (j *JenkinsConfigProvider) GetLog() ([]byte, error) { | ||||
| 	URL := j.GetBuildUrl() + "consoleText" | ||||
| 	URL := j.GetBuildURL() + "consoleText" | ||||
|  | ||||
| 	response, err := j.client.GetRequest(URL, nil, nil) | ||||
| 	if err != nil { | ||||
| 		return []byte{}, errors.Wrapf(err, "Could not read Jenkins logfile. %v", err) | ||||
| 	} | ||||
| 	if response.StatusCode != 200 { | ||||
| 		log.Entry().Error("Could not get log information from Jenkins. Returning with empty log.") | ||||
| 		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 | ||||
| 	} | ||||
| 	defer response.Body.Close() | ||||
|  | ||||
| 	logFile, err := ioutil.ReadAll(response.Body) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return []byte{}, errors.Wrapf(err, "could not read Jenkins logfile from request. %v", err) | ||||
| 		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" | ||||
|  | ||||
| 	URL := j.GetBuildURL() + "api/json" | ||||
| 	response, err := j.client.GetRequest(URL, nil, nil) | ||||
| 	if err != nil { | ||||
| 		log.Entry().Error(err) | ||||
| 		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.Unix(1, 0) | ||||
| 		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().Error(err) | ||||
| 		log.Entry().WithError(err).Infof("could not parse http response, returning 1970") | ||||
| 		return time.Time{}.UTC() | ||||
| 	} | ||||
|  | ||||
| 	rawTimeStamp := responseInterface["timestamp"].(float64) | ||||
| @@ -121,45 +130,88 @@ func (j *JenkinsConfigProvider) GetPipelineStartTime() time.Time { | ||||
|  | ||||
| 	log.Entry().Debugf("Pipeline start time: %v", timeStamp.String()) | ||||
| 	defer response.Body.Close() | ||||
| 	return timeStamp | ||||
| 	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") | ||||
| } | ||||
|  | ||||
| func (j *JenkinsConfigProvider) GetJobUrl() string { | ||||
| // 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") | ||||
| } | ||||
|  | ||||
| func (j *JenkinsConfigProvider) GetBuildId() string { | ||||
| // GetBuildID returns the build ID of the current job, e.g. 1234 | ||||
| func (j *JenkinsConfigProvider) GetBuildID() string { | ||||
| 	return getEnv("BUILD_ID", "n/a") | ||||
| } | ||||
|  | ||||
| func (a *JenkinsConfigProvider) GetStageName() string { | ||||
| // 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 { | ||||
| 	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.S("actions").Children() { | ||||
| 		class := child.S("_class") | ||||
| 		if class.String() == "\"hudson.model.CauseAction\"" { | ||||
| 			for _, val := range child.S("causes").Children() { | ||||
| 				subclass := val.S("_class") | ||||
| 				if subclass.String() == "\"hudson.model.Cause$UserIdCause\"" { | ||||
| 					return "Manual" | ||||
| 				} else if subclass.String() == "\"hudson.triggers.TimerTrigger$TimerTriggerCause\"" { | ||||
| 					return "Schedule" | ||||
| 				} 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") | ||||
| } | ||||
|  | ||||
| func (j *JenkinsConfigProvider) GetBuildUrl() string { | ||||
| // 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") | ||||
| } | ||||
|  | ||||
| func (j *JenkinsConfigProvider) GetRepoUrl() string { | ||||
| // 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"), | ||||
| @@ -168,6 +220,7 @@ func (j *JenkinsConfigProvider) GetPullRequestConfig() PullRequestConfig { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // IsPullRequest returns boolean indicating if current job is a PR | ||||
| func (j *JenkinsConfigProvider) IsPullRequest() bool { | ||||
| 	return truthy("CHANGE_ID") | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,17 @@ | ||||
| package orchestrator | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	piperhttp "github.com/SAP/jenkins-library/pkg/http" | ||||
| 	"github.com/jarcoal/httpmock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| func TestJenkins(t *testing.T) { | ||||
| @@ -12,7 +19,7 @@ func TestJenkins(t *testing.T) { | ||||
| 		defer resetEnv(os.Environ()) | ||||
| 		os.Clearenv() | ||||
| 		os.Setenv("JENKINS_URL", "FOO BAR BAZ") | ||||
| 		os.Setenv("BUILD_URL", "jaas.com/foo/bar/main/42") | ||||
| 		os.Setenv("BUILD_URL", "https://jaas.url/job/foo/job/bar/job/main/1234/") | ||||
| 		os.Setenv("BRANCH_NAME", "main") | ||||
| 		os.Setenv("GIT_COMMIT", "abcdef42713") | ||||
| 		os.Setenv("GIT_URL", "github.com/foo/bar") | ||||
| @@ -20,10 +27,10 @@ func TestJenkins(t *testing.T) { | ||||
| 		p, _ := NewOrchestratorSpecificConfigProvider() | ||||
|  | ||||
| 		assert.False(t, p.IsPullRequest()) | ||||
| 		assert.Equal(t, "jaas.com/foo/bar/main/42", p.GetBuildUrl()) | ||||
| 		assert.Equal(t, "https://jaas.url/job/foo/job/bar/job/main/1234/", p.GetBuildURL()) | ||||
| 		assert.Equal(t, "main", p.GetBranch()) | ||||
| 		assert.Equal(t, "abcdef42713", p.GetCommit()) | ||||
| 		assert.Equal(t, "github.com/foo/bar", p.GetRepoUrl()) | ||||
| 		assert.Equal(t, "github.com/foo/bar", p.GetRepoURL()) | ||||
| 		assert.Equal(t, "Jenkins", p.OrchestratorType()) | ||||
| 	}) | ||||
|  | ||||
| @@ -43,4 +50,449 @@ func TestJenkins(t *testing.T) { | ||||
| 		assert.Equal(t, "main", c.Base) | ||||
| 		assert.Equal(t, "42", c.Key) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("env variables", func(t *testing.T) { | ||||
| 		defer resetEnv(os.Environ()) | ||||
| 		os.Clearenv() | ||||
| 		os.Setenv("JENKINS_HOME", "/var/lib/jenkins") | ||||
| 		os.Setenv("BUILD_ID", "1234") | ||||
| 		os.Setenv("JOB_URL", "https://jaas.url/job/foo/job/bar/job/main") | ||||
| 		os.Setenv("JENKINS_VERSION", "42") | ||||
| 		os.Setenv("JOB_NAME", "foo/bar/BRANCH") | ||||
| 		os.Setenv("STAGE_NAME", "Promote") | ||||
| 		os.Setenv("BUILD_URL", "https://jaas.url/job/foo/job/bar/job/main/1234/") | ||||
| 		os.Setenv("STAGE_NAME", "Promote") | ||||
|  | ||||
| 		p := JenkinsConfigProvider{} | ||||
|  | ||||
| 		assert.Equal(t, "/var/lib/jenkins", p.getJenkinsHome()) | ||||
| 		assert.Equal(t, "1234", p.GetBuildID()) | ||||
| 		assert.Equal(t, "https://jaas.url/job/foo/job/bar/job/main", p.GetJobURL()) | ||||
| 		assert.Equal(t, "42", p.OrchestratorVersion()) | ||||
| 		assert.Equal(t, "Jenkins", p.OrchestratorType()) | ||||
| 		assert.Equal(t, "foo/bar/BRANCH", p.GetJobName()) | ||||
| 		assert.Equal(t, "Promote", p.GetStageName()) | ||||
| 		assert.Equal(t, "https://jaas.url/job/foo/job/bar/job/main/1234/", p.GetBuildURL()) | ||||
|  | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestJenkinsConfigProvider_GetPipelineStartTime(t *testing.T) { | ||||
| 	type fields struct { | ||||
| 		client  piperhttp.Client | ||||
| 		options piperhttp.ClientOptions | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name                    string | ||||
| 		fields                  fields | ||||
| 		want                    time.Time | ||||
| 		wantHTTPErr             bool | ||||
| 		wantHTTPStatusCodeError bool | ||||
| 		wantHTTPJSONParseError  bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:                    "Retrieve correct time", | ||||
| 			want:                    time.Date(2022, time.March, 21, 22, 30, 0, 0, time.UTC), | ||||
| 			wantHTTPErr:             false, | ||||
| 			wantHTTPStatusCodeError: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "ParseHTTPResponseBodyJSON error", | ||||
| 			want:                    time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), | ||||
| 			wantHTTPErr:             false, | ||||
| 			wantHTTPStatusCodeError: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "GetRequest fails", | ||||
| 			want:                    time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), | ||||
| 			wantHTTPErr:             true, | ||||
| 			wantHTTPStatusCodeError: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "response code != 200 http.StatusNoContent", | ||||
| 			want:                    time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), | ||||
| 			wantHTTPErr:             false, | ||||
| 			wantHTTPStatusCodeError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "parseResponseBodyJson fails", | ||||
| 			want:                    time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), | ||||
| 			wantHTTPErr:             false, | ||||
| 			wantHTTPStatusCodeError: false, | ||||
| 			wantHTTPJSONParseError:  true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	j := &JenkinsConfigProvider{ | ||||
| 		client: piperhttp.Client{}, | ||||
| 	} | ||||
| 	j.client.SetOptions(piperhttp.ClientOptions{ | ||||
| 		MaxRequestDuration:        5 * time.Second, | ||||
| 		Token:                     "TOKEN", | ||||
| 		TransportSkipVerification: true, | ||||
| 		UseDefaultTransport:       true, | ||||
| 		MaxRetries:                -1, | ||||
| 	}) | ||||
| 	httpmock.Activate() | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			defer resetEnv(os.Environ()) | ||||
| 			os.Clearenv() | ||||
| 			buildURl := "https://jaas.url/job/foo/job/bar/job/main/1234/" | ||||
| 			os.Setenv("BUILD_URL", buildURl) | ||||
|  | ||||
| 			fakeUrl := buildURl + "api/json" | ||||
| 			defer httpmock.DeactivateAndReset() | ||||
| 			httpmock.RegisterResponder("GET", fakeUrl, | ||||
| 				func(req *http.Request) (*http.Response, error) { | ||||
| 					if tt.wantHTTPErr { | ||||
| 						return nil, errors.New("this error shows up") | ||||
| 					} | ||||
| 					if tt.wantHTTPStatusCodeError { | ||||
| 						return &http.Response{ | ||||
| 							Status:     "204", | ||||
| 							StatusCode: http.StatusNoContent, | ||||
| 							Request:    req, | ||||
| 						}, nil | ||||
| 					} | ||||
| 					if tt.wantHTTPJSONParseError { | ||||
| 						// Intentionally malformed JSON response | ||||
| 						return httpmock.NewJsonResponse(200, "timestamp:asdffd") | ||||
| 					} | ||||
| 					return httpmock.NewStringResponse(200, "{\"timestamp\":1647901800932,\"url\":\"https://jaas.url/view/piperpipelines/job/foo/job/bar/job/main/3731/\"}"), nil | ||||
| 				}, | ||||
| 			) | ||||
|  | ||||
| 			assert.Equalf(t, tt.want, j.GetPipelineStartTime(), "GetPipelineStartTime()") | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestJenkinsConfigProvider_GetBuildStatus(t *testing.T) { | ||||
| 	apiSuccess := []byte(`{ "queueId":376475, | ||||
| 				"result":"SUCCESS", | ||||
| 				"timestamp":1647946800925 | ||||
| 				}`) | ||||
| 	apiFailure := []byte(`{ "queueId":376475, | ||||
| 				"result":"FAILURE", | ||||
| 				"timestamp":1647946800925 | ||||
| 				}`) | ||||
| 	apiAborted := []byte(`{ "queueId":376475, | ||||
| 				"result":"ABORTED", | ||||
| 				"timestamp":1647946800925 | ||||
| 				}`) | ||||
|  | ||||
| 	apiOTHER := []byte(`{ "queueId":376475, | ||||
| 				"result":"SOMETHING", | ||||
| 				"timestamp":1647946800925 | ||||
| 				}`) | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		want           string | ||||
| 		apiInformation []byte | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "SUCCESS", | ||||
| 			apiInformation: apiSuccess, | ||||
| 			want:           "SUCCESS", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "ABORTED", | ||||
| 			apiInformation: apiAborted, | ||||
| 			want:           "ABORTED", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "FAILURE", | ||||
| 			apiInformation: apiFailure, | ||||
| 			want:           "FAILURE", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Unknown FAILURE", | ||||
| 			apiInformation: apiOTHER, | ||||
| 			want:           "FAILURE", | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			var apiInformation map[string]interface{} | ||||
| 			err := json.Unmarshal(tt.apiInformation, &apiInformation) | ||||
| 			if err != nil { | ||||
| 				t.Fatal("could not parse json:", err) | ||||
| 			} | ||||
| 			j := &JenkinsConfigProvider{ | ||||
| 				apiInformation: apiInformation, | ||||
| 			} | ||||
| 			assert.Equalf(t, tt.want, j.GetBuildStatus(), "GetBuildStatus()") | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestJenkinsConfigProvider_GetBuildReason(t *testing.T) { | ||||
| 	apiJsonSchedule := []byte(`{ | ||||
| 				"_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", | ||||
| 				"actions": [{ | ||||
| 						"_class": "hudson.model.CauseAction", | ||||
| 						"causes": [{ | ||||
| 							"_class": "hudson.triggers.TimerTrigger$TimerTriggerCause", | ||||
| 							"shortDescription": "Started by timer" | ||||
| 						}] | ||||
| 					}, | ||||
| 					{ | ||||
| 						"_class": "jenkins.metrics.impl.TimeInQueueAction", | ||||
| 						"blockedDurationMillis": "0" | ||||
| 					} | ||||
| 				] | ||||
| 				}`) | ||||
|  | ||||
| 	apiJSONManual := []byte(`{ | ||||
| 				"_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", | ||||
| 				"actions": [{ | ||||
| 						"_class": "hudson.model.CauseAction", | ||||
| 						"causes": [{ | ||||
| 							"_class": "hudson.model.Cause$UserIdCause", | ||||
| 							"shortDescription": "Started by user John Doe", | ||||
| 							"userId": "i12345", | ||||
| 							"userName": "John Doe" | ||||
| 						}] | ||||
| 					}, | ||||
| 					{ | ||||
| 						"_class": "jenkins.metrics.impl.TimeInQueueAction", | ||||
| 						"blockedDurationMillis": "0" | ||||
| 					} | ||||
| 				] | ||||
| 				}`) | ||||
|  | ||||
| 	apiJSONUnknown := []byte(`{ | ||||
| 				"_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", | ||||
| 				"actions": [{ | ||||
| 						"_class": "hudson.model.CauseAction", | ||||
| 						"causes": [{ | ||||
| 							"_class": "hudson.model.RandomThingHere", | ||||
| 							"shortDescription": "Something" | ||||
| 						}] | ||||
| 					}, | ||||
| 					{ | ||||
| 						"_class": "jenkins.metrics.impl.TimeInQueueAction", | ||||
| 						"blockedDurationMillis": "0" | ||||
| 					} | ||||
| 				] | ||||
| 				}`) | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		apiInformation []byte | ||||
| 		want           string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Manual trigger", | ||||
| 			apiInformation: apiJSONManual, | ||||
| 			want:           "Manual", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Scheduled trigger", | ||||
| 			apiInformation: apiJsonSchedule, | ||||
| 			want:           "Schedule", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Unknown", | ||||
| 			apiInformation: apiJSONUnknown, | ||||
| 			want:           "Unknown", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty api", | ||||
| 			apiInformation: []byte(`{}`), | ||||
| 			want:           "Unknown", | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
|  | ||||
| 			var apiInformation map[string]interface{} | ||||
| 			err := json.Unmarshal(tt.apiInformation, &apiInformation) | ||||
| 			if err != nil { | ||||
| 				t.Fatal("could not parse json:", err) | ||||
| 			} | ||||
| 			j := &JenkinsConfigProvider{apiInformation: apiInformation} | ||||
|  | ||||
| 			assert.Equalf(t, tt.want, j.GetBuildReason(), "GetBuildReason()") | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestJenkinsConfigProvider_getAPIInformation(t *testing.T) { | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name                    string | ||||
| 		wantHTTPErr             bool | ||||
| 		wantHTTPStatusCodeError bool | ||||
| 		wantHTTPJSONParseError  bool | ||||
| 		apiInformation          map[string]interface{} | ||||
| 		wantAPIInformation      map[string]interface{} | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:               "success case", | ||||
| 			apiInformation:     map[string]interface{}{}, | ||||
| 			wantAPIInformation: map[string]interface{}{"Success": "Case"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:               "apiInformation already set", | ||||
| 			apiInformation:     map[string]interface{}{"API info": "set"}, | ||||
| 			wantAPIInformation: map[string]interface{}{"API info": "set"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:               "failed to get response", | ||||
| 			apiInformation:     map[string]interface{}{}, | ||||
| 			wantHTTPErr:        true, | ||||
| 			wantAPIInformation: map[string]interface{}{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "response code != 200 http.StatusNoContent", | ||||
| 			wantHTTPStatusCodeError: true, | ||||
| 			apiInformation:          map[string]interface{}{}, | ||||
| 			wantAPIInformation:      map[string]interface{}{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                   "parseResponseBodyJson fails", | ||||
| 			wantHTTPJSONParseError: true, | ||||
| 			apiInformation:         map[string]interface{}{}, | ||||
| 			wantAPIInformation:     map[string]interface{}{}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			j := &JenkinsConfigProvider{ | ||||
| 				apiInformation: tt.apiInformation, | ||||
| 			} | ||||
| 			j.client.SetOptions(piperhttp.ClientOptions{ | ||||
| 				MaxRequestDuration:        5 * time.Second, | ||||
| 				Token:                     "TOKEN", | ||||
| 				TransportSkipVerification: true, | ||||
| 				UseDefaultTransport:       true, // need to use default transport for http mock | ||||
| 				MaxRetries:                -1, | ||||
| 			}) | ||||
|  | ||||
| 			defer resetEnv(os.Environ()) | ||||
| 			os.Clearenv() | ||||
| 			os.Setenv("BUILD_URL", "https://jaas.url/job/foo/job/bar/job/main/1234/") | ||||
|  | ||||
| 			fakeUrl := "https://jaas.url/job/foo/job/bar/job/main/1234/api/json" | ||||
| 			httpmock.Activate() | ||||
| 			defer httpmock.DeactivateAndReset() | ||||
| 			httpmock.RegisterResponder("GET", fakeUrl, | ||||
| 				func(req *http.Request) (*http.Response, error) { | ||||
| 					if tt.wantHTTPErr { | ||||
| 						return nil, errors.New("this error shows up") | ||||
| 					} | ||||
| 					if tt.wantHTTPStatusCodeError { | ||||
| 						return &http.Response{ | ||||
| 							Status:     "204", | ||||
| 							StatusCode: http.StatusNoContent, | ||||
| 							Request:    req, | ||||
| 						}, nil | ||||
| 					} | ||||
| 					if tt.wantHTTPJSONParseError { | ||||
| 						// Intentionally malformed JSON response | ||||
| 						return httpmock.NewJsonResponse(200, "timestamp:broken") | ||||
| 					} | ||||
| 					return httpmock.NewStringResponse(200, "{\"Success\":\"Case\"}"), nil | ||||
| 				}, | ||||
| 			) | ||||
| 			j.fetchAPIInformation() | ||||
| 			assert.Equal(t, tt.wantAPIInformation, j.apiInformation) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestJenkinsConfigProvider_GetLog(t *testing.T) { | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name                    string | ||||
| 		want                    []byte | ||||
| 		wantErr                 assert.ErrorAssertionFunc | ||||
| 		wantHTTPErr             bool | ||||
| 		wantHTTPStatusCodeError bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:    "Successfully got log file", | ||||
| 			want:    []byte("Success!"), | ||||
| 			wantErr: assert.NoError, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "HTTP error", | ||||
| 			want:        []byte(""), | ||||
| 			wantErr:     assert.Error, | ||||
| 			wantHTTPErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "Status code error", | ||||
| 			want:                    []byte(""), | ||||
| 			wantErr:                 assert.NoError, | ||||
| 			wantHTTPStatusCodeError: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			j := &JenkinsConfigProvider{} | ||||
| 			j.client.SetOptions(piperhttp.ClientOptions{ | ||||
| 				MaxRequestDuration:        5 * time.Second, | ||||
| 				Token:                     "TOKEN", | ||||
| 				TransportSkipVerification: true, | ||||
| 				UseDefaultTransport:       true, // need to use default transport for http mock | ||||
| 				MaxRetries:                -1, | ||||
| 			}) | ||||
|  | ||||
| 			defer resetEnv(os.Environ()) | ||||
| 			os.Clearenv() | ||||
| 			os.Setenv("BUILD_URL", "https://jaas.url/job/foo/job/bar/job/main/1234/") | ||||
|  | ||||
| 			fakeUrl := "https://jaas.url/job/foo/job/bar/job/main/1234/consoleText" | ||||
| 			httpmock.Activate() | ||||
| 			defer httpmock.DeactivateAndReset() | ||||
| 			httpmock.RegisterResponder("GET", fakeUrl, | ||||
| 				func(req *http.Request) (*http.Response, error) { | ||||
| 					if tt.wantHTTPErr { | ||||
| 						return nil, errors.New("this error shows up") | ||||
| 					} | ||||
| 					if tt.wantHTTPStatusCodeError { | ||||
| 						return &http.Response{ | ||||
| 							Status:     "204", | ||||
| 							StatusCode: http.StatusNoContent, | ||||
| 							Request:    req, | ||||
| 						}, nil | ||||
| 					} | ||||
| 					return httpmock.NewStringResponse(200, "Success!"), nil | ||||
| 				}, | ||||
| 			) | ||||
|  | ||||
| 			got, err := j.GetLog() | ||||
| 			if !tt.wantErr(t, err, fmt.Sprintf("GetLog()")) { | ||||
| 				return | ||||
| 			} | ||||
| 			assert.Equalf(t, tt.want, got, "GetLog()") | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestJenkinsConfigProvider_InitOrchestratorProvider(t *testing.T) { | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		settings       *OrchestratorSettings | ||||
| 		apiInformation map[string]interface{} | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "Init, test empty apiInformation", | ||||
| 			settings: &OrchestratorSettings{}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			j := &JenkinsConfigProvider{} | ||||
| 			j.InitOrchestratorProvider(tt.settings) | ||||
| 			var expected map[string]interface{} | ||||
| 			assert.Equal(t, j.apiInformation, expected) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -22,17 +22,18 @@ type OrchestratorSpecificConfigProviding interface { | ||||
| 	OrchestratorVersion() string | ||||
| 	GetStageName() string | ||||
| 	GetBranch() string | ||||
| 	GetBuildUrl() string | ||||
| 	GetBuildId() string | ||||
| 	GetJobUrl() string | ||||
| 	GetBuildURL() string | ||||
| 	GetBuildID() string | ||||
| 	GetJobURL() string | ||||
| 	GetJobName() string | ||||
| 	GetCommit() string | ||||
| 	GetPullRequestConfig() PullRequestConfig | ||||
| 	GetRepoUrl() string | ||||
| 	GetRepoURL() string | ||||
| 	IsPullRequest() bool | ||||
| 	GetLog() ([]byte, error) | ||||
| 	GetPipelineStartTime() time.Time | ||||
| 	GetBuildStatus() string | ||||
| 	GetBuildReason() string | ||||
| } | ||||
|  | ||||
| type PullRequestConfig struct { | ||||
| @@ -41,6 +42,7 @@ type PullRequestConfig struct { | ||||
| 	Key    string | ||||
| } | ||||
|  | ||||
| // OrchestratorSettings struct to set orchestrator specific settings e.g. Jenkins credentials | ||||
| type OrchestratorSettings struct { | ||||
| 	JenkinsUser  string | ||||
| 	JenkinsToken string | ||||
| @@ -60,6 +62,7 @@ func NewOrchestratorSpecificConfigProvider() (OrchestratorSpecificConfigProvidin | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // DetectOrchestrator returns the name of the current orchestrator e.g. Jenkins, Azure, Unknown | ||||
| func DetectOrchestrator() Orchestrator { | ||||
| 	if isAzure() { | ||||
| 		return Orchestrator(AzureDevOps) | ||||
|   | ||||
| @@ -7,75 +7,96 @@ import ( | ||||
|  | ||||
| type UnknownOrchestratorConfigProvider struct{} | ||||
|  | ||||
| // InitOrchestratorProvider returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| } | ||||
|  | ||||
| // OrchestratorVersion returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) OrchestratorVersion() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "N/A" | ||||
| } | ||||
|  | ||||
| func (u *UnknownOrchestratorConfigProvider) GetBuildStatus() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "SUCCESS" | ||||
| } | ||||
|  | ||||
| func (u *UnknownOrchestratorConfigProvider) GetBuildId() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| // GetBuildStatus returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetBuildStatus() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "FAILURE" | ||||
| } | ||||
|  | ||||
| // GetBuildReason returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetBuildReason() string { | ||||
| 	log.Entry().Infof("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| // GetBuildID returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetBuildID() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| // GetJobName returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetJobName() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| // OrchestratorType returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) OrchestratorType() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "Unknown" | ||||
| } | ||||
|  | ||||
| // GetLog returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetLog() ([]byte, error) { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return nil, nil | ||||
| 	return []byte{}, nil | ||||
| } | ||||
|  | ||||
| // GetPipelineStartTime returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetPipelineStartTime() time.Time { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	timestamp, _ := time.Parse(time.UnixDate, "Wed Feb 25 11:06:39 PST 1970") | ||||
| 	return timestamp | ||||
| 	return time.Time{}.UTC() | ||||
| } | ||||
|  | ||||
| // GetStageName returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetStageName() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| // GetBranch returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetBranch() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| func (u *UnknownOrchestratorConfigProvider) GetBuildUrl() string { | ||||
| // GetBuildURL returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetBuildURL() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| func (u *UnknownOrchestratorConfigProvider) GetJobUrl() string { | ||||
| // GetJobURL returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetJobURL() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| // GetCommit returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetCommit() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| func (u *UnknownOrchestratorConfigProvider) GetRepoUrl() string { | ||||
| // GetRepoURL returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetRepoURL() string { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return "n/a" | ||||
| } | ||||
|  | ||||
| // GetPullRequestConfig returns n/a for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) GetPullRequestConfig() PullRequestConfig { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return PullRequestConfig{ | ||||
| @@ -85,6 +106,7 @@ func (u *UnknownOrchestratorConfigProvider) GetPullRequestConfig() PullRequestCo | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // IsPullRequest returns false for the unknownOrchestrator | ||||
| func (u *UnknownOrchestratorConfigProvider) IsPullRequest() bool { | ||||
| 	log.Entry().Warning("Unknown orchestrator - returning default values.") | ||||
| 	return false | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package orchestrator | ||||
| import ( | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| @@ -15,10 +16,10 @@ func TestUnknownOrchestrator(t *testing.T) { | ||||
| 		p, _ := NewOrchestratorSpecificConfigProvider() | ||||
|  | ||||
| 		assert.False(t, p.IsPullRequest()) | ||||
| 		assert.Equal(t, "n/a", p.GetBuildUrl()) | ||||
| 		assert.Equal(t, "n/a", p.GetBuildURL()) | ||||
| 		assert.Equal(t, "n/a", p.GetBranch()) | ||||
| 		assert.Equal(t, "n/a", p.GetCommit()) | ||||
| 		assert.Equal(t, "n/a", p.GetRepoUrl()) | ||||
| 		assert.Equal(t, "n/a", p.GetRepoURL()) | ||||
| 		assert.Equal(t, "Unknown", p.OrchestratorType()) | ||||
| 	}) | ||||
|  | ||||
| @@ -34,4 +35,24 @@ func TestUnknownOrchestrator(t *testing.T) { | ||||
| 		assert.Equal(t, "n/a", c.Base) | ||||
| 		assert.Equal(t, "n/a", c.Key) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("env variables", func(t *testing.T) { | ||||
| 		defer resetEnv(os.Environ()) | ||||
| 		os.Clearenv() | ||||
|  | ||||
| 		p := UnknownOrchestratorConfigProvider{} | ||||
|  | ||||
| 		assert.Equal(t, "n/a", p.OrchestratorVersion()) | ||||
| 		assert.Equal(t, "n/a", p.GetBuildID()) | ||||
| 		assert.Equal(t, "n/a", p.GetJobName()) | ||||
| 		assert.Equal(t, "Unknown", p.OrchestratorType()) | ||||
| 		assert.Equal(t, time.Time{}.UTC(), p.GetPipelineStartTime()) | ||||
| 		assert.Equal(t, "FAILURE", p.GetBuildStatus()) | ||||
| 		assert.Equal(t, "n/a", p.GetRepoURL()) | ||||
| 		assert.Equal(t, "n/a", p.GetBuildURL()) | ||||
| 		assert.Equal(t, "n/a", p.GetStageName()) | ||||
| 		log, err := p.GetLog() | ||||
| 		assert.Equal(t, []byte{}, log) | ||||
| 		assert.Equal(t, nil, err) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -14,8 +14,8 @@ type BaseData struct { | ||||
| 	URL             string `json:"url"` | ||||
| 	StepName        string `json:"e_3"` // set by step generator | ||||
| 	StageName       string `json:"e_10"` | ||||
| 	PipelineURLHash string `json:"e_4"`  // defaults to sha1 of provider.GetBuildUrl() | ||||
| 	BuildURLHash    string `json:"e_5"`  // defaults to sha1 of provider.GetJobUrl() | ||||
| 	PipelineURLHash string `json:"e_4"`  // defaults to sha1 of provider.GetBuildURL() | ||||
| 	BuildURLHash    string `json:"e_5"`  // defaults to sha1 of provider.GetJobURL() | ||||
| 	Orchestrator    string `json:"e_14"` // defaults to provider.OrchestratorType() | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -88,13 +88,13 @@ func (t *Telemetry) Initialize(telemetryDisabled bool, stepName string) { | ||||
| } | ||||
|  | ||||
| func (t *Telemetry) getPipelineURLHash() string { | ||||
| 	jobUrl := t.provider.GetJobUrl() | ||||
| 	return t.toSha1OrNA(jobUrl) | ||||
| 	jobURL := t.provider.GetJobURL() | ||||
| 	return t.toSha1OrNA(jobURL) | ||||
| } | ||||
|  | ||||
| func (t *Telemetry) getBuildURLHash() string { | ||||
| 	buildUrl := t.provider.GetBuildUrl() | ||||
| 	return t.toSha1OrNA(buildUrl) | ||||
| 	buildURL := t.provider.GetBuildURL() | ||||
| 	return t.toSha1OrNA(buildURL) | ||||
| } | ||||
|  | ||||
| func (t *Telemetry) toSha1OrNA(input string) string { | ||||
| @@ -161,7 +161,7 @@ func (t *Telemetry) logStepTelemetryData() { | ||||
| 		StepDuration:    t.data.CustomData.Duration, | ||||
| 		ErrorCategory:   t.data.CustomData.ErrorCategory, | ||||
| 		ErrorDetail:     fatalError, | ||||
| 		CorrelationID:   t.provider.GetBuildUrl(), | ||||
| 		CorrelationID:   t.provider.GetBuildURL(), | ||||
| 		PiperCommitHash: t.data.CustomData.PiperCommitHash, | ||||
| 	} | ||||
| 	stepTelemetryJSON, err := json.Marshal(stepTelemetryData) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user