Merge branch 'feature/automatic-contracts-image-version-detection'

This commit is contained in:
Ben 2025-04-22 10:17:58 +02:00
commit a77bbbdaaf
No known key found for this signature in database
GPG Key ID: 0F16E812E736C24B
18 changed files with 233 additions and 27 deletions

View File

@ -1,6 +1,6 @@
namespace Core
{
internal class PluginManager
internal class PluginManager : IPluginAccess
{
private readonly List<PluginToolsPair> pairs = new List<PluginToolsPair>();
@ -14,6 +14,7 @@
ApplyLogPrefix(plugin, tools);
}
AwakePlugins();
}
internal void AnnouncePlugins()
@ -43,7 +44,7 @@
}
}
internal T GetPlugin<T>() where T : IProjectPlugin
public T GetPlugin<T>() where T : IProjectPlugin
{
return (T)pairs.Single(p => p.Plugin.GetType() == typeof(T)).Plugin;
}
@ -55,6 +56,14 @@
return plugin;
}
private void AwakePlugins()
{
foreach (var p in pairs)
{
p.Plugin.Awake(this);
}
}
private void ApplyLogPrefix(IProjectPlugin plugin, PluginTools tools)
{
if (plugin is IHasLogPrefix hasLogPrefix)

View File

@ -4,6 +4,7 @@ namespace Core
{
public interface IProjectPlugin
{
void Awake(IPluginAccess access);
void Announce();
void Decommission();
}
@ -18,6 +19,11 @@ namespace Core
void AddMetadata(IAddMetadata metadata);
}
public interface IPluginAccess
{
T GetPlugin<T>() where T : IProjectPlugin;
}
public static class ProjectPlugin
{
/// <summary>

View File

@ -6,13 +6,17 @@ namespace CodexContractsPlugin
{
public class CodexContractsContainerRecipe : ContainerRecipeFactory
{
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:latest-dist-tests";
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
private readonly VersionRegistry versionRegistry;
public override string AppName => "codex-contracts";
public override string Image => DockerImage;
public override string Image => versionRegistry.GetContractsDockerImage();
public CodexContractsContainerRecipe(VersionRegistry versionRegistry)
{
this.versionRegistry = versionRegistry;
}
protected override void Initialize(StartupConfig startupConfig)
{

View File

@ -7,15 +7,23 @@ namespace CodexContractsPlugin
{
private readonly IPluginTools tools;
private readonly CodexContractsStarter starter;
private readonly VersionRegistry versionRegistry;
private readonly CodexContractsContainerRecipe recipe;
public CodexContractsPlugin(IPluginTools tools)
{
this.tools = tools;
starter = new CodexContractsStarter(tools);
versionRegistry = new VersionRegistry(tools.GetLog());
recipe = new CodexContractsContainerRecipe(versionRegistry);
starter = new CodexContractsStarter(tools, recipe);
}
public string LogPrefix => "(CodexContracts) ";
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log($"Loaded Codex-Marketplace SmartContracts");
@ -23,7 +31,7 @@ namespace CodexContractsPlugin
public void AddMetadata(IAddMetadata metadata)
{
metadata.Add("codexcontractsid", CodexContractsContainerRecipe.DockerImage);
metadata.Add("codexcontractsid", recipe.Image);
}
public void Decommission()
@ -40,5 +48,10 @@ namespace CodexContractsPlugin
deployment = SerializeGate.Gate(deployment);
return starter.Wrap(gethNode, deployment);
}
public void SetCodexDockerImageProvider(ICodexDockerImageProvider provider)
{
versionRegistry.SetProvider(provider);
}
}
}

View File

@ -12,10 +12,12 @@ namespace CodexContractsPlugin
public class CodexContractsStarter
{
private readonly IPluginTools tools;
private readonly CodexContractsContainerRecipe recipe;
public CodexContractsStarter(IPluginTools tools)
public CodexContractsStarter(IPluginTools tools, CodexContractsContainerRecipe recipe)
{
this.tools = tools;
this.recipe = recipe;
}
public CodexContractsDeployment Deploy(CoreInterface ci, IGethNode gethNode)
@ -26,7 +28,7 @@ namespace CodexContractsPlugin
var startupConfig = CreateStartupConfig(gethNode);
startupConfig.NameOverride = "codex-contracts";
var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig).WaitForOnline();
var containers = workflow.Start(1, recipe, startupConfig).WaitForOnline();
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure.");
var container = containers.Containers[0];

View File

@ -21,6 +21,11 @@ namespace CodexContractsPlugin
return WrapCodexContractsDeployment(ci, gethNode, deployment);
}
public static void SetCodexDockerImageProvider(this CoreInterface ci, ICodexDockerImageProvider provider)
{
Plugin(ci).SetCodexDockerImageProvider(provider);
}
private static CodexContractsPlugin Plugin(CoreInterface ci)
{
return ci.GetPlugin<CodexContractsPlugin>();

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,125 @@
using System.Diagnostics;
using Logging;
namespace CodexContractsPlugin
{
public interface ICodexDockerImageProvider
{
string GetCodexDockerImage();
}
public class VersionRegistry
{
private ICodexDockerImageProvider provider = new ExceptionProvider();
private static readonly Dictionary<string, string> cache = new Dictionary<string, string>();
private static readonly object cacheLock = new object();
private readonly ILog log;
public VersionRegistry(ILog log)
{
this.log = log;
}
public void SetProvider(ICodexDockerImageProvider provider)
{
this.provider = provider;
}
public string GetContractsDockerImage()
{
try
{
var codexImage = provider.GetCodexDockerImage();
return GetContractsDockerImage(codexImage);
}
catch (Exception exc)
{
throw new Exception("Failed to get contracts docker image.", exc);
}
}
private string GetContractsDockerImage(string codexImage)
{
lock (cacheLock)
{
if (cache.TryGetValue(codexImage, out string? value))
{
return value;
}
var result = GetContractsImage(codexImage);
cache.Add(codexImage, result);
return result;
}
}
private string GetContractsImage(string codexImage)
{
var inspectResult = InspectCodexImage(codexImage);
var image = ParseCodexContractsImageName(inspectResult);
log.Log($"From codex docker image '{codexImage}', determined codex-contracts docker image: '{image}'");
return image;
}
private string InspectCodexImage(string img)
{
Execute("docker", $"pull {img}");
return Execute("docker", $"inspect {img}");
}
private string ParseCodexContractsImageName(string inspectResult)
{
// It is a nice json structure. But we only need this one line.
// "storage.codex.nim-codex.blockchain-image": "codexstorage/codex-contracts-eth:sha-0bf1385-dist-tests"
var lines = inspectResult.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var line = lines.Single(l => l.Contains("storage.codex.nim-codex.blockchain-image"));
var tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return tokens.Last().Replace("\"", "").Trim();
}
private string Execute(string cmd, string args)
{
var startInfo = new ProcessStartInfo(
fileName: cmd,
arguments: args
);
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
var process = Process.Start(startInfo);
if (process == null)
{
throw new Exception("Failed to start: " + cmd + args);
}
KillAfterTimeout(process);
process.WaitForExit();
return process.StandardOutput.ReadToEnd();
}
private void KillAfterTimeout(Process process)
{
// There's a known issue that some docker commands on some platforms
// will fail to stop on their own. This has been known since 2019 and it's not fixed.
// So we will issue a kill to the process ourselves if it exceeds a timeout.
Task.Run(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(30.0));
if (process != null && !process.HasExited)
{
process.Kill();
}
});
}
}
internal class ExceptionProvider : ICodexDockerImageProvider
{
public string GetCodexDockerImage()
{
throw new InvalidOperationException("CodexContractsPlugin has not yet received a CodexDockerImageProvider " +
"and so cannot select a compatible contracts docker image.");
}
}
}

View File

@ -17,6 +17,10 @@ namespace CodexDiscordBotPlugin
public string LogPrefix => "(DiscordBot) ";
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log($"Codex DiscordBot (BiblioTech) loaded.");

View File

@ -7,7 +7,6 @@ namespace CodexPlugin
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
private const string DefaultDockerImage = "codexstorage/nim-codex:latest-dist-tests";
public const string ApiPortTag = "codex_api_port";
public const string ListenPortTag = "codex_listen_port";
public const string MetricsPortTag = "codex_metrics_port";
@ -16,11 +15,15 @@ namespace CodexPlugin
// Used by tests for time-constraint assertions.
public static readonly TimeSpan MaxUploadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
public static readonly TimeSpan MaxDownloadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
private readonly CodexDockerImage codexDockerImage;
public override string AppName => "codex";
public override string Image => GetDockerImage();
public override string Image => codexDockerImage.GetCodexDockerImage();
public static string DockerImageOverride { get; set; } = string.Empty;
public CodexContainerRecipe(CodexDockerImage codexDockerImage)
{
this.codexDockerImage = codexDockerImage;
}
protected override void Initialize(StartupConfig startupConfig)
{
@ -163,13 +166,5 @@ namespace CodexPlugin
// Default Codex quota: 8 Gb, using +20% to be safe.
return 8.GB().Multiply(1.2);
}
private string GetDockerImage()
{
var image = Environment.GetEnvironmentVariable("CODEXDOCKERIMAGE");
if (!string.IsNullOrEmpty(image)) return image;
if (!string.IsNullOrEmpty(DockerImageOverride)) return DockerImageOverride;
return DefaultDockerImage;
}
}
}

