// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using Aspire.Hosting.Publishing;
#pragma warning disable ASPIREPIPELINES003
#pragma warning disable ASPIRECONTAINERRUNTIME001
namespace Aspire.Hosting.Tests.Publishing;
using Aspire.Hosting.ApplicationModel;
public sealed class FakeContainerRuntime(bool shouldFail = false, bool isRunning = true, string name = "fake-runtime") : IContainerRuntime, IContainerRuntimeResolver
{
private int _checkIfRunningCallCount;
private int _resolveAsyncCallCount;
public string Name => name;
public bool WasHealthCheckCalled => CheckIfRunningCallCount > 0;
public int CheckIfRunningCallCount => Volatile.Read(ref _checkIfRunningCallCount);
public int ResolveAsyncCallCount => Volatile.Read(ref _resolveAsyncCallCount);
public bool WasTagImageCalled { get; private set; }
public bool WasRemoveImageCalled { get; private set; }
public bool WasPushImageCalled { get; private set; }
public bool WasBuildImageCalled { get; private set; }
public bool WasInspectImageConfigCalled { get; private set; }
public bool WasInspectImageManifestCalled { get; private set; }
public bool WasLoginToRegistryCalled { get; private set; }
public bool WasComposeDownCalled { get; private set; }
public ComposeOperationContext? LastComposeDownContext { get; private set; }
public ConcurrentBag<(string localImageName, string targetImageName)> TagImageCalls { get; } = [];
public ConcurrentBag<string> RemoveImageCalls { get; } = [];
public ConcurrentBag<IResource> PushImageCalls { get; } = [];
public ConcurrentBag<string> InspectImageConfigCalls { get; } = [];
public ConcurrentBag<string> InspectImageManifestCalls { get; } = [];
public ConcurrentBag<(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options)> BuildImageCalls { get; } = [];
public ConcurrentBag<(string registryServer, string username, string password)> LoginToRegistryCalls { get; } = [];
public Dictionary<string, string?>? CapturedBuildArguments { get; private set; }
public Dictionary<string, BuildImageSecretValue>? CapturedBuildSecrets { get; private set; }
public string? CapturedStage { get; private set; }
public Func<string, string, ContainerImageBuildOptions?, Dictionary<string, string?>, Dictionary<string, BuildImageSecretValue>, string?, CancellationToken, Task>? BuildImageAsyncCallback { get; set; }
public Func<string, string, CancellationToken, Task>? TagImageAsyncCallback { get; set; }
public Func<string, CancellationToken, Task>? RemoveImageAsyncCallback { get; set; }
public Func<string, CancellationToken, Task<ContainerImageManifestInspectionResult>>? InspectImageManifestAsyncCallback { get; set; }
public Func<CancellationToken, Task<bool>>? CheckIfRunningAsyncCallback { get; set; }
public Func<CancellationToken, Task<IContainerRuntime>>? ResolveAsyncCallback { get; set; }
public string? InspectedImageDigest { get; set; }
public string? InspectedImageOperatingSystem { get; set; }
public string? InspectedImageArchitecture { get; set; }
public Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken)
{
Interlocked.Increment(ref _checkIfRunningCallCount);
return CheckIfRunningAsyncCallback?.Invoke(cancellationToken) ?? Task.FromResult(isRunning && !shouldFail);
}
public Task TagImageAsync(string localImageName, string targetImageName, CancellationToken cancellationToken)
{
WasTagImageCalled = true;
TagImageCalls.Add((localImageName, targetImageName));
if (shouldFail)
{
throw new InvalidOperationException("Fake container runtime is configured to fail");
}
return TagImageAsyncCallback?.Invoke(localImageName, targetImageName, cancellationToken) ?? Task.CompletedTask;
}
public Task RemoveImageAsync(string imageName, CancellationToken cancellationToken)
{
WasRemoveImageCalled = true;
RemoveImageCalls.Add(imageName);
if (shouldFail)
{
throw new InvalidOperationException("Fake container runtime is configured to fail");
}
return RemoveImageAsyncCallback?.Invoke(imageName, cancellationToken) ?? Task.CompletedTask;
}
public Task PushImageAsync(IResource resource, CancellationToken cancellationToken)
{
WasPushImageCalled = true;
PushImageCalls.Add(resource);
if (shouldFail)
{
throw new InvalidOperationException("Fake container runtime is configured to fail");
}
return Task.CompletedTask;
}
public async Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, BuildImageSecretValue> buildSecrets, string? stage, CancellationToken cancellationToken)
{
// Capture the arguments for verification in tests
CapturedBuildArguments = buildArguments;
CapturedBuildSecrets = buildSecrets;
CapturedStage = stage;
WasBuildImageCalled = true;
BuildImageCalls.Add((contextPath, dockerfilePath, options));
if (shouldFail)
{
throw new InvalidOperationException("Fake container runtime is configured to fail");
}
if (BuildImageAsyncCallback is not null)
{
await BuildImageAsyncCallback(contextPath, dockerfilePath, options, buildArguments, buildSecrets, stage, cancellationToken);
}
// For testing, we don't need to actually build anything
}
public Task LoginToRegistryAsync(string registryServer, string username, string password, CancellationToken cancellationToken)
{
WasLoginToRegistryCalled = true;
LoginToRegistryCalls.Add((registryServer, username, password));
if (shouldFail)
{
throw new InvalidOperationException("Fake container runtime is configured to fail");
}
return Task.CompletedTask;
}
public Task<ContainerImageConfigInspectionResult> InspectImageConfigAsync(string imageName, CancellationToken cancellationToken)
{
WasInspectImageConfigCalled = true;
InspectImageConfigCalls.Add(imageName);
if (shouldFail)
{
throw new InvalidOperationException("Fake container runtime is configured to fail");
}
var config = new ContainerImageConfig([], [], workingDirectory: null);
return Task.FromResult(ContainerImageConfigInspectionResult.Success(config, "{}"));
}
public Task<ContainerImageManifestInspectionResult> InspectImageManifestAsync(string imageName, CancellationToken cancellationToken)
{
WasInspectImageManifestCalled = true;
InspectImageManifestCalls.Add(imageName);
if (shouldFail)
{
throw new InvalidOperationException("Fake container runtime is configured to fail");
}
if (InspectImageManifestAsyncCallback is not null)
{
return InspectImageManifestAsyncCallback(imageName, cancellationToken);
}
if (InspectedImageDigest is not null &&
InspectedImageOperatingSystem is not null &&
InspectedImageArchitecture is not null)
{
var manifest = new ContainerImageManifest(
InspectedImageDigest,
InspectedImageOperatingSystem,
InspectedImageArchitecture);
return Task.FromResult(ContainerImageManifestInspectionResult.Success([manifest]));
}
return Task.FromResult(ContainerImageManifestInspectionResult.Success([], "{}"));
}
public Task ComposeUpAsync(ComposeOperationContext context, CancellationToken cancellationToken)
{
if (shouldFail)
{
throw new DistributedApplicationException("Fake container runtime is configured to fail");
}
return Task.CompletedTask;
}
public Task ComposeDownAsync(ComposeOperationContext context, CancellationToken cancellationToken)
{
WasComposeDownCalled = true;
LastComposeDownContext = context;
if (shouldFail)
{
throw new DistributedApplicationException("Fake container runtime is configured to fail");
}
return Task.CompletedTask;
}
public Task<IReadOnlyList<ComposeServiceInfo>?> ComposeListServicesAsync(ComposeOperationContext context, CancellationToken cancellationToken)
{
return Task.FromResult<IReadOnlyList<ComposeServiceInfo>?>(null);
}
public Task<IContainerRuntime> ResolveAsync(CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _resolveAsyncCallCount);
return ResolveAsyncCallback?.Invoke(cancellationToken) ?? Task.FromResult<IContainerRuntime>(this);
}
}