|
1 | | -using Microsoft.ApplicationInsights.Extensibility; |
2 | | -using Microsoft.ApplicationInsights; |
3 | | -using Microsoft.AspNetCore.Http; |
4 | | -using Microsoft.Extensions.Caching.Memory; |
| 1 | +using Azure.Core; |
| 2 | +using Azure.Identity; |
| 3 | +using Microsoft.ApplicationInsights; |
5 | 4 | using Microsoft.Extensions.Configuration; |
6 | | -using Microsoft.TeamFoundation.Build.WebApi; |
7 | | -using Microsoft.TeamFoundation.Core.WebApi; |
8 | | -using Microsoft.VisualStudio.Services.Common; |
9 | | -using Microsoft.VisualStudio.Services.WebApi; |
10 | | -using Newtonsoft.Json; |
11 | | -using Octokit; |
| 5 | +using Microsoft.TeamFoundation.Build.WebApi; |
| 6 | +using Microsoft.TeamFoundation.Core.WebApi; |
| 7 | +using Microsoft.VisualStudio.Services.Client; |
| 8 | +using Microsoft.VisualStudio.Services.WebApi; |
| 9 | +using Newtonsoft.Json; |
| 10 | +using Polly; |
12 | 11 | using System; |
13 | 12 | using System.Collections.Generic; |
14 | 13 | using System.IO; |
15 | 14 | using System.Linq; |
16 | | -using System.Net; |
17 | 15 | using System.Net.Http; |
18 | 16 | using System.Net.Http.Headers; |
19 | | -using System.Text.Json; |
| 17 | +using System.Threading; |
20 | 18 | using System.Threading.Tasks; |
21 | 19 |
|
22 | 20 | namespace APIViewWeb.Repositories |
23 | 21 | { |
24 | 22 | public class DevopsArtifactRepository : IDevopsArtifactRepository |
25 | 23 | { |
26 | | - private readonly HttpClient _devopsClient; |
27 | 24 | private readonly IConfiguration _configuration; |
28 | | - private readonly string _devopsAccessToken; |
29 | 25 | private readonly string _hostUrl; |
30 | 26 | private readonly TelemetryClient _telemetryClient; |
31 | 27 |
|
32 | 28 | public DevopsArtifactRepository(IConfiguration configuration, TelemetryClient telemetryClient) |
33 | 29 | { |
34 | 30 | _configuration = configuration; |
35 | | - _devopsAccessToken = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", _configuration["Azure-Devops-PAT"]))); |
36 | 31 | _hostUrl = _configuration["APIVIew-Host-Url"]; |
37 | 32 | _telemetryClient = telemetryClient; |
38 | | - |
39 | | - _devopsClient = new HttpClient(); |
40 | | - _devopsClient.DefaultRequestHeaders.Accept.Clear(); |
41 | | - _devopsClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); |
42 | | - _devopsClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", _devopsAccessToken); |
43 | 33 | } |
44 | 34 |
|
45 | 35 | public async Task<Stream> DownloadPackageArtifact(string repoName, string buildId, string artifactName, string filePath, string project, string format= "file") |
46 | 36 | { |
47 | | - var downloadUrl = await GetDownloadArtifactUrl(repoName, buildId, artifactName, project); |
48 | | - if (!string.IsNullOrEmpty(downloadUrl)) |
| 37 | + var downloadUrl = await getDownloadArtifactUrl(buildId, artifactName, project); |
| 38 | + if (string.IsNullOrEmpty(downloadUrl)) |
| 39 | + { |
| 40 | + throw new Exception(string.Format("Failed to get download url for artifact {0} in build {1} in project {2}", artifactName, buildId, project)); |
| 41 | + } |
| 42 | + |
| 43 | + if(!string.IsNullOrEmpty(filePath)) |
49 | 44 | { |
50 | | - if(!string.IsNullOrEmpty(filePath)) |
| 45 | + if (!filePath.StartsWith("/")) |
51 | 46 | { |
52 | | - if (!filePath.StartsWith("/")) |
53 | | - { |
54 | | - filePath = "/" + filePath; |
55 | | - } |
56 | | - downloadUrl = downloadUrl.Split("?")[0] + "?format=" + format + "&subPath=" + filePath; |
| 47 | + filePath = "/" + filePath; |
57 | 48 | } |
58 | | - |
59 | | - var downloadResp = await GetFromDevopsAsync(downloadUrl); |
60 | | - downloadResp.EnsureSuccessStatusCode(); |
61 | | - return await downloadResp.Content.ReadAsStreamAsync(); |
| 49 | + downloadUrl = downloadUrl.Split("?")[0] + "?format=" + format + "&subPath=" + filePath; |
62 | 50 | } |
63 | | - return null; |
| 51 | + |
| 52 | + HttpResponseMessage downloadResp = await GetFromDevopsAsync(downloadUrl); |
| 53 | + downloadResp.EnsureSuccessStatusCode(); |
| 54 | + return await downloadResp.Content.ReadAsStreamAsync(); |
64 | 55 | } |
65 | 56 |
|
66 | | - private async Task<HttpResponseMessage> GetFromDevopsAsync(string request) |
| 57 | + private async Task<string> getDownloadArtifactUrl(string buildId, string artifactName, string project) |
67 | 58 | { |
68 | | - var downloadResp = await _devopsClient.GetAsync(request); |
69 | | - |
70 | | - |
71 | | - if (!downloadResp.IsSuccessStatusCode) |
72 | | - { |
73 | | - var retryAfter = downloadResp.Headers.GetValues("Retry-After"); |
74 | | - var rateLimitResource = downloadResp.Headers.GetValues("X-RateLimit-Resource"); |
75 | | - var rateLimitDelay = downloadResp.Headers.GetValues("X-RateLimit-Delay"); |
76 | | - var rateLimitLimit = downloadResp.Headers .GetValues("X-RateLimit-Limit"); |
77 | | - var rateLimitRemaining = downloadResp.Headers.GetValues("X-RateLimit-Remaining"); |
78 | | - var rateLimitReset = downloadResp.Headers.GetValues("X-RateLimit-Reset"); |
79 | | - |
80 | | - var traceMessage = $"request: {request} failed with statusCode: {downloadResp.StatusCode}," + |
81 | | - $"retryAfter: {retryAfter.FirstOrDefault()}, rateLimitResource: {rateLimitResource.FirstOrDefault()}, rateLimitDelay: {rateLimitDelay.FirstOrDefault()}," + |
82 | | - $"rateLimitLimit: {rateLimitLimit.FirstOrDefault()}, rateLimitRemaining: {rateLimitRemaining.FirstOrDefault()}, rateLimitReset: {rateLimitReset.FirstOrDefault()}"; |
83 | | - |
84 | | - _telemetryClient.TrackTrace(traceMessage); |
85 | | - } |
| 59 | + var connection = await CreateVssConnection(); |
| 60 | + var buildClient = connection.GetClient<BuildHttpClient>(); |
| 61 | + var artifact = await buildClient.GetArtifactAsync(project, int.Parse(buildId), artifactName); |
| 62 | + return artifact?.Resource?.DownloadUrl; |
| 63 | + } |
86 | 64 |
|
87 | | - int count = 0; |
88 | | - int[] waitTimes = new int[] { 0, 1, 2, 4, 8, 16, 32, 64, 128, 256 }; |
89 | | - while ((downloadResp.StatusCode == HttpStatusCode.TooManyRequests || downloadResp.StatusCode == HttpStatusCode.BadRequest) && count < waitTimes.Length) |
90 | | - { |
91 | | - _telemetryClient.TrackTrace($"Download request from devops artifact is either throttled or flaky, waiting {waitTimes[count]} seconds before retrying, Retry count: {count}"); |
92 | | - await Task.Delay(TimeSpan.FromSeconds(waitTimes[count])); |
93 | | - downloadResp = await _devopsClient.GetAsync(request); |
94 | | - count++; |
95 | | - } |
96 | | - return downloadResp; |
| 65 | + private async Task<VssConnection> CreateVssConnection() |
| 66 | + { |
| 67 | + var accessToken = await getAccessToken(); |
| 68 | + var token = new VssAadToken("Bearer", accessToken); |
| 69 | + return new VssConnection(new Uri("https://dev.azure.com/azure-sdk/"), new VssAadCredential(token)); |
97 | 70 | } |
98 | 71 |
|
99 | | - private async Task<string> GetDownloadArtifactUrl(string repoName, string buildId, string artifactName, string project) |
| 72 | + private async Task<string> getAccessToken() |
100 | 73 | { |
101 | | - var artifactGetReq = GetArtifactRestAPIForRepo(repoName).Replace("{buildId}", buildId).Replace("{artifactName}", artifactName).Replace("{project}", project); |
102 | | - var response = await GetFromDevopsAsync(artifactGetReq); |
103 | | - response.EnsureSuccessStatusCode(); |
104 | | - var buildResource = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); |
105 | | - if (buildResource == null) |
106 | | - { |
107 | | - return null; |
108 | | - } |
109 | | - return buildResource.RootElement.GetProperty("resource").GetProperty("downloadUrl").GetString(); |
| 74 | + // APIView deployed instances uses managed identity to authenticate requests to Azure DevOps. |
| 75 | + // For local testing, VS will use developer credentials to create token |
| 76 | + var credential = new DefaultAzureCredential(); |
| 77 | + var tokenRequestContext = new TokenRequestContext(VssAadSettings.DefaultScopes); |
| 78 | + var token = await credential.GetTokenAsync(tokenRequestContext, CancellationToken.None); |
| 79 | + return token.Token; |
110 | 80 | } |
111 | 81 |
|
112 | | - private string GetArtifactRestAPIForRepo(string repoName) |
| 82 | + private async Task<HttpResponseMessage> GetFromDevopsAsync(string request) |
113 | 83 | { |
114 | | - var downloadArtifactRestApi = _configuration["download-artifact-rest-api-for-" + repoName]; |
115 | | - if (downloadArtifactRestApi == null) |
| 84 | + var httpClient = new HttpClient(); |
| 85 | + var accessToken = await getAccessToken(); |
| 86 | + httpClient.DefaultRequestHeaders.Accept.Clear(); |
| 87 | + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); |
| 88 | + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); |
| 89 | + var maxRetryAttempts = 10; |
| 90 | + var pauseBetweenFailures = TimeSpan.FromSeconds(2); |
| 91 | + |
| 92 | + var retryPolicy = Policy |
| 93 | + .Handle<HttpRequestException>() |
| 94 | + .WaitAndRetryAsync(maxRetryAttempts, i => pauseBetweenFailures); |
| 95 | + |
| 96 | + HttpResponseMessage downloadResp = null; |
| 97 | + await retryPolicy.ExecuteAsync(async () => |
116 | 98 | { |
117 | | - downloadArtifactRestApi = _configuration["download-artifact-rest-api"]; |
118 | | - } |
119 | | - return downloadArtifactRestApi; |
| 99 | + downloadResp = await httpClient.GetAsync(request); |
| 100 | + }); |
| 101 | + return downloadResp; |
120 | 102 | } |
121 | 103 |
|
122 | 104 | public async Task RunPipeline(string pipelineName, string reviewDetails, string originalStorageUrl) |
123 | 105 | { |
124 | 106 | //Create dictionary of all required parametes to run tools - generate-<language>-apireview pipeline in azure devops |
125 | 107 | var reviewDetailsDict = new Dictionary<string, string> { { "Reviews", reviewDetails }, { "APIViewUrl", _hostUrl }, { "StorageContainerUrl", originalStorageUrl } }; |
126 | | - var devOpsCreds = new VssBasicCredential("nobody", _configuration["Azure-Devops-PAT"]); |
127 | | - var devOpsConnection = new VssConnection(new Uri($"https://dev.azure.com/azure-sdk/"), devOpsCreds); |
| 108 | + var devOpsConnection = await CreateVssConnection(); |
128 | 109 | string projectName = _configuration["Azure-Devops-internal-project"] ?? "internal"; |
129 | 110 |
|
130 | 111 | BuildHttpClient buildClient = await devOpsConnection.GetClientAsync<BuildHttpClient>(); |
131 | 112 | var projectClient = await devOpsConnection.GetClientAsync<ProjectHttpClient>(); |
132 | 113 | string envName = _configuration["apiview-deployment-environment"]; |
133 | 114 | string updatedPipelineName = string.IsNullOrEmpty(envName) ? pipelineName : $"{pipelineName}-{envName}"; |
134 | 115 | int definitionId = await GetPipelineId(updatedPipelineName, buildClient, projectName); |
135 | | - if (definitionId == 0) |
136 | | - { |
137 | | - throw new Exception(string.Format("Azure Devops pipeline is not found with name {0}. Please recheck and ensure pipeline exists with this name", updatedPipelineName)); |
| 116 | + if (definitionId == 0) |
| 117 | + { |
| 118 | + throw new Exception(string.Format("Azure Devops pipeline is not found with name {0}. Please recheck and ensure pipeline exists with this name", updatedPipelineName)); |
138 | 119 | } |
139 | 120 |
|
140 | | - var definition = await buildClient.GetDefinitionAsync(projectName, definitionId); |
| 121 | + var definition = await buildClient.GetDefinitionAsync(projectName, definitionId); |
141 | 122 | var project = await projectClient.GetProject(projectName); |
142 | | - await buildClient.QueueBuildAsync(new Build() |
143 | | - { |
144 | | - Definition = definition, |
145 | | - Project = project, |
146 | | - Parameters = JsonConvert.SerializeObject(reviewDetailsDict) |
| 123 | + await buildClient.QueueBuildAsync(new Build() |
| 124 | + { |
| 125 | + Definition = definition, |
| 126 | + Project = project, |
| 127 | + Parameters = JsonConvert.SerializeObject(reviewDetailsDict) |
147 | 128 | }); |
148 | 129 | } |
149 | 130 |
|
150 | | - private async Task<int> GetPipelineId(string pipelineName, BuildHttpClient client, string projectName) |
151 | | - { |
152 | | - var pipelines = await client.GetFullDefinitionsAsync2(project: projectName); |
153 | | - if (pipelines != null) |
154 | | - { |
155 | | - return pipelines.Single(p => p.Name == pipelineName).Id; |
156 | | - } |
157 | | - return 0; |
| 131 | + private async Task<int> GetPipelineId(string pipelineName, BuildHttpClient client, string projectName) |
| 132 | + { |
| 133 | + var pipelines = await client.GetFullDefinitionsAsync2(project: projectName); |
| 134 | + if (pipelines != null) |
| 135 | + { |
| 136 | + return pipelines.Single(p => p.Name == pipelineName).Id; |
| 137 | + } |
| 138 | + return 0; |
158 | 139 | } |
159 | 140 | } |
160 | 141 | } |
0 commit comments