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
5 changes: 5 additions & 0 deletions AzureSearchEmulator.Aspire.DemoAppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

// Example container usage via F23.Aspire.Hosting.AzureSearchEmulator
builder.AddAzureSearchEmulator("emulator-container")
// The package pins the image tag to its own version, but that image is not published until the
// release is tagged -- so in this repo the pinned tag does not exist yet and the pull fails.
// The demo tracks the code in the working tree rather than a release, so "latest" is the right
// tag here. Consumers of the package should leave the pinned default alone.
.WithImageTag("latest")
.WithIndexesVolume();

builder.Build().Run();
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,30 @@ await callback.Callback(new HttpsCertificateConfigurationCallbackAnnotationConte
return await valueProvider.GetValueAsync(TestContext.Current.CancellationToken);
}

[Fact]
public void AddAzureSearchEmulator_PinsImageTagToPackageVersion()
{
// "latest" is an ordinary Docker tag, so a host that had already pulled it kept running the
// image from that first pull and never picked up the emulator a newer package expects.
// Pinning to the package version means upgrading the NuGet reference pulls a tag the host
// does not have yet.

// Arrange
var builder = DistributedApplication.CreateBuilder();

// Act
var resource = builder.AddAzureSearchEmulator("my-emulator").Resource;

// Assert
Assert.True(resource.TryGetAnnotationsOfType<ContainerImageAnnotation>(out var imageAnnotations));
var image = Assert.Single(imageAnnotations);

Assert.Equal("ghcr.io", image.Registry);
Assert.Equal("feature23/azuresearchemulator", image.Image);
Assert.Equal(AzureSearchEmulatorResource.ImageTag, image.Tag);
Assert.NotEqual("latest", image.Tag);
}

[InlineData(false)]
[InlineData(true)]
[Theory]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using F23.Aspire.Hosting.AzureSearchEmulator;
using System.Xml.Linq;
using F23.Aspire.Hosting.AzureSearchEmulator;

namespace AzureSearchEmulator.Aspire.Tests;

Expand All @@ -16,4 +17,51 @@ public void Constructor_InitializesResourceProperly()
// Assert
Assert.Equal(name, resource.Name);
}

[Fact]
public void ImageTag_MatchesPackageVersion()
{
// The emulator image and this package are released together, so the pinned tag has to be a
// tag the release actually publishes. The Docker workflow tags images from the git tag via
// `type=semver,pattern={{version}}`, so `v1.2.3` publishes the image tag `1.2.3` -- which
// is the package version. Deriving the tag from the assembly keeps the two in step without
// a second place to edit at release time; this asserts that derivation stays correct.

// Arrange
// Read the version the package actually ships from the csproj, rather than from the
// assembly the tag is already derived from -- comparing the derivation against its own
// input would pass no matter what the release version is.
var csproj = Path.Combine(GetRepositoryRoot(),
"AzureSearchEmulator.Aspire", "AzureSearchEmulator.Aspire.csproj");

var packageVersion = XDocument.Load(csproj)
.Descendants("Version")
.Single()
.Value
.Trim();

// Act
var tag = AzureSearchEmulatorResource.ImageTag;

// Assert
Assert.Equal(packageVersion, tag);

// The SDK appends "+<commit sha>" to the informational version, which is not part of any
// published image tag.
Assert.DoesNotContain('+', tag);
}

private static string GetRepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);

while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AzureSearchEmulator.sln")))
{
directory = directory.Parent;
}

Assert.NotNull(directory);

return directory.FullName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<PackageTags>azure search emulator ai hosting docker</PackageTags>
<Description>Azure Search Emulator hosting support for Aspire.</Description>
<IsPackable>true</IsPackable>
<Version>1.0.0-beta.3</Version>
<Version>1.0.0-beta.4</Version>
<Company>feature[23]</Company>
<Copyright>Copyright (c) feature[23] 2025</Copyright>
<Authors>Paul Irwin</Authors>
Expand Down
26 changes: 25 additions & 1 deletion AzureSearchEmulator.Aspire/AzureSearchEmulatorResource.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
using Aspire.Hosting.ApplicationModel;
using System.Reflection;
using Aspire.Hosting.ApplicationModel;

namespace F23.Aspire.Hosting.AzureSearchEmulator;

public class AzureSearchEmulatorResource(string name) : ContainerResource(name)
{
public const int DefaultHttpPort = 5100;
public const int DefaultHttpsPort = 5143;

internal const string ImageRegistry = "ghcr.io";
internal const string ImageName = "feature23/azuresearchemulator";

/// <summary>
/// The container image tag used by <see cref="AzureSearchEmulatorResourceExtensions"/>.
/// </summary>
/// <remarks>
/// This is the emulator version that matches this package's version, rather than "latest".
/// Docker treats "latest" as an ordinary tag, so a host that had already pulled it kept running
/// the image it pulled the first time and never picked up the emulator that a newer package
/// expects. Pinning ties the container to the package, so upgrading the NuGet reference pulls
/// the matching image.
///
/// This is asserted against the package version by AzureSearchEmulatorResourceTests, so
/// releasing a new version does not need this value updated by hand.
/// </remarks>
internal static readonly string ImageTag = typeof(AzureSearchEmulatorResource).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion
// The SDK appends "+<commit sha>" to the informational version; the published image tag
// carries only the version itself.
.Split('+')[0];
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public static class AzureSearchEmulatorResourceExtensions
/// <remarks>
/// It is recommended to configure a volume for persisting index data using
/// <see cref="WithIndexesVolume"/>.
/// You can also override the default image tag ("latest") by using the returned resource builder's
/// The image tag defaults to the emulator version matching this package's version. You can
/// override it by using the returned resource builder's
/// <see cref="ContainerResourceBuilderExtensions.WithImageTag{T}"/> method.
/// </remarks>
public IResourceBuilder<AzureSearchEmulatorResource> AddAzureSearchEmulator(string name,
Expand All @@ -32,9 +33,9 @@ public IResourceBuilder<AzureSearchEmulatorResource> AddAzureSearchEmulator(stri

#pragma warning disable ASPIRECERTIFICATES001 // WithHttpsDeveloperCertificate is experimental; see comment below.
var resourceBuilder = builder.AddResource(resource)
.WithImage("feature23/azuresearchemulator")
.WithImageTag("latest")
.WithImageRegistry("ghcr.io")
.WithImage(AzureSearchEmulatorResource.ImageName)
.WithImageTag(AzureSearchEmulatorResource.ImageTag)
.WithImageRegistry(AzureSearchEmulatorResource.ImageRegistry)
.WithHttpEndpoint(port: httpPort, targetPort: AzureSearchEmulatorResource.DefaultHttpPort, env: "HTTP_PORTS")
.WithHttpsEndpoint(port: httpsPort, targetPort: AzureSearchEmulatorResource.DefaultHttpsPort, env: "HTTPS_PORTS")
.WithEnvironment("ASPNETCORE_URLS", $"https://+:{resource.GetEndpoint("https").Property(EndpointProperty.Port)};http://+:{resource.GetEndpoint("http").Property(EndpointProperty.Port)}")
Expand Down
2 changes: 1 addition & 1 deletion AzureSearchEmulator/AzureSearchEmulator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<UserSecretsId>6751dc28-24d4-4572-8f44-e652593dd678</UserSecretsId>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>1.0.0-beta.3</Version>
<Version>1.0.0-beta.4</Version>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<LangVersion>14</LangVersion>
<IsPackable>true</IsPackable>
Expand Down
Loading