Skip to content

Commit

Permalink
Added distributed locks via a file share (#811)
Browse files Browse the repository at this point in the history
  • Loading branch information
mburumaxwell authored Sep 20, 2023
1 parent e48d1fb commit 6849f19
Showing 6 changed files with 138 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Medallion.Threading;
using Medallion.Threading.FileSystem;
using Tingle.Dependabot.Workflow;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>Extensions on <see cref="IServiceCollection"/>.</summary>
public static class IServiceCollectionExtensions
{
/// <summary>
/// Add <see cref="IDistributedLockProvider"/>, a provider for <see cref="IDistributedLock"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to be configured.</param>
/// <param name="environment">The <see cref="IHostEnvironment"/> to use.</param>
/// <param name="configuration">The root configuration instance from which to pull settings.</param>
/// <returns></returns>
public static IServiceCollection AddDistributedLockProvider(this IServiceCollection services, IHostEnvironment environment, IConfiguration configuration)
{
var configKey = ConfigurationPath.Combine("DistributedLocking", "FilePath");

var path = configuration.GetValue<string?>(configKey);

// when the path is null in development, set one
if (string.IsNullOrWhiteSpace(path) && environment.IsDevelopment())
{
path = Path.Combine(environment.ContentRootPath, "distributed-locks");
}

if (string.IsNullOrWhiteSpace(path))
{
throw new InvalidOperationException($"'{nameof(path)}' must be provided via configuration at '{configKey}'.");
}

services.AddSingleton<IDistributedLockProvider>(new FileDistributedSynchronizationProvider(new(path)));

return services;
}

public static IServiceCollection AddWorkflowServices(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<WorkflowOptions>(configuration);
services.ConfigureOptions<WorkflowConfigureOptions>();

services.AddSingleton<UpdateRunner>();
services.AddSingleton<UpdateScheduler>();

services.AddScoped<AzureDevOpsProvider>();
services.AddScoped<Synchronizer>();

return services;
}
}
11 changes: 3 additions & 8 deletions server/Tingle.Dependabot/Program.cs
Original file line number Diff line number Diff line change
@@ -66,16 +66,11 @@
});
});

// Configure other services
builder.Services.AddMemoryCache();
builder.Services.AddDistributedMemoryCache();

// Configure other services
builder.Services.Configure<WorkflowOptions>(builder.Configuration.GetSection("Workflow"));
builder.Services.ConfigureOptions<WorkflowConfigureOptions>();
builder.Services.AddSingleton<UpdateRunner>();
builder.Services.AddSingleton<UpdateScheduler>();
builder.Services.AddScoped<AzureDevOpsProvider>();
builder.Services.AddScoped<Synchronizer>();
builder.Services.AddDistributedLockProvider(builder.Environment, builder.Configuration);
builder.Services.AddWorkflowServices(builder.Configuration.GetSection("Workflow"));

// Add event bus
var selectedTransport = builder.Configuration.GetValue<EventBusTransportKind?>("EventBus:SelectedTransport");
2 changes: 2 additions & 0 deletions server/Tingle.Dependabot/Tingle.Dependabot.csproj
Original file line number Diff line number Diff line change
@@ -18,8 +18,10 @@
<PackageReference Include="AspNetCore.Authentication.ApiKey" Version="7.0.0" />
<PackageReference Include="AspNetCore.Authentication.Basic" Version="7.0.0" />
<PackageReference Include="Azure.Identity" Version="1.10.1" />
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.16.1" /> <!-- Allows for faster updates -->
<PackageReference Include="Azure.Monitor.Query" Version="1.2.0" />
<PackageReference Include="Azure.ResourceManager.AppContainers" Version="1.1.0" />
<PackageReference Include="DistributedLock.FileSystem" Version="1.0.1" />
<PackageReference Include="FlakeId" Version="1.1.1" />
<PackageReference Include="Macross.Json.Extensions" Version="3.0.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
65 changes: 35 additions & 30 deletions server/main.bicep
Original file line number Diff line number Diff line change
@@ -57,11 +57,16 @@ param minReplicas int = 1 // necessary for in-memory scheduling
@description('The maximum number of replicas')
param maxReplicas int = 1

var fileShares = [
{ name: 'certs' }
{ name: 'distributed-locks', writeable: true }
{ name: 'working-dir', writeable: true }
]

