From ddac970ff51e62939f79d23650829e4248276add Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Wed, 20 Sep 2023 10:22:16 +0300 Subject: [PATCH] Added distributed locks via a file share --- .../IServiceCollectionExtensions.cs | 52 +++++++++++++++ .../{ => Extensions}/SystemExtensions.cs | 0 server/Tingle.Dependabot/Program.cs | 11 +--- .../Tingle.Dependabot.csproj | 2 + server/main.bicep | 65 ++++++++++--------- server/main.json | 55 +++++++++++++--- 6 files changed, 138 insertions(+), 47 deletions(-) create mode 100644 server/Tingle.Dependabot/Extensions/IServiceCollectionExtensions.cs rename server/Tingle.Dependabot/{ => Extensions}/SystemExtensions.cs (100%) diff --git a/server/Tingle.Dependabot/Extensions/IServiceCollectionExtensions.cs b/server/Tingle.Dependabot/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 00000000..77010fc0 --- /dev/null +++ b/server/Tingle.Dependabot/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,52 @@ +using Medallion.Threading; +using Medallion.Threading.FileSystem; +using Tingle.Dependabot.Workflow; + +namespace Microsoft.Extensions.DependencyInjection; + +/// Extensions on . +public static class IServiceCollectionExtensions +{ + /// + /// Add , a provider for . + /// + /// The to be configured. + /// The to use. + /// The root configuration instance from which to pull settings. + /// + public static IServiceCollection AddDistributedLockProvider(this IServiceCollection services, IHostEnvironment environment, IConfiguration configuration) + { + var configKey = ConfigurationPath.Combine("DistributedLocking", "FilePath"); + + var path = configuration.GetValue(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(new FileDistributedSynchronizationProvider(new(path))); + + return services; + } + + public static IServiceCollection AddWorkflowServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration); + services.ConfigureOptions(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/server/Tingle.Dependabot/SystemExtensions.cs b/server/Tingle.Dependabot/Extensions/SystemExtensions.cs similarity index 100% rename from server/Tingle.Dependabot/SystemExtensions.cs rename to server/Tingle.Dependabot/Extensions/SystemExtensions.cs diff --git a/server/Tingle.Dependabot/Program.cs b/server/Tingle.Dependabot/Program.cs index 2fa171bf..0b69144c 100644 --- a/server/Tingle.Dependabot/Program.cs +++ b/server/Tingle.Dependabot/Program.cs @@ -66,16 +66,11 @@ }); }); +// Configure other services builder.Services.AddMemoryCache(); builder.Services.AddDistributedMemoryCache(); - -// Configure other services -builder.Services.Configure(builder.Configuration.GetSection("Workflow")); -builder.Services.ConfigureOptions(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddDistributedLockProvider(builder.Environment, builder.Configuration); +builder.Services.AddWorkflowServices(builder.Configuration.GetSection("Workflow")); // Add event bus var selectedTransport = builder.Configuration.GetValue("EventBus:SelectedTransport"); diff --git a/server/Tingle.Dependabot/Tingle.Dependabot.csproj b/server/Tingle.Dependabot/Tingle.Dependabot.csproj index dd467233..73c2f107 100644 --- a/server/Tingle.Dependabot/Tingle.Dependabot.csproj +++ b/server/Tingle.Dependabot/Tingle.Dependabot.csproj @@ -18,8 +18,10 @@ + + diff --git a/server/main.bicep b/server/main.bicep index e679dc59..91ac53ee 100644 --- a/server/main.bicep +++ b/server/main.bicep @@ -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 diff --git a/server/main.json b/server/main.json index 77b9dbd1..303395db 100644 --- a/server/main.json +++ b/server/main.json @@ -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" } ],