|
#pragma warning disable ASPIREPUBLISHERS001
#pragma warning disable ASPIRECOMPUTE001
#pragma warning disable ASPIREPIPELINES001
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Publishing;
using Azure.Identity;
using Azure.Provisioning;
using Azure.Provisioning.Storage;
using Azure.Storage.Files.Shares;
using Azure.Storage.Files.Shares.Models;
using Pipelines.Library;
var builder = DistributedApplication.CreateBuilder(args);
builder.Pipeline.AddAppServiceZipDeploy();
var aasEnv = builder.AddAzureAppServiceEnvironment("appservice-env");
var acaEnv = builder.AddAzureContainerAppEnvironment("aca-env")
.ConfigureInfrastructure(infra =>
{
var volumeStorageAccount = infra.GetProvisionableResources().OfType<StorageAccount>().SingleOrDefault();
if (volumeStorageAccount == null)
{
return;
}
infra.Add(new ProvisioningOutput("STORAGE_VOLUME_ACCOUNT_NAME", typeof(string))
{
Value = volumeStorageAccount.Name
});
var fileShares = infra.GetProvisionableResources().OfType<Azure.Provisioning.Storage.FileShare>().ToList();
for (var i = 0; i < fileShares.Count; i++)
{
var fileShare = fileShares[i];
infra.Add(new ProvisioningOutput($"SHARES_{i}_NAME", typeof(string))
{
Value = fileShare.Name
});
}
});
var withBindMount = builder.AddDockerfile("with-bind-mount", ".", "./Dockerfile.bindmount")
.WithComputeEnvironment(acaEnv)
.WithBindMount("../data", "/data");
// This step could also be modeled as a Bicep resource with the role assignment
// for the principalId associated with the deployment.
builder.Pipeline.AddStep("assign-storage-role", async (context) =>
{
var resourcesWithBindMounts = context.Model.Resources
.Where(r => r.TryGetContainerMounts(out var mounts) &&
mounts.Any(m => m.Type == ContainerMountType.BindMount))
.ToList();
if (resourcesWithBindMounts.Count == 0)
{
return;
}
var storageAccountName = await acaEnv.GetOutput("storagE_VOLUME_ACCOUNT_NAME").GetValueAsync();
if (string.IsNullOrEmpty(storageAccountName))
{
return;
}
var roleAssignmentStep = await context.ActivityReporter
.CreateStepAsync($"assign-storage-role", context.CancellationToken)
.ConfigureAwait(false);
await using (roleAssignmentStep.ConfigureAwait(false))
{
var assignRoleTask = await roleAssignmentStep
.CreateTaskAsync($"Granting file share access to current user", context.CancellationToken)
.ConfigureAwait(false);
await using (assignRoleTask.ConfigureAwait(false))
{
try
{
// Get the current signed-in user's object ID
var getUserProcess = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "az",
Arguments = "ad signed-in-user show --query id -o tsv",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
});
if (getUserProcess == null)
{
await assignRoleTask.CompleteAsync(
"Failed to start az CLI process",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
return;
}
var userObjectId = await getUserProcess.StandardOutput.ReadToEndAsync(context.CancellationToken).ConfigureAwait(false);
userObjectId = userObjectId.Trim();
await getUserProcess.WaitForExitAsync(context.CancellationToken).ConfigureAwait(false);
if (getUserProcess.ExitCode != 0)
{
var error = await getUserProcess.StandardError.ReadToEndAsync(context.CancellationToken).ConfigureAwait(false);
await assignRoleTask.CompleteAsync(
$"Failed to get signed-in user: {error}",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
return;
}
// Get the current subscription ID
var getSubscriptionProcess = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "az",
Arguments = "account show --query id -o tsv",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
});
if (getSubscriptionProcess == null)
{
await assignRoleTask.CompleteAsync(
"Failed to get subscription ID",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
return;
}
var subscriptionId = await getSubscriptionProcess.StandardOutput.ReadToEndAsync(context.CancellationToken).ConfigureAwait(false);
subscriptionId = subscriptionId.Trim();
await getSubscriptionProcess.WaitForExitAsync(context.CancellationToken).ConfigureAwait(false);
if (getSubscriptionProcess.ExitCode != 0)
{
var error = await getSubscriptionProcess.StandardError.ReadToEndAsync(context.CancellationToken).ConfigureAwait(false);
await assignRoleTask.CompleteAsync(
$"Failed to get subscription ID: {error}",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
return;
}
// Get the resource group for the storage account
var getResourceGroupProcess = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "az",
Arguments = $"storage account show --name {storageAccountName} --query resourceGroup -o tsv",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
});
if (getResourceGroupProcess == null)
{
await assignRoleTask.CompleteAsync(
"Failed to get resource group",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
return;
}
var resourceGroup = await getResourceGroupProcess.StandardOutput.ReadToEndAsync(context.CancellationToken).ConfigureAwait(false);
resourceGroup = resourceGroup.Trim();
await getResourceGroupProcess.WaitForExitAsync(context.CancellationToken).ConfigureAwait(false);
if (getResourceGroupProcess.ExitCode != 0)
{
var error = await getResourceGroupProcess.StandardError.ReadToEndAsync(context.CancellationToken).ConfigureAwait(false);
await assignRoleTask.CompleteAsync(
$"Failed to get resource group: {error}",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
return;
}
// Build the scope for the storage account
var scope = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}";
// Assign the Storage File Data Privileged Contributor role
var assignRoleProcess = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "az",
Arguments = $"role assignment create --role \"Storage File Data Privileged Contributor\" --assignee {userObjectId} --scope {scope}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
});
if (assignRoleProcess == null)
{
await assignRoleTask.CompleteAsync(
"Failed to start az CLI process for role assignment",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
return;
}
await assignRoleProcess.WaitForExitAsync(context.CancellationToken).ConfigureAwait(false);
if (assignRoleProcess.ExitCode != 0)
{
var error = await assignRoleProcess.StandardError.ReadToEndAsync(context.CancellationToken).ConfigureAwait(false);
await assignRoleTask.CompleteAsync(
$"Failed to assign role: {error}",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
return;
}
await assignRoleTask.CompleteAsync(
$"Successfully assigned Storage File Data Privileged Contributor role",
CompletionState.Completed,
context.CancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
await assignRoleTask.CompleteAsync(
$"Error assigning role: {ex.Message}",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
}
}
await roleAssignmentStep.CompleteAsync(
"Role assignment completed",
CompletionState.Completed,
context.CancellationToken).ConfigureAwait(false);
}
}, requiredBy: "upload-bind-mounts", dependsOn: WellKnownPipelineSteps.ProvisionInfrastructure);
builder.Pipeline.AddStep("upload-bind-mounts", async (context) =>
{
var resourcesWithBindMounts = context.Model.Resources
.Where(r => r.TryGetContainerMounts(out var mounts) &&
mounts.Any(m => m.Type == ContainerMountType.BindMount))
.ToList();
if (resourcesWithBindMounts.Count == 0)
{
return;
}
var uploadStep = await context.ActivityReporter
.CreateStepAsync($"upload-bind-mounts", context.CancellationToken)
.ConfigureAwait(false);
await using (uploadStep.ConfigureAwait(false))
{
var totalUploads = 0;
var storageAccountName = await acaEnv.GetOutput("storagE_VOLUME_ACCOUNT_NAME").GetValueAsync();
var resource = withBindMount.Resource;
if (!resource.TryGetContainerMounts(out var mounts))
{
return;
}
var bindMounts = mounts.Where(m => m.Type == ContainerMountType.BindMount).ToList();
for (var i = 0; i < bindMounts.Count; i++)
{
var bindMount = bindMounts[i];
var sourcePath = bindMount.Source;
if (string.IsNullOrEmpty(sourcePath))
{
continue;
}
var fileShareName = await acaEnv.GetOutput($"shareS_{i}_NAME").GetValueAsync();
var uploadTask = await uploadStep
.CreateTaskAsync($"Uploading {Path.GetFileName(sourcePath)} to {fileShareName}", context.CancellationToken)
.ConfigureAwait(false);
await using (uploadTask.ConfigureAwait(false))
{
if (!Directory.Exists(sourcePath))
{
await uploadTask.CompleteAsync(
$"Source path {sourcePath} does not exist",
CompletionState.CompletedWithWarning,
context.CancellationToken).ConfigureAwait(false);
continue;
}
var files = Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories);
var fileCount = files.Length;
var credential = new AzureCliCredential();
var fileShareUri = new Uri($"https://{storageAccountName}.file.core.windows.net/{fileShareName}");
var clientOptions = new ShareClientOptions
{
ShareTokenIntent = ShareTokenIntent.Backup
};
var shareClient = new ShareClient(fileShareUri, credential, clientOptions);
foreach (var filePath in files)
{
var relativePath = Path.GetRelativePath(sourcePath, filePath);
var directoryPath = Path.GetDirectoryName(relativePath) ?? string.Empty;
var directoryClient = shareClient.GetRootDirectoryClient();
if (!string.IsNullOrEmpty(directoryPath))
{
var parts = directoryPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
foreach (var part in parts)
{
directoryClient = directoryClient.GetSubdirectoryClient(part);
await directoryClient.CreateIfNotExistsAsync(cancellationToken: context.CancellationToken).ConfigureAwait(false);
}
}
var fileName = Path.GetFileName(filePath);
var fileClient = directoryClient.GetFileClient(fileName);
using var fileStream = File.OpenRead(filePath);
await fileClient.CreateAsync(fileStream.Length, cancellationToken: context.CancellationToken).ConfigureAwait(false);
await fileClient.UploadAsync(fileStream, cancellationToken: context.CancellationToken).ConfigureAwait(false);
}
await uploadTask.CompleteAsync(
$"Successfully uploaded {fileCount} file(s) from {sourcePath}",
CompletionState.Completed,
context.CancellationToken).ConfigureAwait(false);
totalUploads += fileCount;
}
}
await uploadStep.CompleteAsync(
$"Successfully uploaded {totalUploads} file(s) to Azure File Shares",
CompletionState.Completed,
context.CancellationToken).ConfigureAwait(false);
}
}, requiredBy: WellKnownPipelineSteps.DeployCompute, dependsOn: WellKnownPipelineSteps.ProvisionInfrastructure);
builder.AddProject<Projects.Publishers_ApiService>("api-service")
.WithComputeEnvironment(aasEnv)
.WithExternalHttpEndpoints();
#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
// of the dashboard. It is not required in end developer code. Comment out this code
// or build with `/p:SkipDashboardReference=true`, to test end developer
// dashboard launch experience, Refer to Directory.Build.props for the path to
// the dashboard binary (defaults to the Aspire.Dashboard bin output in the
// artifacts dir).
builder.AddProject<Projects.Aspire_Dashboard>(KnownResourceNames.AspireDashboard);
#endif
builder.Build().Run();
|