github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/orchestrator/azureDevOps.go (about)

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