Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4611271
Initial plan
Copilot Nov 30, 2025
41ca003
Add Aspire.Hosting.LetsEncrypt integration with Certbot container sup…
Copilot Nov 30, 2025
ac3b0f7
Pin certbot container image to stable version v5.1.0
Copilot Nov 30, 2025
3d6ccad
Add CertificatePath and PrivateKeyPath properties to CertbotResource
Copilot Nov 30, 2025
87bb3cd
Rename Aspire.Hosting.LetsEncrypt to Aspire.Hosting.Certbot and WithC…
Copilot Dec 2, 2025
67cbe2d
Refactor Certbot API: make HTTP endpoint opt-in, split args into With…
Copilot Dec 5, 2025
793c0ae
Refactor args composition: centralize in AddCertbot WithArgs callback…
Copilot Dec 5, 2025
ad1b0c7
Add WaitForCompletion to usage examples for resources that depend on …
Copilot Dec 5, 2025
1a7c32d
Add WithCertbotCertificate method that combines volume mount and Wait…
Copilot Dec 5, 2025
a5aac1d
Update method name from WithServerAuthenticationCertificateConfigurat…
Copilot Dec 5, 2025
b2f5493
Merge remote-tracking branch 'origin/main' into copilot/add-lets-encr…
Copilot Dec 5, 2025
e6bba87
Update documentation to reference correct HTTPS certificate APIs and …
Copilot Dec 5, 2025
2b408c0
Remove WithCertificateVolume method and related tests
Copilot Dec 5, 2025
9cd0d37
Update src/Aspire.Hosting.Certbot/Aspire.Hosting.Certbot.csproj
davidfowl Dec 5, 2025
e7b75bc
Simplify README: remove verbose usage explanation and Connection Prop…
Copilot Dec 6, 2025
ed7ec9e
Simplify README further: remove Configuration section and focus on mi…
Copilot Dec 6, 2025
44d566d
Fix markdown linting: remove multiple consecutive blank lines
Copilot Dec 6, 2025
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
50 changes: 48 additions & 2 deletions src/Aspire.Hosting.Certbot/CertbotBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ public static class CertbotBuilderExtensions
/// .WithHttp01Challenge();
///
/// var myService = builder.AddContainer("myservice", "myimage")
/// .WithVolume("letsencrypt", "/etc/letsencrypt")
/// .WaitForCompletion(certbot);
/// .WithCertbotCertificate(certbot);
/// </code>
/// </example>
/// </remarks>
Expand Down Expand Up @@ -145,6 +144,10 @@ public static IResourceBuilder<CertbotResource> WithHttp01Challenge(
/// This method adds the certificates volume to the specified container resource,
/// allowing it to access SSL/TLS certificates obtained by Certbot.
/// </para>
/// <para>
/// This method only mounts the volume. Consider using <see cref="WithCertbotCertificate{T}"/> instead,
/// which also ensures the container waits for certificate acquisition to complete.
/// </para>
/// <example>
/// <code lang="csharp">
/// var domain = builder.AddParameter("domain");
Expand All @@ -169,4 +172,47 @@ public static IResourceBuilder<T> WithCertificateVolume<T>(

return builder.WithVolume(CertbotResource.CertificatesVolumeName, mountPath);
}

/// <summary>
/// Configures the container to use SSL/TLS certificates from a Certbot resource.
/// </summary>
/// <typeparam name="T">The type of the container resource.</typeparam>
/// <param name="builder">The resource builder for the container resource that needs access to the certificates.</param>
/// <param name="certbot">The Certbot resource builder.</param>
/// <param name="mountPath">The path where the certificates volume should be mounted. Defaults to /etc/letsencrypt.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// <para>
/// This method mounts the certificates volume and ensures the container waits for the Certbot
/// resource to complete certificate acquisition before starting.
/// </para>
/// <para>
/// <strong>Note:</strong> This method may conflict with <c>WithServerAuthenticationCertificateConfiguration</c>
/// if both are used on the same resource. Only use one certificate configuration method per resource.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithServerAuthenticationCertificateConfiguration was renamed to WithHttpsCertificateConifguration in main. Also, this comment should reference WithHttpsCertificate and WithHttpsDeveloperCertificate instead of the configuration callback API. Instead of telling users they can't use both APIs, we should instruct them not to use both at runtime; it's totally okay to use certbot and WithHttpsCertificate if certbot is only added when running in publish mode.

We should also add a note that the user needs to configure their application manually when using certbot (ideally with an example of configuring something like yarp via environment variables). The example should probably wrap the certbot config in a publish mode check.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot implement this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated documentation in commit e6bba87:

  • Changed references from WithServerAuthenticationCertificateConfiguration to WithHttpsCertificate and WithHttpsCertificateConfiguration
  • Clarified that conflicts only occur at runtime, allowing both APIs to coexist when used in different execution modes
  • Added comprehensive example showing publish mode usage with ExecutionContext.IsPublishMode check
  • Included manual configuration example using environment variables for YARP

/// </para>
/// <example>
/// <code lang="csharp">
/// var domain = builder.AddParameter("domain");
/// var email = builder.AddParameter("email");
///
/// var certbot = builder.AddCertbot("certbot", domain, email)
/// .WithHttp01Challenge();
///
/// var yarp = builder.AddContainer("yarp", "myimage")
/// .WithCertbotCertificate(certbot);
/// </code>
/// </example>
/// </remarks>
public static IResourceBuilder<T> WithCertbotCertificate<T>(
this IResourceBuilder<T> builder,
IResourceBuilder<CertbotResource> certbot,
string mountPath = CertbotResource.CertificatesPath) where T : ContainerResource, IResourceWithWaitSupport
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(certbot);

return builder
.WithVolume(CertbotResource.CertificatesVolumeName, mountPath)
.WaitForCompletion(certbot);
}
}
25 changes: 19 additions & 6 deletions src/Aspire.Hosting.Certbot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ var certbot = builder.AddCertbot("certbot", domain, email)
.WithHttp01Challenge();

