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
45 changes: 45 additions & 0 deletions src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ private async Task WriteProjectAsync(ProjectResource project)
await WriteDeploymentTarget(deploymentTarget).ConfigureAwait(false);
}

WriteContainerFilesDestination(project);

await WriteCommandLineArgumentsAsync(project).ConfigureAwait(false);

await WriteEnvironmentVariablesAsync(project).ConfigureAwait(false);
Expand All @@ -214,6 +216,39 @@ private async Task WriteDeploymentTarget(DeploymentTargetAnnotation deploymentTa
}
}

private void WriteContainerFilesDestination(IResource resource)
{
if (!resource.TryGetAnnotationsOfType<ContainerFilesDestinationAnnotation>(out var containerFilesAnnotations))
{
return;
}

Writer.WriteStartObject("containerFiles");

foreach (var containerFileDestination in containerFilesAnnotations)
{
var source = containerFileDestination.Source;

Writer.WriteStartObject(source.Name);
Writer.WriteString("destination", containerFileDestination.DestinationPath);

// Get source paths from the source resource
if (source.TryGetAnnotationsOfType<ContainerFilesSourceAnnotation>(out var sourceAnnotations))
{
Writer.WriteStartArray("sources");
foreach (var sourceAnnotation in sourceAnnotations)
{
Writer.WriteStringValue(sourceAnnotation.SourcePath);
}
Writer.WriteEndArray();
}

Writer.WriteEndObject();
}

Writer.WriteEndObject();
}

private async Task WriteExecutableAsync(ExecutableResource executable)
{
Writer.WriteString("type", "executable.v0");
Expand All @@ -227,6 +262,8 @@ private async Task WriteExecutableAsync(ExecutableResource executable)

Writer.WriteString("command", executable.Command);

WriteContainerFilesDestination(executable);

await WriteCommandLineArgumentsAsync(executable).ConfigureAwait(false);

await WriteEnvironmentVariablesAsync(executable).ConfigureAwait(false);
Expand Down Expand Up @@ -316,6 +353,9 @@ public async Task WriteContainerAsync(ContainerResource container)
// Write args if they are present
await WriteCommandLineArgumentsAsync(container).ConfigureAwait(false);

// Write container files destination if present
WriteContainerFilesDestination(container);

// Write volume & bind mount details
WriteContainerMounts(container);

Expand Down Expand Up @@ -353,6 +393,11 @@ private async Task WriteBuildContextAsync(ContainerResource container)
Writer.WriteString("stage", stage);
}

if (!annotation.HasEntrypoint)
{
Writer.WriteBoolean("buildOnly", true);
}

if (annotation.BuildArguments.Count > 0)
{
Writer.WriteStartObject("args");
Expand Down
113 changes: 113 additions & 0 deletions src/Schema/aspire-8.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,27 @@
"connectionString": {
"$ref": "#/definitions/connectionString"
},
"containerFiles": {
"type": "object",
"description": "Container files to be copied from other resources into this container's image.",
"additionalProperties": {
"type": "object",
"properties": {
"destination": {
"type": "string",
"description": "The destination path within this container where files will be copied."
},
"sources": {
"type": "array",
"description": "The source paths within the source container to copy from.",
"items": {
"type": "string"
}
}
},
"required": [ "destination" ]
}
},
"env": {
"$ref": "#/definitions/env"
},
Expand Down Expand Up @@ -125,6 +146,27 @@
"connectionString": {
"$ref": "#/definitions/connectionString"
},
"containerFiles": {
"type": "object",
"description": "Container files to be copied from other resources into this container's image.",
"additionalProperties": {
"type": "object",
"properties": {
"destination": {
"type": "string",
"description": "The destination path within this container where files will be copied."
},
"sources": {
"type": "array",
"description": "The source paths within the source container to copy from.",
"items": {
"type": "string"
}
}
},
"required": [ "destination" ]
}
},
"env": {
"$ref": "#/definitions/env"
},
Expand Down Expand Up @@ -152,6 +194,27 @@
"type": "string",
"description": "The path to the project file. Relative paths are interpreted as being relative to the location of the manifest file."
},
"containerFiles": {
"type": "object",
"description": "Container files to be copied from other resources into this project's container image.",
"additionalProperties": {
"type": "object",
"properties": {
"destination": {
"type": "string",
"description": "The destination path within this container where files will be copied."
},
"sources": {
"type": "array",
"description": "The source paths within the source container to copy from.",
"items": {
"type": "string"
}
}
},
"required": [ "destination" ]
}
},
"args": {
"$ref": "#/definitions/args"
},
Expand Down Expand Up @@ -186,6 +249,27 @@
}
]
},
"containerFiles": {
"type": "object",
"description": "Container files to be copied from other resources into this project's container image.",
"additionalProperties": {
"type": "object",
"properties": {
"destination": {
"type": "string",
"description": "The destination path within this container where files will be copied."
},
"sources": {
"type": "array",
"description": "The source paths within the source container to copy from.",
"items": {
"type": "string"
}
}
},
"required": [ "destination" ]
}
},
"args": {
"$ref": "#/definitions/args"
},
Expand Down Expand Up @@ -214,6 +298,27 @@
"type": "string",
"description": "The path to the command. Should be interpreted as being relative to the AppHost directory."
},
"containerFiles": {
"type": "object",
"description": "Container files to be copied from other resources into this executable's container image.",
"additionalProperties": {
"type": "object",
"properties": {
"destination": {
"type": "string",
"description": "The destination path within this container where files will be copied."
},
"sources": {
"type": "array",
"description": "The source paths within the source container to copy from.",
"items": {
"type": "string"
}
}
},
"required": [ "destination" ]
}
},
"args": {
"$ref": "#/definitions/args"
},
Expand Down Expand Up @@ -592,6 +697,14 @@
"type": "string",
"description": "The path to the Dockerfile. Can be relative or absolute. If relative it is relative to the manifest file."
},
"stage": {
"type": "string",
"description": "The name of the build stage to use for multi-stage Dockerfiles."
},
"buildOnly": {
"type": "boolean",
"description": "Indicates whether this container is built only to provide files for other containers and should not be deployed as a running service."
},
"args": {
"type": "object",
"description": "A list of build arguments which are used during container build.",
Expand Down
3 changes: 2 additions & 1 deletion tests/Aspire.Hosting.JavaScript.Tests/AddViteAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public async Task VerifyDefaultDockerfile()
"type": "container.v1",
"build": {
"context": "vite",
"dockerfile": "vite.Dockerfile"
"dockerfile": "vite.Dockerfile",
"buildOnly": true
},
"env": {
"NODE_ENV": "production",
Expand Down
142 changes: 142 additions & 0 deletions tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,148 @@ public async Task ParameterInputDefaultValuesGenerateCorrectly()
Assert.Equal(expectedManifest, manifest.ToString());
}

[Fact]
public async Task ContainerFilesAreWrittenToManifest()
{
var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions
{
Args = GetManifestArgs()
});

// Create a source container with ContainerFilesSourceAnnotation
var sourceContainer = builder.AddContainer("source", "node:22")
.WithAnnotation(new ContainerFilesSourceAnnotation { SourcePath = "/app/dist" });

// Create a destination container with ContainerFilesDestinationAnnotation
var destContainer = builder.AddContainer("dest", "nginx:alpine")
.WithAnnotation(new ContainerFilesDestinationAnnotation
{
Source = sourceContainer.Resource,
DestinationPath = "/usr/share/nginx/html"
});

builder.Build().Run();

var destManifest = await ManifestUtils.GetManifest(destContainer.Resource).DefaultTimeout();

var expectedManifest = """
{
"type": "container.v0",
"image": "nginx:alpine",
"containerFiles": {
"source": {
"destination": "/usr/share/nginx/html",
"sources": [
"/app/dist"
]
}
}
}
""";

Assert.Equal(expectedManifest, destManifest.ToString());
}