var sqlServerAdministratorLogin = uniqueString(resourceGroup().id) // e.g. zecnx476et7xm (13 characters)
var sqlServerAdministratorLoginPassword = '${skip(uniqueString(resourceGroup().id), 5)}%${uniqueString('sql-password', resourceGroup().id)}' // e.g. abcde%zecnx476et7xm (19 characters)
// avoid conflicts across multiple deployments for resources that generate FQDN based on the name
var collisionSuffix = uniqueString(resourceGroup().id) // e.g. zecnx476et7xm (13 characters)
var fileShareName = 'working-dir'
var queueNames = [
'process-synchronization'
'repository-created'
@@ -94,10 +99,7 @@ resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-
resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2021-11-01' = {
name: '${name}-${collisionSuffix}'
location: location
properties: {
disableLocalAuth: false
zoneRedundant: false
}
properties: { disableLocalAuth: false, zoneRedundant: false }
sku: { name: 'Basic' }

resource authorizationRule 'AuthorizationRules' existing = { name: 'RootManageSharedAccessKey' }
@@ -122,7 +124,16 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
resource fileServices 'fileServices' existing = {
name: 'default'

resource workingDir 'shares' = { name: fileShareName }
resource shares 'shares' = [for fs in fileShares: {
name: fs.name
properties: {
accessTier: contains(fs, 'accessTier') ? fs.accessTier : 'TransactionOptimized'
// container apps does not support NFS
// https://github.com/microsoft/azure-container-apps/issues/717
// enabledProtocols: contains(fs, 'enabledProtocols') ? fs.enabledProtocols : 'SMB'
shareQuota: contains(fs, 'shareQuota') ? fs.shareQuota : 1
}
}]
}
}

@@ -147,18 +158,13 @@ resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
resource sqlServerFirewallRuleForAzure 'Microsoft.Sql/servers/firewallRules@2022-08-01-preview' = {
parent: sqlServer
name: 'AllowAllWindowsAzureIps'
properties: {
endIpAddress: '0.0.0.0'
startIpAddress: '0.0.0.0'
}
properties: { endIpAddress: '0.0.0.0', startIpAddress: '0.0.0.0' }
}
resource sqlServerDatabase 'Microsoft.Sql/servers/databases@2022-05-01-preview' = {
parent: sqlServer
name: name
location: location
sku: {
name: 'Basic'
}
sku: { name: 'Basic' }
properties: {
collation: 'SQL_Latin1_General_CP1_CI_AS'
maxSizeBytes: 2147483648
@@ -204,17 +210,17 @@ resource appEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
}
}

resource workingDir 'storages' = {
name: fileShareName
resource storages 'storages' = [for fs in fileShares: {
name: fs.name
properties: {
azureFile: {
accessMode: 'ReadWrite'
shareName: fileShareName
accountName: storageAccount.name
accountKey: storageAccount.listKeys().keys[0].value
shareName: fs.name
accessMode: contains(fs, 'writeable') && bool(fs.writeable) ? 'ReadWrite' : 'ReadOnly'
}
}
}
}]
}