var myService = builder.AddContainer("myservice", "myimage")
.WithCertificateVolume(certbot)
.WaitForCompletion(certbot);
.WithCertbotCertificate(certbot);
```

The certbot container will:

- Obtain certificates for the specified domain using the ACME protocol
- Store certificates in a shared volume at `/etc/letsencrypt`
- Use the configured challenge method (e.g., HTTP-01) for domain validation
- Ensure dependent containers wait for certificate acquisition before starting

## Configuration

Expand Down Expand Up @@ -76,12 +76,23 @@ Certificate permissions are automatically set to allow non-root containers to re

### Sharing Certificates with Other Resources

Use the `WithCertificateVolume` extension method to mount the certificates volume in other containers:
Use the `WithCertbotCertificate` extension method to configure a container with certificates from Certbot:

```csharp
var yarp = builder.AddContainer("yarp", "myimage")
.WithCertificateVolume(certbot)
.WaitForCompletion(certbot);
.WithCertbotCertificate(certbot);
```

This method automatically:
- Mounts the certificates volume at `/etc/letsencrypt`
- Ensures the container waits for certificate acquisition to complete

For more control, you can use `WithCertificateVolume` and `WaitForCompletion` separately:

```csharp
var myService = builder.AddContainer("myservice", "myimage")
.WithCertificateVolume(certbot)
.WaitForCompletion(certbot);
```

Or mount the volume directly:
Expand All @@ -92,6 +103,8 @@ var myService = builder.AddContainer("myservice", "myimage")
.WaitForCompletion(certbot);
```

**Important:** Do not use `WithCertbotCertificate` or `WithCertificateVolume` together with `WithServerAuthenticationCertificateConfiguration` on the same resource, as they may conflict. Choose one certificate configuration method per resource.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot got renamed to WithServerHttpsConfiguration

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to WithServerHttpsConfiguration in commit a5aac1d. Changed both the README and XML documentation.


### Certificate Locations

After Certbot obtains certificates, they are available at:
Expand All @@ -114,7 +127,7 @@ var privateKeyPath = certbot.Resource.PrivateKeyPath; // /etc/letsencrypt/li

The Certbot resource does not expose connection properties through `WithReference`. This is because the Certbot resource is a certificate provisioning tool, not a service that other resources connect to.

Instead, use the `WithCertificateVolume` extension method to share certificates with other containers via a mounted volume. See the [Sharing Certificates with Other Resources](#sharing-certificates-with-other-resources) section above for usage examples.
Instead, use the `WithCertbotCertificate` extension method to configure containers with certificates from Certbot. This method handles mounting the certificates volume and waiting for certificate acquisition. See the [Sharing Certificates with Other Resources](#sharing-certificates-with-other-resources) section above for usage examples.

## Additional documentation

Expand Down
45 changes: 45 additions & 0 deletions tests/Aspire.Hosting.Certbot.Tests/AddCertbotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,49 @@ public void WithHttp01ChallengeWithCustomPort()
Assert.Equal(80, endpoint.TargetPort);
Assert.Equal(8080, endpoint.Port);
}

[Fact]
public void WithCertbotCertificateAddsVolumeAndWaitAnnotations()
{
using var builder = TestDistributedApplicationBuilder.Create();
var domain = builder.AddParameter("domain");
var email = builder.AddParameter("email");
var certbot = builder.AddCertbot("certbot", domain, email);

var container = builder.AddContainer("test", "testimage")
.WithCertbotCertificate(certbot);

// Check volume annotation
var volumes = container.Resource.Annotations.OfType<ContainerMountAnnotation>().ToList();
Assert.Single(volumes);
var volumeAnnotation = volumes[0];
Assert.Equal("letsencrypt", volumeAnnotation.Source);
Assert.Equal("/etc/letsencrypt", volumeAnnotation.Target);
Assert.Equal(ContainerMountType.Volume, volumeAnnotation.Type);

// Check wait annotation
var waitAnnotations = container.Resource.Annotations.OfType<WaitAnnotation>().ToList();
Assert.Single(waitAnnotations);
var waitAnnotation = waitAnnotations[0];
Assert.Equal(certbot.Resource, waitAnnotation.Resource);
Assert.Equal(WaitType.WaitForCompletion, waitAnnotation.WaitType);
}

[Fact]
public void WithCertbotCertificateWithCustomMountPath()
{
using var builder = TestDistributedApplicationBuilder.Create();
var domain = builder.AddParameter("domain");
var email = builder.AddParameter("email");
var certbot = builder.AddCertbot("certbot", domain, email);

var container = builder.AddContainer("test", "testimage")
.WithCertbotCertificate(certbot, "/custom/certs");

var volumes = container.Resource.Annotations.OfType<ContainerMountAnnotation>().ToList();
Assert.Single(volumes);
var volumeAnnotation = volumes[0];
Assert.Equal("letsencrypt", volumeAnnotation.Source);
Assert.Equal("/custom/certs", volumeAnnotation.Target);
}
}
Loading