diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index bee2d4a8..ebef9932 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -16,12 +16,9 @@ namespace CodexPlugin public static readonly TimeSpan MaxDownloadTimePerMegabyte = TimeSpan.FromSeconds(2.0); public override string AppName => "codex"; - public override string Image { get; } + public override string Image => GetDockerImage(); - public CodexContainerRecipe() - { - Image = GetDockerImage(); - } + public static string DockerImageOverride { get; set; } = string.Empty; protected override void Initialize(StartupConfig startupConfig) { @@ -122,6 +119,7 @@ namespace CodexPlugin { var image = Environment.GetEnvironmentVariable("CODEXDOCKERIMAGE"); if (!string.IsNullOrEmpty(image)) return image; + if (!string.IsNullOrEmpty(DockerImageOverride)) return DockerImageOverride; return DefaultDockerImage; } } diff --git a/ProjectPlugins/CodexPlugin/LocalCodexBuilder.cs b/ProjectPlugins/CodexPlugin/LocalCodexBuilder.cs new file mode 100644 index 00000000..5e4b2d09 --- /dev/null +++ b/ProjectPlugins/CodexPlugin/LocalCodexBuilder.cs @@ -0,0 +1,116 @@ +using CodexPlugin; +using Logging; +using System.Diagnostics; + +namespace CodexNetDeployer +{ + public class LocalCodexBuilder + { + private readonly ILog log; + private readonly string? repoPath; + private readonly string? dockerUsername; + + public LocalCodexBuilder(ILog log, string? repoPath, string? dockerUsername) + { + this.log = log; + this.repoPath = repoPath; + this.dockerUsername = dockerUsername; + } + + public LocalCodexBuilder(ILog log, string? repoPath) + : this(log, repoPath, Environment.GetEnvironmentVariable("DOCKERUSERNAME")) + { + } + + public LocalCodexBuilder(ILog log) + : this(log, Environment.GetEnvironmentVariable("CODEXREPOPATH")) + { + } + + public void Intialize() + { + if (!IsEnabled()) return; + + if (string.IsNullOrEmpty(dockerUsername)) throw new Exception("Docker username required. (Pass to constructor or set 'DOCKERUSERNAME' environment variable.)"); + if (string.IsNullOrEmpty(repoPath)) throw new Exception("Codex repo path required. (Pass to constructor or set 'CODEXREPOPATH' environment variable.)"); + if (!Directory.Exists(repoPath)) throw new Exception($"Path '{repoPath}' does not exist."); + var files = Directory.GetFiles(repoPath); + if (!files.Any(f => f.ToLowerInvariant().EndsWith("codex.nim"))) throw new Exception($"Path '{repoPath}' does not appear to be the Codex repo root."); + + 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}'."; + } + + public void Build() + { + if (!IsEnabled()) return; + Log("Docker login..."); + DockerLogin(); + + Log($"Logged in. Building Codex image in path '{repoPath}'..."); + + var customImage = GenerateImageName(); + Docker($"build", "-t", customImage, "-f", "./codex.Dockerfile", + "--build-arg=\"MAKE_PARALLEL=4\"", + "--build-arg=\"NIMFLAGS=-d:disableMarchNative -d:codex_enable_api_debug_peers=true -d:codex_enable_api_debug_fetch=true -d:codex_enable_simulated_proof_failures\"", + "--build-arg=\"NAT_IP_AUTO=true\"", + ".."); + + Log($"Image '{customImage}' built successfully. Pushing..."); + + Docker("push", customImage); + + CodexContainerRecipe.DockerImageOverride = customImage; + Log("Image pushed. Good to go!"); + } + + private void DockerLogin() + { + var dockerPassword = Environment.GetEnvironmentVariable("DOCKERPASSWORD"); + + if (string.IsNullOrEmpty(dockerUsername) || string.IsNullOrEmpty(dockerPassword)) + { + Log("Environment variable 'DOCKERPASSWORD' not provided."); + Log("Trying system default..."); + Docker("login"); + } + else + { + Docker("login", "-u", dockerUsername, "-p", dockerPassword); + } + } + + private string GenerateImageName() + { + return $"{dockerUsername!}/nim-codex-autoimage:{Guid.NewGuid().ToString().ToLowerInvariant()}"; + } + + private void Docker(params string[] args) + { + var dockerPath = Path.Combine(repoPath!, "docker"); + + var startInfo = new ProcessStartInfo() + { + FileName = "docker", + Arguments = string.Join(" ", args), + WorkingDirectory = dockerPath, + }; + var process = Process.Start(startInfo); + if (process == null) throw new Exception("Failed to start docker process."); + if (!process.WaitForExit(TimeSpan.FromMinutes(10))) throw new Exception("Docker processed timed out after 10 minutes."); + if (process.ExitCode != 0) throw new Exception("Docker process exited with error."); + } + + private bool IsEnabled() + { + return !string.IsNullOrEmpty(repoPath); + } + + private void Log(string msg) + { + log.Log(msg); + } + } +} diff --git a/Tools/CodexNetDeployer/Configuration.cs b/Tools/CodexNetDeployer/Configuration.cs index 3ff1020e..fa94c157 100644 --- a/Tools/CodexNetDeployer/Configuration.cs +++ b/Tools/CodexNetDeployer/Configuration.cs @@ -1,5 +1,6 @@ using ArgsUniform; using CodexPlugin; +using static Org.BouncyCastle.Math.EC.ECCurve; namespace CodexNetDeployer { @@ -14,6 +15,10 @@ namespace CodexNetDeployer [Uniform("kube-namespace", "kn", "KUBENAMESPACE", true, "Kubernetes namespace to be used for deployment.")] public string KubeNamespace { get; set; } = string.Empty; + [Uniform("codex-local-repo", "cr", "CODEXLOCALREPOPATH", false, "If set, instead of using the default Codex docker image, the local repository will be used to build an image. " + + "This requires the 'DOCKERUSERNAME' and 'DOCKERPASSWORD' environment variables to be set.")] + public string CodexLocalRepoPath { get; set; } = string.Empty; + [Uniform("nodes", "n", "NODES", true, "Number of Codex nodes to be created.")] public int? NumberOfCodexNodes { get; set; } @@ -59,13 +64,44 @@ namespace CodexNetDeployer [Uniform("check-connect", "cc", "CHECKCONNECT", false, "If true, deployer check ensure peer-connectivity between all deployed nodes after deployment. Default is false.")] public bool CheckPeerConnection { get; set; } = false; - - public List Validate() + + public Configuration() + { + // dotnet run \ + //--kube - config =/ opt / kubeconfig.yaml \ + //--kube -namespace=codex-continuous-tests \ + //--nodes=5 \ + //--validators=3 \ + //--log-level=Trace \ + //--storage-quota=2048 \ + //--storage-sell=1024 \ + //--min-price=1024 \ + //--max-collateral=1024 \ + //--max-duration=3600000 \ + //--block-ttl=180 \ + //--block-mi=120 \ + //--block-mn=10000 \ + //--metrics=1 \ + //--check-connect=1 + + KubeNamespace = "autodockertest"; + NumberOfCodexNodes = 3; + NumberOfValidators = 1; + StorageQuota = 2048; + StorageSell = 1024; + + CodexLocalRepoPath = "D:/Projects/nim-codex"; + } + + public List Validate() { var errors = new List(); + StringIsSet(nameof(KubeNamespace), KubeNamespace, errors); + StringIsSet(nameof(KubeConfigFile), KubeConfigFile, errors); + StringIsSet(nameof(TestsTypePodLabel), TestsTypePodLabel, errors); + ForEachProperty( - onString: (n, v) => StringIsSet(n, v, errors), onInt: (n, v) => IntIsOverZero(n, v, errors)); if (NumberOfValidators > NumberOfCodexNodes) @@ -80,12 +116,11 @@ namespace CodexNetDeployer return errors; } - private void ForEachProperty(Action onString, Action onInt) + private void ForEachProperty(Action onInt) { var properties = GetType().GetProperties(); foreach (var p in properties) { - if (p.PropertyType == typeof(string)) onString(p.Name, (string)p.GetValue(this)!); if (p.PropertyType == typeof(int?)) onInt(p.Name, (int?)p.GetValue(this)!); if (p.PropertyType == typeof(int)) onInt(p.Name, (int)p.GetValue(this)!); } diff --git a/Tools/CodexNetDeployer/Deployer.cs b/Tools/CodexNetDeployer/Deployer.cs index 5c1365f0..d95f6bff 100644 --- a/Tools/CodexNetDeployer/Deployer.cs +++ b/Tools/CodexNetDeployer/Deployer.cs @@ -13,11 +13,13 @@ namespace CodexNetDeployer private readonly Configuration config; private readonly PeerConnectivityChecker peerConnectivityChecker; private readonly EntryPoint entryPoint; + private readonly LocalCodexBuilder localCodexBuilder; public Deployer(Configuration config) { this.config = config; peerConnectivityChecker = new PeerConnectivityChecker(); + localCodexBuilder = new LocalCodexBuilder(new ConsoleLog(), config.CodexLocalRepoPath); ProjectPlugin.Load(); ProjectPlugin.Load(); @@ -30,6 +32,8 @@ namespace CodexNetDeployer { var ep = CreateEntryPoint(new ConsoleLog()); + localCodexBuilder.Intialize(); + Log("Using plugins:" + Environment.NewLine); var metadata = ep.GetPluginMetadata(); var longestKey = metadata.Keys.Max(k => k.Length); @@ -44,6 +48,8 @@ namespace CodexNetDeployer public CodexDeployment Deploy() { + localCodexBuilder.Build(); + Log("Initializing..."); var ci = entryPoint.CreateInterface();