View File

@ -0,0 +1,20 @@
using CodexContractsPlugin;
namespace CodexPlugin
{
public class CodexDockerImage : ICodexDockerImageProvider
{
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-c9a5ef8-dist-tests";
//"codexstorage/nim-codex:latest-dist-tests";
public static string Override { get; set; } = string.Empty;
public string GetCodexDockerImage()
{
var image = Environment.GetEnvironmentVariable("CODEXDOCKERIMAGE");
if (!string.IsNullOrEmpty(image)) return image;
if (!string.IsNullOrEmpty(Override)) return Override;
return DefaultDockerImage;
}
}
}

View File

@ -13,12 +13,15 @@ namespace CodexPlugin
private readonly CodexLogLevel defaultLogLevel = CodexLogLevel.Trace;
private readonly CodexHooksFactory hooksFactory = new CodexHooksFactory();
private readonly ProcessControlMap processControlMap = new ProcessControlMap();
private readonly CodexDockerImage codexDockerImage = new CodexDockerImage();
private readonly CodexContainerRecipe recipe;
private readonly CodexWrapper codexWrapper;
public CodexPlugin(IPluginTools tools)
{
this.tools = tools;
recipe = new CodexContainerRecipe(codexDockerImage);
codexStarter = CreateCodexStarter();
codexWrapper = new CodexWrapper(tools, processControlMap, hooksFactory);
}
@ -28,7 +31,7 @@ namespace CodexPlugin
if (UseContainers)
{
Log("Using Containerized Codex instances");
return new ContainerCodexStarter(tools, processControlMap);
return new ContainerCodexStarter(tools, recipe, processControlMap);
}
Log("Using Binary Codex instances");
@ -37,8 +40,15 @@ namespace CodexPlugin
public string LogPrefix => "(Codex) ";
public void Awake(IPluginAccess access)
{
access.GetPlugin<CodexContractsPlugin.CodexContractsPlugin>().SetCodexDockerImageProvider(codexDockerImage);
}
public void Announce()
{
// give codex docker image to contracts plugin.
Log($"Loaded with Codex ID: '{codexWrapper.GetCodexId()}' - Revision: {codexWrapper.GetCodexRevision()}");
}