[Fact]
public async Task ContainerFilesWithMultipleSourcesAreWrittenToManifest()
{
var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions
{
Args = GetManifestArgs()
});

// Create a source container with multiple ContainerFilesSourceAnnotations
var sourceContainer = builder.AddContainer("source", "node:22")
.WithAnnotation(new ContainerFilesSourceAnnotation { SourcePath = "/app/dist" })
.WithAnnotation(new ContainerFilesSourceAnnotation { SourcePath = "/app/assets" });

// Create a destination container with ContainerFilesDestinationAnnotation
var destContainer = builder.AddContainer("dest", "nginx:alpine")
.WithAnnotation(new ContainerFilesDestinationAnnotation
{
Source = sourceContainer.Resource,
DestinationPath = "/usr/share/nginx/html"
});

builder.Build().Run();

var destManifest = await ManifestUtils.GetManifest(destContainer.Resource).DefaultTimeout();

var expectedManifest = """
{
"type": "container.v0",
"image": "nginx:alpine",
"containerFiles": {
"source": {
"destination": "/usr/share/nginx/html",
"sources": [
"/app/dist",
"/app/assets"
]
}
}
}
""";

Assert.Equal(expectedManifest, destManifest.ToString());
}

[Fact]
public async Task ContainerFilesWithMultipleDestinationsAreWrittenToManifest()
{
var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions
{
Args = GetManifestArgs()
});

// Create two source containers
var source1 = builder.AddContainer("source1", "node:22")
.WithAnnotation(new ContainerFilesSourceAnnotation { SourcePath = "/app/dist" });

var source2 = builder.AddContainer("source2", "node:22")
.WithAnnotation(new ContainerFilesSourceAnnotation { SourcePath = "/app/assets" });

// Create a destination container with multiple ContainerFilesDestinationAnnotations
var destContainer = builder.AddContainer("dest", "nginx:alpine")
.WithAnnotation(new ContainerFilesDestinationAnnotation
{
Source = source1.Resource,
DestinationPath = "/usr/share/nginx/html"
})
.WithAnnotation(new ContainerFilesDestinationAnnotation
{
Source = source2.Resource,
DestinationPath = "/usr/share/nginx/assets"
});

builder.Build().Run();

var destManifest = await ManifestUtils.GetManifest(destContainer.Resource).DefaultTimeout();

var expectedManifest = """
{
"type": "container.v0",
"image": "nginx:alpine",
"containerFiles": {
"source1": {
"destination": "/usr/share/nginx/html",
"sources": [
"/app/dist"
]
},
"source2": {
"destination": "/usr/share/nginx/assets",
"sources": [
"/app/assets"
]
}
}
}
""";

Assert.Equal(expectedManifest, destManifest.ToString());
}

private static TestProgram CreateTestProgramJsonDocumentManifestPublisher(bool includeIntegrationServices = false, bool includeNodeApp = false)
{
var program = TestProgram.Create<ManifestGenerationTests>(GetJsonManifestArgs(), includeIntegrationServices, includeNodeApp);
Expand Down
Loading