diff --git a/AzureSearchEmulator.Aspire.DemoAppHost/AppHost.cs b/AzureSearchEmulator.Aspire.DemoAppHost/AppHost.cs index 6616435..c8bc1d0 100644 --- a/AzureSearchEmulator.Aspire.DemoAppHost/AppHost.cs +++ b/AzureSearchEmulator.Aspire.DemoAppHost/AppHost.cs @@ -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(); diff --git a/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceExtensionsTests.cs b/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceExtensionsTests.cs index 06d4942..ef4e451 100644 --- a/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceExtensionsTests.cs +++ b/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceExtensionsTests.cs @@ -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(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] diff --git a/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceTests.cs b/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceTests.cs index d9bf234..09a936e 100644 --- a/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceTests.cs +++ b/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceTests.cs @@ -1,4 +1,5 @@ -using F23.Aspire.Hosting.AzureSearchEmulator; +using System.Xml.Linq; +using F23.Aspire.Hosting.AzureSearchEmulator; namespace AzureSearchEmulator.Aspire.Tests; @@ -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 "+" 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; + } } diff --git a/AzureSearchEmulator.Aspire/AzureSearchEmulator.Aspire.csproj b/AzureSearchEmulator.Aspire/AzureSearchEmulator.Aspire.csproj index 7f3c6c3..4abdfb9 100644 --- a/AzureSearchEmulator.Aspire/AzureSearchEmulator.Aspire.csproj +++ b/AzureSearchEmulator.Aspire/AzureSearchEmulator.Aspire.csproj @@ -13,7 +13,7 @@ azure search emulator ai hosting docker Azure Search Emulator hosting support for Aspire. true - 1.0.0-beta.3 + 1.0.0-beta.4 feature[23] Copyright (c) feature[23] 2025 Paul Irwin diff --git a/AzureSearchEmulator.Aspire/AzureSearchEmulatorResource.cs b/AzureSearchEmulator.Aspire/AzureSearchEmulatorResource.cs index 438fe7c..5e1b19e 100644 --- a/AzureSearchEmulator.Aspire/AzureSearchEmulatorResource.cs +++ b/AzureSearchEmulator.Aspire/AzureSearchEmulatorResource.cs @@ -1,4 +1,5 @@ -using Aspire.Hosting.ApplicationModel; +using System.Reflection; +using Aspire.Hosting.ApplicationModel; namespace F23.Aspire.Hosting.AzureSearchEmulator; @@ -6,4 +7,27 @@ 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"; + + /// + /// The container image tag used by . + /// + /// + /// 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. + /// + internal static readonly string ImageTag = typeof(AzureSearchEmulatorResource).Assembly + .GetCustomAttribute()! + .InformationalVersion + // The SDK appends "+" to the informational version; the published image tag + // carries only the version itself. + .Split('+')[0]; } diff --git a/AzureSearchEmulator.Aspire/AzureSearchEmulatorResourceExtensions.cs b/AzureSearchEmulator.Aspire/AzureSearchEmulatorResourceExtensions.cs index bbd724c..90672a3 100644 --- a/AzureSearchEmulator.Aspire/AzureSearchEmulatorResourceExtensions.cs +++ b/AzureSearchEmulator.Aspire/AzureSearchEmulatorResourceExtensions.cs @@ -21,7 +21,8 @@ public static class AzureSearchEmulatorResourceExtensions /// /// It is recommended to configure a volume for persisting index data using /// . - /// 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 /// method. /// public IResourceBuilder AddAzureSearchEmulator(string name, @@ -32,9 +33,9 @@ public IResourceBuilder 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)}") diff --git a/AzureSearchEmulator/AzureSearchEmulator.csproj b/AzureSearchEmulator/AzureSearchEmulator.csproj index 4d91153..5172aba 100644 --- a/AzureSearchEmulator/AzureSearchEmulator.csproj +++ b/AzureSearchEmulator/AzureSearchEmulator.csproj @@ -5,7 +5,7 @@ 6751dc28-24d4-4572-8f44-e652593dd678 enable enable - 1.0.0-beta.3 + 1.0.0-beta.4 Nullable 14 true