View File

@ -10,12 +10,13 @@ namespace CodexPlugin
{
private readonly IPluginTools pluginTools;
private readonly ProcessControlMap processControlMap;
private readonly CodexContainerRecipe recipe = new CodexContainerRecipe();
private readonly CodexContainerRecipe recipe;
private readonly ApiChecker apiChecker;
public ContainerCodexStarter(IPluginTools pluginTools, ProcessControlMap processControlMap)
public ContainerCodexStarter(IPluginTools pluginTools, CodexContainerRecipe recipe, ProcessControlMap processControlMap)
{
this.pluginTools = pluginTools;
this.recipe = recipe;
this.processControlMap = processControlMap;
apiChecker = new ApiChecker(pluginTools);
}

View File

@ -40,7 +40,7 @@ namespace CodexNetDeployer
Log($"Codex docker image will be built in path '{repoPath}'.");
Log("Please note this can take several minutes. If you're not trying to use a Codex image with local code changes,");
Log("Consider using the default test image or consider setting the 'CODEXDOCKERIMAGE' environment variable to use an already built image.");
CodexContainerRecipe.DockerImageOverride = $"Using docker image locally built in path '{repoPath}'.";
CodexDockerImage.Override = $"Using docker image locally built in path '{repoPath}'.";
}
public void Build()
@ -62,7 +62,7 @@ namespace CodexNetDeployer
Docker("push", customImage);
CodexContainerRecipe.DockerImageOverride = customImage;
CodexDockerImage.Override = customImage;
Log("Image pushed. Good to go!");
}

View File

@ -13,6 +13,10 @@ namespace DeployAndRunPlugin
this.tools = tools;
}
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log("Deploy-and-Run plugin loaded.");

View File

@ -16,6 +16,10 @@ namespace GethPlugin
public string LogPrefix => "(Geth) ";
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log($"Loaded Geth plugin.");

View File

@ -18,6 +18,10 @@ namespace MetricsPlugin
public string LogPrefix => "(Metrics) ";
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log($"Prometheus plugin loaded with '{starter.GetPrometheusId()}'.");

View File

@ -33,7 +33,7 @@ namespace ContinuousTests
var entryPoint = CreateEntryPoint();
// We have to be sure that the transient node we start is using the same image as whatever's already in the deployed network.
// Therefore, we use the image of the bootstrap node.
CodexContainerRecipe.DockerImageOverride = bootstrapNode.GetImageName();
CodexDockerImage.Override = bootstrapNode.GetImageName();
try
{