/* Application Insights */
@@ -235,16 +241,7 @@ resource app 'Microsoft.App/containerApps@2023-05-01' = {
properties: {
managedEnvironmentId: appEnvironment.id
configuration: {
ingress: {
external: true
targetPort: 80
traffic: [
{
latestRevision: true
weight: 100
}
]
}
ingress: { external: true, targetPort: 80, traffic: [ { latestRevision: true, weight: 100 } ] }
secrets: [
{ name: 'connection-strings-application-insights', value: appInsights.properties.ConnectionString }
{
@@ -272,7 +269,10 @@ resource app 'Microsoft.App/containerApps@2023-05-01' = {
{
image: 'ghcr.io/tinglesoftware/dependabot-server:${imageTag}'
name: 'dependabot'
volumeMounts: [ { mountPath: '/mnt/dependabot', volumeName: fileShareName } ]
volumeMounts: [
{ mountPath: '/mnt/dependabot', volumeName: 'working-dir' }
{ mountPath: '/mnt/distributed-locks', volumeName: 'distributed-locks' }
]
env: [
{ name: 'AZURE_CLIENT_ID', value: managedIdentity.properties.clientId } // Specifies the User-Assigned Managed Identity to use. Without this, the app attempt to use the system assigned one.
{ name: 'ASPNETCORE_FORWARDEDHEADERS_ENABLED', value: 'true' } // Application is behind proxy
@@ -281,6 +281,8 @@ resource app 'Microsoft.App/containerApps@2023-05-01' = {
{ name: 'ApplicationInsights__ConnectionString', secretRef: 'connection-strings-application-insights' }
{ name: 'ConnectionStrings__Sql', secretRef: 'connection-strings-sql' }

{ name: 'DistributedLocking__FilePath', value: '/mnt/distributed-locks' }

{ name: 'Workflow__SynchronizeOnStartup', value: synchronizeOnStartup ? 'true' : 'false' }
{ name: 'Workflow__CreateOrUpdateWebhooksOnStartup', value: createOrUpdateWebhooksOnStartup ? 'true' : 'false' }
{ name: 'Workflow__ProjectUrl', value: projectUrl }
@@ -341,7 +343,10 @@ resource app 'Microsoft.App/containerApps@2023-05-01' = {
]
}
]
volumes: [ { name: fileShareName, storageName: fileShareName, storageType: 'AzureFile' } ]
volumes: [
{ name: 'working-dir', storageName: 'working-dir', storageType: 'AzureFile' }
{ name: 'distributed-locks', storageName: 'distributed-locks', storageType: 'AzureFile' }
]
scale: {
minReplicas: minReplicas
maxReplicas: maxReplicas
55 changes: 46 additions & 9 deletions server/main.json
Original file line number Diff line number Diff line change
@@ -147,10 +147,22 @@
}
}
],
"fileShares": [
{
"name": "certs"
},
{
"name": "distributed-locks",
"writeable": true
},
{
"name": "working-dir",
"writeable": true
}
],
"sqlServerAdministratorLogin": "[uniqueString(resourceGroup().id)]",
"sqlServerAdministratorLoginPassword": "[format('{0}%{1}', skip(uniqueString(resourceGroup().id), 5), uniqueString('sql-password', resourceGroup().id))]",
"collisionSuffix": "[uniqueString(resourceGroup().id)]",
"fileShareName": "working-dir",
"queueNames": [
"process-synchronization",
"repository-created",
@@ -163,23 +175,35 @@
},
"resources": [
{
"copy": {
"name": "shares",
"count": "[length(variables('fileShares'))]"
},
"type": "Microsoft.Storage/storageAccounts/fileServices/shares",
"apiVersion": "2022-09-01",
"name": "[format('{0}/{1}/{2}', format('{0}{1}', parameters('name'), variables('collisionSuffix')), 'default', variables('fileShareName'))]",
"name": "[format('{0}/{1}/{2}', format('{0}{1}', parameters('name'), variables('collisionSuffix')), 'default', variables('fileShares')[copyIndex()].name)]",
"properties": {
"accessTier": "[if(contains(variables('fileShares')[copyIndex()], 'accessTier'), variables('fileShares')[copyIndex()].accessTier, 'TransactionOptimized')]",
"shareQuota": "[if(contains(variables('fileShares')[copyIndex()], 'shareQuota'), variables('fileShares')[copyIndex()].shareQuota, 1)]"
},
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('name'), variables('collisionSuffix')))]"
]
},
{
"copy": {
"name": "storages",
"count": "[length(variables('fileShares'))]"
},
"type": "Microsoft.App/managedEnvironments/storages",
"apiVersion": "2023-05-01",
"name": "[format('{0}/{1}', parameters('name'), variables('fileShareName'))]",
"name": "[format('{0}/{1}', parameters('name'), variables('fileShares')[copyIndex()].name)]",
"properties": {
"azureFile": {
"accessMode": "ReadWrite",
"shareName": "[variables('fileShareName')]",
"accountName": "[format('{0}{1}', parameters('name'), variables('collisionSuffix'))]",
"accountKey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('name'), variables('collisionSuffix'))), '2022-09-01').keys[0].value]"
"accountKey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('name'), variables('collisionSuffix'))), '2022-09-01').keys[0].value]",
"shareName": "[variables('fileShares')[copyIndex()].name]",
"accessMode": "[if(and(contains(variables('fileShares')[copyIndex()], 'writeable'), bool(variables('fileShares')[copyIndex()].writeable)), 'ReadWrite', 'ReadOnly')]"
}
},
"dependsOn": [
@@ -386,7 +410,11 @@
"volumeMounts": [
{
"mountPath": "/mnt/dependabot",
"volumeName": "[variables('fileShareName')]"
"volumeName": "working-dir"
},
{
"mountPath": "/mnt/distributed-locks",
"volumeName": "distributed-locks"
}
],
"env": [
@@ -410,6 +438,10 @@
"name": "ConnectionStrings__Sql",
"secretRef": "connection-strings-sql"
},
{
"name": "DistributedLocking__FilePath",
"value": "/mnt/distributed-locks"
},
{
"name": "Workflow__SynchronizeOnStartup",
"value": "[if(parameters('synchronizeOnStartup'), 'true', 'false')]"
@@ -534,8 +566,13 @@
],
"volumes": [
{
"name": "[variables('fileShareName')]",
"storageName": "[variables('fileShareName')]",
"name": "working-dir",
"storageName": "working-dir",
"storageType": "AzureFile"
},
{
"name": "distributed-locks",
"storageName": "distributed-locks",
"storageType": "AzureFile"
}
],

0 comments on commit 6849f19

Please sign in to comment.