diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Azure.Sdk.Tools.PipelineWitness.csproj b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Azure.Sdk.Tools.PipelineWitness.csproj
index 44c05d553d9..34e190e3427 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Azure.Sdk.Tools.PipelineWitness.csproj
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Azure.Sdk.Tools.PipelineWitness.csproj
@@ -10,6 +10,7 @@
+
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/BlobUploadProcessor.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/BlobUploadProcessor.cs
index e5453307496..60257722f67 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/BlobUploadProcessor.cs
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/BlobUploadProcessor.cs
@@ -13,11 +13,13 @@ namespace Azure.Sdk.Tools.PipelineWitness
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+ using Azure.Sdk.Tools.PipelineWitness.Configuration;
using Azure.Sdk.Tools.PipelineWitness.Services;
using Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Azure.Storage.Queues;
+
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.TeamFoundation.Build.WebApi;
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/ISecretClientProvider.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/ISecretClientProvider.cs
new file mode 100644
index 00000000000..3b6bb5cd419
--- /dev/null
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/ISecretClientProvider.cs
@@ -0,0 +1,11 @@
+using System;
+
+using Azure.Security.KeyVault.Secrets;
+
+namespace Azure.Sdk.Tools.PipelineWitness.Configuration
+{
+ public interface ISecretClientProvider
+ {
+ SecretClient GetSecretClient(Uri vaultUri);
+ }
+}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/PipelineWitnessSettings.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/PipelineWitnessSettings.cs
similarity index 73%
rename from tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/PipelineWitnessSettings.cs
rename to tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/PipelineWitnessSettings.cs
index f4c46c3d912..2109b592328 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/PipelineWitnessSettings.cs
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/PipelineWitnessSettings.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
-namespace Azure.Sdk.Tools.PipelineWitness
+namespace Azure.Sdk.Tools.PipelineWitness.Configuration
{
public class PipelineWitnessSettings
{
@@ -12,12 +12,17 @@ public class PipelineWitnessSettings
public string KeyVaultUri { get; set; }
///
- /// Gets or sets uri of the storage account use for queue processing
+ /// Gets or sets uri of the cosmos account to use
+ ///
+ public string CosmosAccountUri { get; set; }
+
+ ///
+ /// Gets or sets uri of the storage account to use for queue processing
///
public string QueueStorageAccountUri { get; set; }
///
- /// Gets or sets uri of the blob storage account use for blob export
+ /// Gets or sets uri of the blob storage account to use for blob export
///
public string BlobStorageAccountUri { get; set; }
@@ -80,5 +85,25 @@ public class PipelineWitnessSettings
/// Gets or sets the definition id of the pipeline owners extraction build
///
public int PipelineOwnersDefinitionId { get; set; }
+
+ ///
+ /// Gets or sets the database to use
+ ///
+ public string CosmosDatabase { get; set; }
+
+ ///
+ /// Gets or sets the container to use for async locks
+ ///
+ public string CosmosAsyncLockContainer { get; set; }
+
+ ///
+ /// Gets or sets the authorization key for the Cosmos account
+ ///
+ public string CosmosAuthorizationKey { get; set; }
+
+ ///
+ /// Gets or sets the access token to use for Azure DevOps clients
+ ///
+ public string DevopsAccessToken { get; set; }
}
}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/PostConfigureKeyVaultSettings.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/PostConfigureKeyVaultSettings.cs
new file mode 100644
index 00000000000..10a5200557e
--- /dev/null
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/PostConfigureKeyVaultSettings.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Azure.Sdk.Tools.PipelineWitness.Configuration
+{
+ public class PostConfigureKeyVaultSettings : IPostConfigureOptions where T : class
+ {
+ private static readonly Regex secretRegex = new Regex(@"(?https://.*?\.vault\.azure\.net)/secrets/(?.*)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
+ private readonly ILogger logger;
+ private readonly ISecretClientProvider secretClientProvider;
+
+ public PostConfigureKeyVaultSettings(ILogger> logger, ISecretClientProvider secretClientProvider)
+ {
+ this.logger = logger;
+ this.secretClientProvider = secretClientProvider;
+ }
+
+ public void PostConfigure(string name, T options)
+ {
+ var stringProperties = typeof(T)
+ .GetProperties()
+ .Where(x => x.PropertyType == typeof(string));
+
+ foreach (var property in stringProperties)
+ {
+ var value = (string)property.GetValue(options);
+
+ if (value != null)
+ {
+ var match = secretRegex.Match(value);
+
+ if (match.Success)
+ {
+ var vaultUrl = match.Groups["vault"].Value;
+ var secretName = match.Groups["secret"].Value;
+
+ try
+ {
+ var secretClient = this.secretClientProvider.GetSecretClient(new Uri(vaultUrl));
+ this.logger.LogInformation("Replacing setting property {PropertyName} with value from secret {SecretUrl}", property.Name, value);
+
+ var response = secretClient.GetSecret(secretName);
+ var secret = response.Value;
+
+ property.SetValue(options, secret.Value);
+ }
+ catch (Exception exception)
+ {
+ this.logger.LogError(exception, "Unable to read secret {SecretName} from vault {VaultUrl}", secretName, vaultUrl);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/SecretClientProvider.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/SecretClientProvider.cs
new file mode 100644
index 00000000000..54e69ac9450
--- /dev/null
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/SecretClientProvider.cs
@@ -0,0 +1,22 @@
+using System;
+
+using Azure.Core;
+using Azure.Security.KeyVault.Secrets;
+
+namespace Azure.Sdk.Tools.PipelineWitness.Configuration
+{
+ public class SecretClientProvider : ISecretClientProvider
+ {
+ private readonly TokenCredential tokenCredential;
+
+ public SecretClientProvider(TokenCredential tokenCredential)
+ {
+ this.tokenCredential = tokenCredential;
+ }
+
+ public SecretClient GetSecretClient(Uri vaultUri)
+ {
+ return new SecretClient(vaultUri, this.tokenCredential);
+ }
+ }
+}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/CosmosDatabaseInitializer.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/CosmosDatabaseInitializer.cs
new file mode 100644
index 00000000000..9fa992159b6
--- /dev/null
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/CosmosDatabaseInitializer.cs
@@ -0,0 +1,43 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+using Azure.Sdk.Tools.PipelineWitness.Configuration;
+
+using Microsoft.Azure.Cosmos;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+
+namespace Azure.Sdk.Tools.PipelineWitness
+{
+ internal class CosmosDatabaseInitializer : IHostedService
+ {
+ private readonly CosmosClient cosmosClient;
+ private readonly IOptions options;
+
+ public CosmosDatabaseInitializer(CosmosClient cosmosClient, IOptions options)
+ {
+ this.cosmosClient = cosmosClient;
+ this.options = options;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ var settings = this.options.Value;
+
+ Database database = await this.cosmosClient.CreateDatabaseIfNotExistsAsync(settings.CosmosDatabase, cancellationToken: cancellationToken);
+
+ await database.CreateContainerIfNotExistsAsync(
+ new ContainerProperties
+ {
+ Id = settings.CosmosAsyncLockContainer,
+ PartitionKeyPath = "/id",
+ },
+ cancellationToken: cancellationToken);
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/AzurePipelinesBuildDefinitionWorker.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/AzurePipelinesBuildDefinitionWorker.cs
index 3e2f411e464..f47571155e7 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/AzurePipelinesBuildDefinitionWorker.cs
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/AzurePipelinesBuildDefinitionWorker.cs
@@ -6,6 +6,8 @@
using Microsoft.VisualStudio.Services.WebApi;
using System.Diagnostics;
using Microsoft.Extensions.Options;
+using Azure.Sdk.Tools.PipelineWitness.Services.WorkTokens;
+using Azure.Sdk.Tools.PipelineWitness.Configuration;
namespace Azure.Sdk.Tools.PipelineWitness.Services
{
@@ -14,33 +16,51 @@ public class AzurePipelinesBuildDefinitionWorker : BackgroundService
private readonly ILogger logger;
private readonly BlobUploadProcessor runProcessor;
private readonly IOptions options;
+ private IAsyncLockProvider asyncLockProvider;
public AzurePipelinesBuildDefinitionWorker(
ILogger logger,
BlobUploadProcessor runProcessor,
+ IAsyncLockProvider asyncLockProvider,
IOptions options)
{
this.logger = logger;
this.runProcessor = runProcessor;
this.options = options;
+ this.asyncLockProvider = asyncLockProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
+ var processEvery = TimeSpan.FromMinutes(60);
+
while (true)
{
var stopWatch = Stopwatch.StartNew();
var settings = this.options.Value;
- foreach (var project in settings.Projects)
+ try
+ {
+ await using var asyncLock = await this.asyncLockProvider.GetLockAsync("UpdateBuildDefinitions", processEvery, stoppingToken);
+
+ // if there's no asyncLock, this process has alread completed in the last hour
+ if (asyncLock != null)
+ {
+ foreach (var project in settings.Projects)
+ {
+ await this.runProcessor.UploadBuildDefinitionBlobsAsync(settings.Account, project);
+ }
+ }
+ }
+ catch(Exception ex)
{
- await this.runProcessor.UploadBuildDefinitionBlobsAsync(settings.Account, project);
+ this.logger.LogError(ex, "Error processing build definitions");
}
var duration = settings.BuildDefinitionLoopPeriod - stopWatch.Elapsed;
if (duration > TimeSpan.Zero)
{
- await Task.Delay(duration);
+ await Task.Delay(duration, stoppingToken);
}
}
}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/BuildCompleteQueueWorker.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/BuildCompleteQueueWorker.cs
index 0ece5098db3..387d33be904 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/BuildCompleteQueueWorker.cs
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/BuildCompleteQueueWorker.cs
@@ -2,12 +2,16 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+
+using Azure.Sdk.Tools.PipelineWitness.Configuration;
using Azure.Storage.Queues;
using Azure.Storage.Queues.Models;
+
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+
using Newtonsoft.Json.Linq;
namespace Azure.Sdk.Tools.PipelineWitness.Services
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/EnhancedBuildHttpClient.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/EnhancedBuildHttpClient.cs
index 631ea1dac9d..8fabcb06b0a 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/EnhancedBuildHttpClient.cs
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/EnhancedBuildHttpClient.cs
@@ -7,62 +7,63 @@
using Microsoft.TeamFoundation.Build.WebApi;
using Microsoft.VisualStudio.Services.Common;
-namespace Azure.Sdk.Tools.PipelineWitness.Services;
-
-public class EnhancedBuildHttpClient : BuildHttpClient
-
+namespace Azure.Sdk.Tools.PipelineWitness.Services
{
- public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials)
- : base(baseUrl, credentials)
- {}
+ public class EnhancedBuildHttpClient : BuildHttpClient
- public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings)
- : base(baseUrl, credentials, settings)
- {}
+ {
+ public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials)
+ : base(baseUrl, credentials)
+ {}
- public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials, params DelegatingHandler[] handlers)
- : base(baseUrl, credentials, handlers)
- {}
+ public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings)
+ : base(baseUrl, credentials, settings)
+ {}
- public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings, params DelegatingHandler[] handlers)
- : base(baseUrl, credentials, settings, handlers)
- {}
+ public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials, params DelegatingHandler[] handlers)
+ : base(baseUrl, credentials, handlers)
+ {}
- public EnhancedBuildHttpClient(Uri baseUrl, HttpMessageHandler pipeline, bool disposeHandler)
- : base(baseUrl, pipeline, disposeHandler)
- {}
+ public EnhancedBuildHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings, params DelegatingHandler[] handlers)
+ : base(baseUrl, credentials, settings, handlers)
+ {}
- public override async Task GetArtifactContentZipAsync(
- Guid project,
- int buildId,
- string artifactName,
- object userState = null,
- CancellationToken cancellationToken = default)
- {
- var artifact = await base.GetArtifactAsync(project, buildId, artifactName, userState, cancellationToken);
- return await GetArtifactContentZipAsync(artifact, cancellationToken);
- }
+ public EnhancedBuildHttpClient(Uri baseUrl, HttpMessageHandler pipeline, bool disposeHandler)
+ : base(baseUrl, pipeline, disposeHandler)
+ {}
- public override async Task GetArtifactContentZipAsync(
- string project,
- int buildId,
- string artifactName,
- object userState = null,
- CancellationToken cancellationToken = default)
- {
- var artifact = await base.GetArtifactAsync(project, buildId, artifactName, userState, cancellationToken);
- return await GetArtifactContentZipAsync(artifact, cancellationToken);
- }
+ public override async Task GetArtifactContentZipAsync(
+ Guid project,
+ int buildId,
+ string artifactName,
+ object userState = null,
+ CancellationToken cancellationToken = default)
+ {
+ var artifact = await base.GetArtifactAsync(project, buildId, artifactName, userState, cancellationToken);
+ return await GetArtifactContentZipAsync(artifact, cancellationToken);
+ }
- private async Task GetArtifactContentZipAsync(BuildArtifact artifact, CancellationToken cancellationToken)
- {
- var downloadUrl = artifact?.Resource?.DownloadUrl;
- if (string.IsNullOrWhiteSpace(downloadUrl))
+ public override async Task GetArtifactContentZipAsync(
+ string project,
+ int buildId,
+ string artifactName,
+ object userState = null,
+ CancellationToken cancellationToken = default)
{
- throw new InvalidArtifactDataException("Artifact contained no download url");
+ var artifact = await base.GetArtifactAsync(project, buildId, artifactName, userState, cancellationToken);
+ return await GetArtifactContentZipAsync(artifact, cancellationToken);
}
- var responseStream = await Client.GetStreamAsync(downloadUrl, cancellationToken);
- return responseStream;
+ private async Task GetArtifactContentZipAsync(BuildArtifact artifact, CancellationToken cancellationToken)
+ {
+ var downloadUrl = artifact?.Resource?.DownloadUrl;
+ if (string.IsNullOrWhiteSpace(downloadUrl))
+ {
+ throw new InvalidArtifactDataException("Artifact contained no download url");
+ }
+
+ var responseStream = await Client.GetStreamAsync(downloadUrl, cancellationToken);
+ return responseStream;
+ }
}
}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/QueueWorkerBackgroundService.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/QueueWorkerBackgroundService.cs
index ba26736f419..43b0674e788 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/QueueWorkerBackgroundService.cs
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/QueueWorkerBackgroundService.cs
@@ -3,8 +3,11 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+
+using Azure.Sdk.Tools.PipelineWitness.Configuration;
using Azure.Storage.Queues;
using Azure.Storage.Queues.Models;
+
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.Extensions.Hosting;
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/CosmosAsyncLock.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/CosmosAsyncLock.cs
new file mode 100644
index 00000000000..91d98f6523b
--- /dev/null
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/CosmosAsyncLock.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Cosmos;
+
+namespace Azure.Sdk.Tools.PipelineWitness.Services.WorkTokens
+{
+ class CosmosAsyncLock : IAsyncLock
+ {
+ private readonly string id;
+ private readonly PartitionKey partitionKey;
+ private readonly TimeSpan duration;
+ private readonly Container container;
+ private string etag;
+
+ public CosmosAsyncLock(string id, string etag, TimeSpan duration, Container container)
+ {
+ this.id = id;
+ this.partitionKey = new PartitionKey(id);
+ this.etag = etag;
+ this.duration = duration;
+ this.container = container;
+ }
+
+ public bool ReleaseOnDispose { get; set; }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (ReleaseOnDispose)
+ {
+ try
+ {
+ await this.container.DeleteItemAsync(this.id, this.partitionKey, new ItemRequestOptions { IfMatchEtag = this.etag });
+ }
+ catch(CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
+ {
+ }
+ }
+ }
+
+ public async Task TryRenewAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var response = await this.container.ReplaceItemAsync(
+ new CosmosLockDocument(this.id, this.duration),
+ this.id,
+ this.partitionKey,
+ new ItemRequestOptions { IfMatchEtag = this.etag },
+ cancellationToken);
+
+ if (response?.StatusCode == HttpStatusCode.OK)
+ {
+ this.etag = response.ETag;
+ return true;
+ }
+ }
+ catch (CosmosException ex) when(ex.StatusCode == HttpStatusCode.Conflict)
+ {
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/CosmosAsyncLockProvider.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/CosmosAsyncLockProvider.cs
new file mode 100644
index 00000000000..ff695c4f138
--- /dev/null
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/CosmosAsyncLockProvider.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.Azure.Cosmos;
+
+namespace Azure.Sdk.Tools.PipelineWitness.Services.WorkTokens
+{
+ public class CosmosAsyncLockProvider : IAsyncLockProvider
+ {
+ private readonly Container container;
+
+ public CosmosAsyncLockProvider(CosmosClient cosmosClient, string databaseName, string containerName)
+ {
+ if (cosmosClient == null)
+ {
+ throw new ArgumentNullException(nameof(cosmosClient));
+ }
+
+ this.container = cosmosClient.GetContainer(databaseName, containerName);
+ }
+
+ public async Task GetLockAsync(string id, TimeSpan duration, CancellationToken cancellationToken)
+ {
+ var partitionKey = new PartitionKey(id);
+
+ ItemResponse response;
+
+ try
+ {
+ response = await this.container.ReadItemAsync(id, partitionKey, cancellationToken: cancellationToken);
+ }
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
+ {
+ return await CreateLockAsync(id, duration, cancellationToken);
+ }
+
+ var existingLock = response.Resource;
+
+ if (existingLock.Expiration >= DateTime.UtcNow)
+ {
+ return null;
+ }
+
+ try
+ {
+ response = await this.container.ReplaceItemAsync(
+ new CosmosLockDocument(id, duration),
+ id,
+ partitionKey,
+ new ItemRequestOptions { IfMatchEtag = response.ETag },
+ cancellationToken);
+
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ return new CosmosAsyncLock(id, response.ETag, duration, this.container);
+ }
+ }
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
+ {
+ }
+
+ return null;
+ }
+
+ private async Task CreateLockAsync(string id, TimeSpan duration, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var response = await this.container.CreateItemAsync(
+ new CosmosLockDocument(id, duration),
+ new PartitionKey(id),
+ cancellationToken: cancellationToken);
+
+ return new CosmosAsyncLock(id, response.ETag, duration, this.container);
+ }
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
+ {
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/CosmosLockDocument.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/CosmosLockDocument.cs
new file mode 100644
index 00000000000..8b57daaa6bf
--- /dev/null
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/CosmosLockDocument.cs
@@ -0,0 +1,24 @@
+using System;
+using Newtonsoft.Json;
+
+namespace Azure.Sdk.Tools.PipelineWitness.Services.WorkTokens
+{
+ public class CosmosLockDocument
+ {
+ public CosmosLockDocument()
+ {
+ }
+
+ public CosmosLockDocument(string id, TimeSpan duration)
+ {
+ Id = id;
+ Expiration = DateTime.UtcNow.Add(duration);
+ }
+
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ [JsonProperty("expiration")]
+ public DateTime Expiration { get; set; }
+ }
+}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/IAsyncLock.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/IAsyncLock.cs
new file mode 100644
index 00000000000..74607d13ef8
--- /dev/null
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/IAsyncLock.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Azure.Sdk.Tools.PipelineWitness.Services.WorkTokens
+{
+ public interface IAsyncLock : IAsyncDisposable
+ {
+ bool ReleaseOnDispose { get; set; }
+
+ Task TryRenewAsync(CancellationToken cancellationToken);
+ }
+}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/IAsyncLockProvider.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/IAsyncLockProvider.cs
new file mode 100644
index 00000000000..5589bf3804a
--- /dev/null
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/WorkTokens/IAsyncLockProvider.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Azure.Sdk.Tools.PipelineWitness.Services.WorkTokens
+{
+ public interface IAsyncLockProvider
+ {
+ Task GetLockAsync(string id, TimeSpan duration, CancellationToken cancellationToken);
+ }
+}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Startup.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Startup.cs
index 7112acdfadb..a64b4e7ef50 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Startup.cs
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Startup.cs
@@ -1,15 +1,21 @@
using System;
+
+using Azure.Core;
using Azure.Identity;
using Azure.Sdk.Tools.PipelineWitness.ApplicationInsights;
+using Azure.Sdk.Tools.PipelineWitness.Configuration;
using Azure.Sdk.Tools.PipelineWitness.Services;
using Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis;
-using Azure.Security.KeyVault.Secrets;
+using Azure.Sdk.Tools.PipelineWitness.Services.WorkTokens;
+
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Builder;
+using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
using Microsoft.TeamFoundation.Build.WebApi;
using Microsoft.TeamFoundation.Core.WebApi;
using Microsoft.VisualStudio.Services.Common;
@@ -37,17 +43,22 @@ public static void Configure(WebApplicationBuilder builder)
builder.AddBlobServiceClient(new Uri(settings.BlobStorageAccountUri));
builder.AddQueueServiceClient(new Uri(settings.QueueStorageAccountUri))
.ConfigureOptions(o => o.MessageEncoding = Storage.Queues.QueueMessageEncoding.Base64);
+
+ builder.AddClient((CosmosClientOptions options, IServiceProvider provider) =>
+ {
+ var resolvedSettings = provider.GetRequiredService>().Value;
+ return new CosmosClient(settings.CosmosAccountUri, resolvedSettings.CosmosAuthorizationKey, options);
+ });
});
builder.Services.AddSingleton(provider =>
{
- var secretClient = provider.GetRequiredService();
- KeyVaultSecret secret = secretClient.GetSecret("azure-devops-personal-access-token");
- var credential = new VssBasicCredential("nobody", secret.Value);
- var connection = new VssConnection(new Uri("https://dev.azure.com/azure-sdk"), credential);
- return connection;
+ var resolvedSettings = provider.GetRequiredService>().Value;
+ var credential = new VssBasicCredential("nobody", resolvedSettings.DevopsAccessToken);
+ return new VssConnection(new Uri("https://dev.azure.com/azure-sdk"), credential);
});
+ builder.Services.AddSingleton(provider => new CosmosAsyncLockProvider(provider.GetRequiredService(), settings.CosmosDatabase, settings.CosmosAsyncLockContainer));
builder.Services.AddSingleton(provider => provider.GetRequiredService().GetClient());
builder.Services.AddSingleton(provider => provider.GetRequiredService().GetClient());
builder.Services.AddSingleton(provider => provider.GetRequiredService().GetClient());
@@ -76,8 +87,13 @@ public static void Configure(WebApplicationBuilder builder)
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+
builder.Services.Configure(settingsSection);
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton, PostConfigureKeyVaultSettings>();
+ builder.Services.AddHostedService();
builder.Services.AddHostedService(settings.BuildCompleteWorkerCount);
builder.Services.AddHostedService();
}
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.development.json b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.development.json
index 467aae82ead..cadd0c09d93 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.development.json
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.development.json
@@ -24,6 +24,9 @@
"KeyVaultUri": "https://pipelinewitnessstaging.vault.azure.net/",
"QueueStorageAccountUri": "https://pipelinewitnessstaging.queue.core.windows.net",
"BlobStorageAccountUri": "https://pipelinelogsstaging.blob.core.windows.net",
+ "CosmosAccountUri": "https://pipelinewitnessstaging.documents.azure.com",
+ "CosmosAuthorizationKey": "https://pipelinewitnessstaging.vault.azure.net/secrets/cosmosdb-primary-authorization-key",
+ "DevopsAccessToken": "https://pipelinewitnessstaging.vault.azure.net/secrets/azure-devops-personal-access-token",
"BuildDefinitionLoopPeriod": "00:01:00",
"BuildCompleteWorkerCount": 1,
"BuildLogBundlesWorkerCount": 1
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.json b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.json
index c9b5aa4e775..8a4c53632ac 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.json
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.json
@@ -13,8 +13,13 @@
"KeyVaultUri": "https://pipelinewitnessprod.vault.azure.net/",
"QueueStorageAccountUri": "https://pipelinewitnessprod.queue.core.windows.net",
"BlobStorageAccountUri": "https://azsdkengsyspipelinelogs.blob.core.windows.net",
+ "CosmosAccountUri": "https://pipelinewitnessprod.documents.azure.com",
+ "CosmosAuthorizationKey": "https://pipelinewitnessprod.vault.azure.net/secrets/cosmosdb-primary-authorization-key",
+ "DevopsAccessToken": "https://pipelinewitnessprod.vault.azure.net/secrets/azure-devops-personal-access-token",
+ "CosmosDatabase": "records",
+ "CosmosAsyncLockContainer": "locks",
"BuildCompleteQueueName": "azurepipelines-build-completed",
- "BuildCompleteWorkerCount": 5,
+ "BuildCompleteWorkerCount": 10,
"MessageLeasePeriod": "00:03:00",
"MessageErrorSleepPeriod": "00:00:10",
"MaxDequeueCount": 5,
diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.staging.json b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.staging.json
index b7b1f5c26a2..c56fadaf167 100644
--- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.staging.json
+++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/appsettings.staging.json
@@ -2,6 +2,9 @@
"PipelineWitness": {
"KeyVaultUri": "https://pipelinewitnessstaging.vault.azure.net/",
"QueueStorageAccountUri": "https://pipelinewitnessstaging.queue.core.windows.net",
- "BlobStorageAccountUri": "https://pipelinelogsstaging.blob.core.windows.net"
+ "BlobStorageAccountUri": "https://pipelinelogsstaging.blob.core.windows.net",
+ "CosmosAccountUri": "https://pipelinewitnessstaging.documents.azure.com",
+ "CosmosAuthorizationKey": "https://pipelinewitnessstaging.vault.azure.net/secrets/cosmosdb-primary-authorization-key",
+ "DevopsAccessToken": "https://pipelinewitnessstaging.vault.azure.net/secrets/azure-devops-personal-access-token",
}
}