1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +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:
ffeldmann 2022-03-28 09:52:15 +02:00 committed by GitHub
parent 5926aa7f77
commit ccc1c976ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1081 additions and 202 deletions

View File

@ -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")

View 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>")

View File

@ -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"
}

View File

@ -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()")
})
}
}

View File

@ -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")
}

View File

@ -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())
})

View File

@ -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")
}

View File

@ -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)
})
}
}

View File

@ -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)

View File

@ -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

View File

@ -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)
})
}

View File

@ -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()
}

View File

@ -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)