github.com/jaylevin/jenkins-library@v1.230.4/pkg/orchestrator/azureDevOps.go (about)

     1  package orchestrator
     2  
     3  import (
     4  	piperHttp "github.com/SAP/jenkins-library/pkg/http"
     5  	"github.com/SAP/jenkins-library/pkg/log"
     6  	"io/ioutil"
     7  	"os"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  )
    12  
    13  type AzureDevOpsConfigProvider struct {
    14  	client         piperHttp.Client
    15  	options        piperHttp.ClientOptions
    16  	apiInformation map[string]interface{}
    17  }
    18  
    19  // InitOrchestratorProvider initializes http client for AzureDevopsConfigProvider
    20  func (a *AzureDevOpsConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) {
    21  	a.client = piperHttp.Client{}
    22  	a.options = piperHttp.ClientOptions{
    23  		Username:         "",
    24  		Password:         settings.AzureToken,
    25  		MaxRetries:       3,
    26  		TransportTimeout: time.Second * 10,
    27  	}
    28  	a.client.SetOptions(a.options)
    29  	log.Entry().Debug("Successfully initialized Azure config provider")
    30  }
    31  
    32  // fetchAPIInformation fetches Azure API information of current build
    33  func (a *AzureDevOpsConfigProvider) fetchAPIInformation() {
    34  	// if apiInformation is empty fill it otherwise do nothing
    35  	if len(a.apiInformation) == 0 {
    36  		log.Entry().Debugf("apiInformation is empty, getting infos from API")
    37  		URL := a.getSystemCollectionURI() + a.getTeamProjectID() + "/_apis/build/builds/" + a.getAzureBuildID() + "/"
    38  		log.Entry().Debugf("API URL: %s", URL)
    39  		response, err := a.client.GetRequest(URL, nil, nil)
    40  		if err != nil {
    41  			log.Entry().Error("failed to get HTTP response, returning empty API information", err)
    42  			a.apiInformation = map[string]interface{}{}
    43  			return
    44  		} else if response.StatusCode != 200 { //http.StatusNoContent
    45  			log.Entry().Errorf("response code is %v, could not get API information from AzureDevOps. Returning with empty interface.", response.StatusCode)
    46  			a.apiInformation = map[string]interface{}{}
    47  			return
    48  		}
    49  
    50  		err = piperHttp.ParseHTTPResponseBodyJSON(response, &a.apiInformation)
    51  		if err != nil {
    52  			log.Entry().Error("failed to parse HTTP response, returning with empty interface", err)
    53  			a.apiInformation = map[string]interface{}{}
    54  			return
    55  		}
    56  		log.Entry().Debugf("successfully retrieved apiInformation")
    57  	} else {
    58  		log.Entry().Debugf("apiInformation already set")
    59  	}
    60  }
    61  
    62  // getSystemCollectionURI returns the URI of the TFS collection or Azure DevOps organization e.g. https://dev.azure.com/fabrikamfiber/
    63  func (a *AzureDevOpsConfigProvider) getSystemCollectionURI() string {
    64  	return getEnv("SYSTEM_COLLECTIONURI", "n/a")
    65  }
    66  
    67  // getTeamProjectID is the name of the project that contains this build e.g. 123a4567-ab1c-12a1-1234-123456ab7890
    68  func (a *AzureDevOpsConfigProvider) getTeamProjectID() string {
    69  	return getEnv("SYSTEM_TEAMPROJECTID", "n/a")
    70  }
    71  
    72  // getAzureBuildID returns the id of the build, e.g. 1234
    73  func (a *AzureDevOpsConfigProvider) getAzureBuildID() string {
    74  	// INFO: Private function only used for API requests, buildId for e.g. reporting
    75  	// is GetBuildNumber to align with the UI of ADO
    76  	return getEnv("BUILD_BUILDID", "n/a")
    77  }
    78  
    79  // GetJobName returns the pipeline job name, currently org/repo
    80  func (a *AzureDevOpsConfigProvider) GetJobName() string {
    81  	return getEnv("BUILD_REPOSITORY_NAME", "n/a")
    82  }
    83  
    84  // OrchestratorVersion returns the agent version on ADO
    85  func (a *AzureDevOpsConfigProvider) OrchestratorVersion() string {
    86  	return getEnv("AGENT_VERSION", "n/a")
    87  }
    88  
    89  // OrchestratorType returns the orchestrator name e.g. Azure/GitHubActions/Jenkins
    90  func (a *AzureDevOpsConfigProvider) OrchestratorType() string {
    91  	return "Azure"
    92  }
    93  
    94  //GetBuildStatus returns status of the build. Return variables are aligned with Jenkins build statuses.
    95  func (a *AzureDevOpsConfigProvider) GetBuildStatus() string {
    96  	// cases to align with Jenkins: SUCCESS, FAILURE, NOT_BUILD, ABORTED
    97  	switch buildStatus := getEnv("AGENT_JOBSTATUS", "FAILURE"); buildStatus {
    98  	case "Succeeded":
    99  		return "SUCCESS"
   100  	case "Canceled":
   101  		return "ABORTED"
   102  	default:
   103  		// Failed, SucceededWithIssues
   104  		return "FAILURE"
   105  	}
   106  }
   107  
   108  // GetLog returns the whole logfile for the current pipeline run
   109  func (a *AzureDevOpsConfigProvider) GetLog() ([]byte, error) {
   110  	URL := a.getSystemCollectionURI() + a.getTeamProjectID() + "/_apis/build/builds/" + a.getAzureBuildID() + "/logs"
   111  
   112  	response, err := a.client.GetRequest(URL, nil, nil)
   113  
   114  	if err != nil {
   115  		log.Entry().Error("failed to get HTTP response: ", err)
   116  		return []byte{}, err
   117  	}
   118  	if response.StatusCode != 200 { //http.StatusNoContent -> also empty log!
   119  		log.Entry().Errorf("response-Code is %v, could not get log information from AzureDevOps, returning with empty log.", response.StatusCode)
   120  		return []byte{}, nil
   121  	}
   122  	var responseInterface map[string]interface{}
   123  	err = piperHttp.ParseHTTPResponseBodyJSON(response, &responseInterface)
   124  	if err != nil {
   125  		log.Entry().Error("failed to parse http response: ", err)
   126  		return []byte{}, err
   127  	}
   128  	// check if response interface is empty or non-existent
   129  	var logCount int
   130  	if val, ok := responseInterface["count"]; ok {
   131  		logCount = int(val.(float64))
   132  	} else {
   133  		log.Entry().Error("log count variable not found, returning empty log")
   134  		return []byte{}, err
   135  	}
   136  	var logs []byte
   137  	for i := 1; i <= logCount; i++ {
   138  		counter := strconv.Itoa(i)
   139  		logURL := URL + "/" + counter
   140  		log.Entry().Debugf("Getting log no.: %d  from %v", i, logURL)
   141  		response, err := a.client.GetRequest(logURL, nil, nil)
   142  		if err != nil {
   143  			log.Entry().Error("failed to get log", err)
   144  			return []byte{}, err
   145  		}
   146  		if response.StatusCode != 200 { //http.StatusNoContent -> also empty log!
   147  			log.Entry().Errorf("response code is %v, could not get log information from AzureDevOps ", response.StatusCode)
   148  			return []byte{}, err
   149  		}
   150  		content, err := ioutil.ReadAll(response.Body)
   151  		if err != nil {
   152  			log.Entry().Error("failed to parse http response", err)
   153  			return []byte{}, err
   154  		}
   155  		logs = append(logs, content...)
   156  	}
   157  
   158  	return logs, nil
   159  }
   160  
   161  // GetPipelineStartTime returns the pipeline start time in UTC
   162  func (a *AzureDevOpsConfigProvider) GetPipelineStartTime() time.Time {
   163  	//"2022-03-18T07:30:31.1915758Z"
   164  	a.fetchAPIInformation()
   165  	if val, ok := a.apiInformation["startTime"]; ok {
   166  		parsed, err := time.Parse(time.RFC3339, val.(string))
   167  		if err != nil {
   168  			log.Entry().Errorf("could not parse timestamp, %v", err)
   169  			parsed = time.Time{}
   170  		}
   171  		return parsed.UTC()
   172  	}
   173  	return time.Time{}.UTC()
   174  }
   175  
   176  // GetBuildID returns the BuildNumber displayed in the ADO UI
   177  func (a *AzureDevOpsConfigProvider) GetBuildID() string {
   178  	// INFO: ADO has BUILD_ID and buildNumber, as buildNumber is used in the UI we return this value
   179  	// for the buildID used only for API requests we have a private method getAzureBuildID
   180  	// example: buildNumber: 20220318.16 buildId: 76443
   181  	return getEnv("BUILD_BUILDNUMBER", "n/a")
   182  }
   183  
   184  // GetStageName returns the human-readable name given to a stage. e.g. "Promote" or "Init"
   185  func (a *AzureDevOpsConfigProvider) GetStageName() string {
   186  	return getEnv("SYSTEM_STAGEDISPLAYNAME", "n/a")
   187  }
   188  
   189  // GetBranch returns the source branch name, e.g. main
   190  func (a *AzureDevOpsConfigProvider) GetBranch() string {
   191  	tmp := getEnv("BUILD_SOURCEBRANCH", "n/a")
   192  	return strings.TrimPrefix(tmp, "refs/heads/")
   193  }
   194  
   195  // GetBuildURL returns the builds URL e.g. https://dev.azure.com/fabrikamfiber/your-repo-name/_build/results?buildId=1234
   196  func (a *AzureDevOpsConfigProvider) GetBuildURL() string {
   197  	return os.Getenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI") + os.Getenv("SYSTEM_TEAMPROJECT") + "/" + os.Getenv("SYSTEM_DEFINITIONNAME") + "/_build/results?buildId=" + a.getAzureBuildID()
   198  }
   199  
   200  // GetJobURL returns tje current job url e.g. https://dev.azure.com/fabrikamfiber/your-repo-name/_build?definitionId=1234
   201  func (a *AzureDevOpsConfigProvider) GetJobURL() string {
   202  	// TODO: Check if this is the correct URL
   203  	return os.Getenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI") + os.Getenv("SYSTEM_TEAMPROJECT") + "/" + os.Getenv("SYSTEM_DEFINITIONNAME") + "/_build?definitionId=" + os.Getenv("SYSTEM_DEFINITIONID")
   204  }
   205  
   206  // GetCommit returns commit SHA of current build
   207  func (a *AzureDevOpsConfigProvider) GetCommit() string {
   208  	return getEnv("BUILD_SOURCEVERSION", "n/a")
   209  }
   210  
   211  // GetRepoURL returns current repo URL e.g. https://github.com/SAP/jenkins-library
   212  func (a *AzureDevOpsConfigProvider) GetRepoURL() string {
   213  	return getEnv("BUILD_REPOSITORY_URI", "n/a")
   214  }
   215  
   216  // GetBuildReason returns the build reason
   217  func (a *AzureDevOpsConfigProvider) GetBuildReason() string {
   218  	// https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
   219  	return getEnv("BUILD_REASON", "n/a")
   220  }
   221  
   222  // GetPullRequestConfig returns pull request configuration
   223  func (a *AzureDevOpsConfigProvider) GetPullRequestConfig() PullRequestConfig {
   224  	prKey := getEnv("SYSTEM_PULLREQUEST_PULLREQUESTID", "n/a")
   225  
   226  	// This variable is populated for pull requests which have a different pull request ID and pull request number.
   227  	// In this case the pull request ID will contain an internal numeric ID and the pull request number will be provided
   228  	// as part of the 'SYSTEM_PULLREQUEST_PULLREQUESTNUMBER' environment variable.
   229  	prNumber, prNumberEnvVarSet := os.LookupEnv("SYSTEM_PULLREQUEST_PULLREQUESTNUMBER")
   230  	if prNumberEnvVarSet == true {
   231  		prKey = prNumber
   232  	}
   233  
   234  	return PullRequestConfig{
   235  		Branch: os.Getenv("SYSTEM_PULLREQUEST_SOURCEBRANCH"),
   236  		Base:   os.Getenv("SYSTEM_PULLREQUEST_TARGETBRANCH"),
   237  		Key:    prKey,
   238  	}
   239  }
   240  
   241  // IsPullRequest indicates whether the current build is a PR
   242  func (a *AzureDevOpsConfigProvider) IsPullRequest() bool {
   243  	return getEnv("BUILD_REASON", "n/a") == "PullRequest"
   244  }
   245  
   246  func isAzure() bool {
   247  	envVars := []string{"AZURE_HTTP_USER_AGENT"}
   248  	return areIndicatingEnvVarsSet(envVars)
   249  }