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"
}
],