Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ namespace Aspire.Hosting.Pipelines;
[DebuggerDisplay("{ToString(),nq}")]
internal sealed class DistributedApplicationPipeline : IDistributedApplicationPipeline
{
internal const string ValidateBuildOnlyContainerReferencesStepName = "validate-build-only-container-references";

private readonly List<PipelineStep> _steps = [];
private readonly List<Func<PipelineConfigurationContext, Task>> _configurationCallbacks = [];

Expand Down Expand Up @@ -310,6 +312,18 @@ public DistributedApplicationPipeline()
Action = _ => Task.CompletedTask,
});

_steps.Add(new PipelineStep
{
Name = ValidateBuildOnlyContainerReferencesStepName,
Description = "Validates that build-only containers are consumed by another resource before publish or deploy.",
Action = static context =>
{
ValidateBuildOnlyContainerReferences(context.Model);
return Task.CompletedTask;
},
RequiredBySteps = [WellKnownPipelineSteps.PublishPrereq, WellKnownPipelineSteps.DeployPrereq],
});

// Add diagnostic step for dependency graph analysis
_steps.Add(new PipelineStep
{
Expand Down Expand Up @@ -398,6 +412,48 @@ private static void ValidateComputeEnvironmentBindings(DistributedApplicationMod
$"Specify which environment each resource should target by calling 'WithComputeEnvironment' on the resource builder.");
}

private static void ValidateBuildOnlyContainerReferences(DistributedApplicationModel model)
{
var buildOnlyContainers = model.GetBuildResources()
.Where(resource => resource.IsBuildOnlyContainer() && !resource.IsExcludedFromPublish())
.ToList();

if (buildOnlyContainers.Count == 0)
{
return;
}

var consumedBuildOnlyContainerNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

foreach (var resource in model.Resources)
{
if (resource.IsExcludedFromPublish())
{
continue;
}

foreach (var annotation in resource.Annotations.OfType<ContainerFilesDestinationAnnotation>())
{
if (!string.Equals(resource.Name, annotation.Source.Name, StringComparison.OrdinalIgnoreCase))
{
consumedBuildOnlyContainerNames.Add(annotation.Source.Name);
}
}
}

var unconsumedBuildOnlyContainers = buildOnlyContainers
.Where(resource => !consumedBuildOnlyContainerNames.Contains(resource.Name))
.ToList();

if (unconsumedBuildOnlyContainers.Count > 0)
{
var resourceNames = string.Join("', '", unconsumedBuildOnlyContainers.Select(resource => resource.Name));
throw new DistributedApplicationException(
$"Build-only container resource(s) '{resourceNames}' are not consumed by another resource and won't participate in publish or deploy. " +
$"Reference them from another resource, for example using 'PublishWithContainerFiles' or 'PublishWithStaticFiles', or suppress this validation for the app by calling 'builder.Pipeline.DisableBuildOnlyContainerValidation()'.");
}
}

public bool HasSteps => _steps.Count > 0;

public void AddStep(string name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Aspire.Hosting.Pipelines;

/// <summary>
/// Extension methods for <see cref="IDistributedApplicationPipeline"/>.
/// </summary>
[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public static class DistributedApplicationPipelineExtensions
{
/// <summary>
/// Disables the publish and deploy validation that requires build-only containers to be consumed by another resource.
/// </summary>
/// <param name="pipeline">The distributed application pipeline.</param>
/// <returns>The distributed application pipeline for chaining.</returns>
/// <remarks>
/// This is an application-wide escape hatch for scenarios where the build-only container validation is too restrictive
/// for a particular app. Prefer wiring build-only containers through <c>PublishWithContainerFiles</c> or
/// <c>PublishWithStaticFiles</c> when possible.
/// </remarks>
[AspireExport(Description = "Disables publish and deploy validation for unconsumed build-only containers.")]
public static IDistributedApplicationPipeline DisableBuildOnlyContainerValidation(this IDistributedApplicationPipeline pipeline)
{
ArgumentNullException.ThrowIfNull(pipeline);

pipeline.AddPipelineConfiguration(static context =>
{
var validationStep = context.Steps.SingleOrDefault(step => step.Name == DistributedApplicationPipeline.ValidateBuildOnlyContainerReferencesStepName);
validationStep?.RequiredBySteps.Clear();
return Task.CompletedTask;
});

return pipeline;
}
}
1 change: 1 addition & 0 deletions tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ public async Task DeployAsync_WithBuildOnlyContainers()
};
});
ConfigureTestServices(builder, armClientProvider: armClientProvider, activityReporter: mockActivityReporter);
builder.Pipeline.DisableBuildOnlyContainerValidation();

var containerAppEnv = builder.AddAzureContainerAppEnvironment("env");

Expand Down
Loading
Loading