diff --git a/Framework/KubernetesWorkflow/K8sController.cs b/Framework/KubernetesWorkflow/K8sController.cs index b994b756..bfea181b 100644 --- a/Framework/KubernetesWorkflow/K8sController.cs +++ b/Framework/KubernetesWorkflow/K8sController.cs @@ -84,6 +84,31 @@ namespace KubernetesWorkflow return result; } + public int[] GetUsedExternalPorts() + { + return client.Run(c => + { + var result = new List(); + + var services = c.ListServiceForAllNamespaces(); + var nodePorts = services.Items.Where(s => s.Spec.Type == "NodePort").ToArray(); + if (!nodePorts.Any()) return result.ToArray(); + + foreach (var service in nodePorts) + { + foreach (var port in service.Spec.Ports) + { + if (port.NodePort.HasValue) + { + result.Add(port.NodePort.Value); + } + } + } + + return result.ToArray(); + }); + } + public void DeleteAllNamespacesStartingWith(string prefix) { log.Debug(); @@ -704,17 +729,17 @@ namespace KubernetesWorkflow private RunningService? CreateInternalService(ContainerRecipe[] recipes) { - return CreateService(recipes, r => r.InternalPorts.Concat(r.ExposedPorts).ToArray(), "ClusterIP", "int"); + return CreateService(recipes, r => r.InternalPorts.Concat(r.ExposedPorts).ToArray(), "ClusterIP", "int", false); } private RunningService? CreateExternalService(ContainerRecipe[] recipes) { - return CreateService(recipes, r => r.ExposedPorts, "NodePort", "ext"); + return CreateService(recipes, r => r.ExposedPorts, "NodePort", "ext", true); } - private RunningService? CreateService(ContainerRecipe[] recipes, Func portSelector, string serviceType, string namePostfix) + private RunningService? CreateService(ContainerRecipe[] recipes, Func portSelector, string serviceType, string namePostfix, bool isNodePort) { - var ports = CreateServicePorts(recipes, portSelector); + var ports = CreateServicePorts(recipes, portSelector, isNodePort); if (!ports.Any()) return null; var serviceSpec = new V1Service @@ -788,7 +813,7 @@ namespace KubernetesWorkflow }; } - private List CreateServicePorts(ContainerRecipe[] recipes, Func portSelector) + private List CreateServicePorts(ContainerRecipe[] recipes, Func portSelector, bool isNodePort) { var result = new List(); foreach (var recipe in recipes) @@ -796,29 +821,33 @@ namespace KubernetesWorkflow var ports = portSelector(recipe); foreach (var port in ports) { - result.AddRange(CreateServicePorts(recipe, port)); + result.AddRange(CreateServicePorts(recipe, port, isNodePort)); } } return result; } - private List CreateServicePorts(ContainerRecipe recipe, Port recipePort) + private List CreateServicePorts(ContainerRecipe recipe, Port recipePort, bool isNodePort) { var result = new List(); - if (recipePort.IsTcp()) CreateServicePort(result, recipe, recipePort, "TCP"); - if (recipePort.IsUdp()) CreateServicePort(result, recipe, recipePort, "UDP"); + if (recipePort.IsTcp()) CreateServicePort(result, recipe, recipePort, "TCP", isNodePort); + if (recipePort.IsUdp()) CreateServicePort(result, recipe, recipePort, "UDP", isNodePort); return result; } - private void CreateServicePort(List result, ContainerRecipe recipe, Port port, string protocol) + private void CreateServicePort(List result, ContainerRecipe recipe, Port port, string protocol, bool isNodePort) { - result.Add(new V1ServicePort + var p = new V1ServicePort { Name = GetNameForPort(recipe, port), Protocol = protocol, Port = port.Number, TargetPort = GetNameForPort(recipe, port) - }); + }; + + if (isNodePort) p.NodePort = port.Number; + + result.Add(p); } #endregion diff --git a/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs b/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs index c2ba8d8f..931013dc 100644 --- a/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs +++ b/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs @@ -56,17 +56,17 @@ namespace KubernetesWorkflow.Recipe protected Port AddExposedPort(string tag, PortProtocol protocol = PortProtocol.TCP) { - return AddExposedPort(factory.CreatePort(tag, protocol)); + return AddExposedPort(factory.CreateExternalPort(tag, protocol)); } protected Port AddExposedPort(int number, string tag, PortProtocol protocol = PortProtocol.TCP) { - return AddExposedPort(factory.CreatePort(number, tag, protocol)); + return AddExposedPort(factory.CreateExternalPort(number, tag, protocol)); } protected Port AddInternalPort(string tag = "", PortProtocol protocol = PortProtocol.TCP) { - var p = factory.CreatePort(tag, protocol); + var p = factory.CreateInternalPort(tag, protocol); internalPorts.Add(p); return p; } diff --git a/Framework/KubernetesWorkflow/Recipe/RecipeComponentFactory.cs b/Framework/KubernetesWorkflow/Recipe/RecipeComponentFactory.cs index c144746d..39353856 100644 --- a/Framework/KubernetesWorkflow/Recipe/RecipeComponentFactory.cs +++ b/Framework/KubernetesWorkflow/Recipe/RecipeComponentFactory.cs @@ -5,16 +5,36 @@ namespace KubernetesWorkflow.Recipe { public class RecipeComponentFactory { - private NumberSource portNumberSource = new NumberSource(8080); + private readonly NumberSource internalNumberSource = new NumberSource(8080); + private static readonly NumberSource externalNumberSource = new NumberSource(30000); + private static int[] usedExternalPorts = Array.Empty(); - public Port CreatePort(int number, string tag, PortProtocol protocol) + public void Update(K8sController controller) { + usedExternalPorts = controller.GetUsedExternalPorts(); + } + + public Port CreateInternalPort(string tag, PortProtocol protocol) + { + return new Port(internalNumberSource.GetNextNumber(), tag, protocol); + } + + public Port CreateExternalPort(int number, string tag, PortProtocol protocol) + { + if (usedExternalPorts.Contains(number)) throw new Exception($"External port number {number} is already in use by the cluster."); return new Port(number, tag, protocol); } - public Port CreatePort(string tag, PortProtocol protocol) + public Port CreateExternalPort(string tag, PortProtocol protocol) { - return new Port(portNumberSource.GetNextNumber(), tag, protocol); + while (true) + { + var number = externalNumberSource.GetNextNumber(); + if (!usedExternalPorts.Contains(number)) + { + return new Port(number, tag, protocol); + } + } } public EnvVar CreateEnvVar(string name, int value) diff --git a/Framework/KubernetesWorkflow/StartupWorkflow.cs b/Framework/KubernetesWorkflow/StartupWorkflow.cs index 20ab7538..15bc3e30 100644 --- a/Framework/KubernetesWorkflow/StartupWorkflow.cs +++ b/Framework/KubernetesWorkflow/StartupWorkflow.cs @@ -54,12 +54,19 @@ namespace KubernetesWorkflow { return K8s(controller => { + componentFactory.Update(controller); + var recipes = CreateRecipes(numberOfContainers, recipeFactory, startupConfig); var startResult = controller.BringOnline(recipes, location); var containers = CreateContainers(startResult, recipes, startupConfig); var rc = new RunningContainers(startupConfig, startResult, containers); cluster.Configuration.Hooks.OnContainersStarted(rc); + + if (startResult.ExternalService != null) + { + componentFactory.Update(controller); + } return rc; }); } @@ -222,7 +229,7 @@ namespace KubernetesWorkflow } catch (k8s.Autorest.HttpOperationException ex) { - log.Error(JsonConvert.SerializeObject(ex)); + log.Error(JsonConvert.SerializeObject(ex.Response)); throw; } } @@ -238,7 +245,7 @@ namespace KubernetesWorkflow } catch (k8s.Autorest.HttpOperationException ex) { - log.Error(JsonConvert.SerializeObject(ex)); + log.Error(JsonConvert.SerializeObject(ex.Response)); throw; } } diff --git a/Framework/Logging/BaseLog.cs b/Framework/Logging/BaseLog.cs index bb5df83a..cc323a8b 100644 --- a/Framework/Logging/BaseLog.cs +++ b/Framework/Logging/BaseLog.cs @@ -55,7 +55,9 @@ namespace Logging public virtual void Error(string message) { - Log($"[ERROR] {message}"); + var msg = $"[ERROR] {message}"; + Console.WriteLine(msg); + Log(msg); } public virtual void AddStringReplace(string from, string to) diff --git a/Framework/Logging/NullLog.cs b/Framework/Logging/NullLog.cs index fb7a70e9..a9cf8d2d 100644 --- a/Framework/Logging/NullLog.cs +++ b/Framework/Logging/NullLog.cs @@ -16,6 +16,7 @@ public override void Error(string message) { + Console.WriteLine("Error: " + message); base.Error(message); } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs index 3ca6669e..ea69f3b6 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs @@ -19,12 +19,24 @@ namespace CodexDiscordBotPlugin AddEnvVar("SERVERNAME", config.ServerName); AddEnvVar("ADMINROLE", config.AdminRoleName); AddEnvVar("ADMINCHANNELNAME", config.AdminChannelName); + AddEnvVar("KUBECONFIG", "/opt/kubeconfig.yaml"); + AddEnvVar("KUBENAMESPACE", config.KubeNamespace); + + var gethInfo = config.GethInfo; + AddEnvVar("GETH_HOST", gethInfo.Host); + AddEnvVar("GETH_HTTP_PORT", gethInfo.Port.ToString()); + AddEnvVar("GETH_PRIVATE_KEY", gethInfo.PrivKey); + AddEnvVar("CODEXCONTRACTS_MARKETPLACEADDRESS", gethInfo.MarketplaceAddress); + AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress); + AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi); if (!string.IsNullOrEmpty(config.DataPath)) { AddEnvVar("DATAPATH", config.DataPath); AddVolume(config.DataPath, 1.GB()); } + + AddVolume(name: "kubeconfig", mountPath: "/opt/kubeconfig.yaml", subPath: "kubeconfig.yaml", secret: "discordbot-sa-kubeconfig"); } } } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs index bb00a8de..85c103f2 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs @@ -2,13 +2,15 @@ { public class DiscordBotStartupConfig { - public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName) + public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo) { Name = name; Token = token; ServerName = serverName; AdminRoleName = adminRoleName; AdminChannelName = adminChannelName; + KubeNamespace = kubeNamespace; + GethInfo = gethInfo; } public string Name { get; } @@ -16,6 +18,28 @@ public string ServerName { get; } public string AdminRoleName { get; } public string AdminChannelName { get; } + public string KubeNamespace { get; } + public DiscordBotGethInfo GethInfo { get; } public string? DataPath { get; set; } } + + public class DiscordBotGethInfo + { + public DiscordBotGethInfo(string host, int port, string privKey, string marketplaceAddress, string tokenAddress, string abi) + { + Host = host; + Port = port; + PrivKey = privKey; + MarketplaceAddress = marketplaceAddress; + TokenAddress = tokenAddress; + Abi = abi; + } + + public string Host { get; } + public int Port { get; } + public string PrivKey { get; } + public string MarketplaceAddress { get; } + public string TokenAddress { get; } + public string Abi { get; } + } } diff --git a/ProjectPlugins/CodexPlugin/CodexApiTypes.cs b/ProjectPlugins/CodexPlugin/CodexApiTypes.cs index af523833..b1965cde 100644 --- a/ProjectPlugins/CodexPlugin/CodexApiTypes.cs +++ b/ProjectPlugins/CodexPlugin/CodexApiTypes.cs @@ -5,9 +5,10 @@ namespace CodexPlugin public class CodexDebugResponse { public string id { get; set; } = string.Empty; - public string[] addrs { get; set; } = new string[0]; + public string[] addrs { get; set; } = Array.Empty(); public string repo { get; set; } = string.Empty; public string spr { get; set; } = string.Empty; + public string[] announceAddresses { get; set; } = Array.Empty(); public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty(); public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty(); public CodexDebugVersionResponse codex { get; set; } = new(); diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index e6315d28..87d7f06c 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -48,8 +48,9 @@ namespace CodexPlugin if (config.PublicTestNet != null) { - AddEnvVar("CODEX_NAT", config.PublicTestNet.PublicNatIP); + // This makes the node announce itself to its public IP address. AddEnvVar("NAT_IP_AUTO", "false"); + AddEnvVar("NAT_PUBLIC_IP_AUTO", PublicIpService.Address); } else { diff --git a/ProjectPlugins/CodexPlugin/CodexSetup.cs b/ProjectPlugins/CodexPlugin/CodexSetup.cs index 8b7dbb1d..a491fb32 100644 --- a/ProjectPlugins/CodexPlugin/CodexSetup.cs +++ b/ProjectPlugins/CodexPlugin/CodexSetup.cs @@ -133,7 +133,7 @@ namespace CodexPlugin private IEnumerable DescribeArgs() { - if (PublicTestNet != null) yield return $"Public TestNet at {PublicTestNet.PublicNatIP}:{PublicTestNet.PublicListenPort}"; + if (PublicTestNet != null) yield return $"Public TestNet with listenPort: {PublicTestNet.PublicListenPort}"; yield return $"LogLevel={LogLevelWithTopics()}"; if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}"; if (StorageQuota != null) yield return $"StorageQuota={StorageQuota}"; diff --git a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs index 6e59f2a0..ed840d0a 100644 --- a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs +++ b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs @@ -65,7 +65,6 @@ namespace CodexPlugin public class CodexTestNetConfig { - public string PublicNatIP { get; set; } = string.Empty; public int PublicDiscoveryPort { get; set; } public int PublicListenPort { get; set; } } diff --git a/ProjectPlugins/GethPlugin/GethContainerRecipe.cs b/ProjectPlugins/GethPlugin/GethContainerRecipe.cs index 75acd69e..e4c2fe86 100644 --- a/ProjectPlugins/GethPlugin/GethContainerRecipe.cs +++ b/ProjectPlugins/GethPlugin/GethContainerRecipe.cs @@ -32,8 +32,11 @@ namespace GethPlugin private string CreateArgs(GethStartupConfig config) { - if (config.IsMiner) AddEnvVar("ENABLE_MINER", "1"); - UnlockAccounts(0, 1); + if (config.IsMiner) + { + AddEnvVar("ENABLE_MINER", "1"); + UnlockAccounts(0, 1); + } var httpPort = CreateApiPort(config, tag: HttpPortTag); var discovery = CreateDiscoveryPort(config); @@ -41,16 +44,20 @@ namespace GethPlugin var authRpc = CreateP2pPort(config, tag: AuthRpcPortTag); var wsPort = CreateP2pPort(config, tag: WsPortTag); - var args = $"--http.addr 0.0.0.0 --http.port {httpPort.Number} --port {listen.Number} --discovery.port {discovery.Number} {GetTestNetArgs(config)} {defaultArgs}"; + var args = $"--http.addr 0.0.0.0 --http.port {httpPort.Number} --port {listen.Number} --discovery.port {discovery.Number} {defaultArgs}"; if (config.BootstrapNode != null) { var bootPubKey = config.BootstrapNode.PublicKey; var bootIp = config.BootstrapNode.IpAddress; var bootPort = config.BootstrapNode.Port; - var bootstrapArg = $" --bootnodes enode://{bootPubKey}@{bootIp}:{bootPort} --nat=extip:{bootIp}"; + var bootstrapArg = $" --bootnodes enode://{bootPubKey}@{bootIp}:{bootPort}"; args += bootstrapArg; } + if (config.IsPublicTestNet != null) + { + AddEnvVar("NAT_PUBLIC_IP_AUTO", PublicIpService.Address); + } return args + $" --authrpc.port {authRpc.Number} --ws --ws.addr 0.0.0.0 --ws.port {wsPort.Number}"; } @@ -58,20 +65,13 @@ namespace GethPlugin private void UnlockAccounts(int startIndex, int numberOfAccounts) { if (startIndex < 0) throw new ArgumentException(); - if (numberOfAccounts < 1) throw new ArgumentException(); + if (numberOfAccounts < 0) throw new ArgumentException(); if (startIndex + numberOfAccounts > 1000) throw new ArgumentException("Out of accounts!"); AddEnvVar("UNLOCK_START_INDEX", startIndex.ToString()); AddEnvVar("UNLOCK_NUMBER", numberOfAccounts.ToString()); } - private string GetTestNetArgs(GethStartupConfig config) - { - if (config.IsPublicTestNet == null) return string.Empty; - - return $"--nat=extip:{config.IsPublicTestNet.PublicIp}"; - } - private Port CreateDiscoveryPort(GethStartupConfig config) { if (config.IsPublicTestNet == null) return AddInternalPort(DiscoveryPortTag); diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index 19c3708e..36ae392b 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -19,13 +19,14 @@ namespace GethPlugin void SendTransaction(string contractAddress, TFunction function) where TFunction : FunctionMessage, new(); decimal? GetSyncedBlockNumber(); bool IsContractAvailable(string abi, string contractAddress); + GethBootstrapNode GetBootstrapRecord(); } - public class GethNode : IGethNode + public class DeploymentGethNode : BaseGethNode, IGethNode { private readonly ILog log; - public GethNode(ILog log, GethDeployment startResult) + public DeploymentGethNode(ILog log, GethDeployment startResult) { this.log = log; StartResult = startResult; @@ -34,6 +35,59 @@ namespace GethPlugin public GethDeployment StartResult { get; } public RunningContainer Container => StartResult.Container; + public GethBootstrapNode GetBootstrapRecord() + { + var address = StartResult.Container.GetInternalAddress(GethContainerRecipe.ListenPortTag); + + return new GethBootstrapNode( + publicKey: StartResult.PubKey, + ipAddress: address.Host.Replace("http://", ""), + port: address.Port + ); + } + + protected override NethereumInteraction StartInteraction() + { + var address = StartResult.Container.GetAddress(log, GethContainerRecipe.HttpPortTag); + var account = StartResult.Account; + + var creator = new NethereumInteractionCreator(log, address.Host, address.Port, account.PrivateKey); + return creator.CreateWorkflow(); + } + } + + public class CustomGethNode : BaseGethNode, IGethNode + { + private readonly ILog log; + private readonly string gethHost; + private readonly int gethPort; + private readonly string privateKey; + + public GethDeployment StartResult => throw new NotImplementedException(); + public RunningContainer Container => throw new NotImplementedException(); + + public CustomGethNode(ILog log, string gethHost, int gethPort, string privateKey) + { + this.log = log; + this.gethHost = gethHost; + this.gethPort = gethPort; + this.privateKey = privateKey; + } + + public GethBootstrapNode GetBootstrapRecord() + { + throw new NotImplementedException(); + } + + protected override NethereumInteraction StartInteraction() + { + var creator = new NethereumInteractionCreator(log, gethHost, gethPort, privateKey); + return creator.CreateWorkflow(); + } + } + + public abstract class BaseGethNode + { public Ether GetEthBalance() { return StartInteraction().GetEthBalance().Eth(); @@ -69,15 +123,6 @@ namespace GethPlugin StartInteraction().SendTransaction(contractAddress, function); } - private NethereumInteraction StartInteraction() - { - var address = StartResult.Container.GetAddress(log, GethContainerRecipe.HttpPortTag); - var account = StartResult.Account; - - var creator = new NethereumInteractionCreator(log, address.Host, address.Port, account.PrivateKey); - return creator.CreateWorkflow(); - } - public decimal? GetSyncedBlockNumber() { return StartInteraction().GetSyncedBlockNumber(); @@ -87,5 +132,7 @@ namespace GethPlugin { return StartInteraction().IsContractAvailable(abi, contractAddress); } + + protected abstract NethereumInteraction StartInteraction(); } } diff --git a/ProjectPlugins/GethPlugin/GethStarter.cs b/ProjectPlugins/GethPlugin/GethStarter.cs index 5287d9ce..c5ad801f 100644 --- a/ProjectPlugins/GethPlugin/GethStarter.cs +++ b/ProjectPlugins/GethPlugin/GethStarter.cs @@ -44,7 +44,7 @@ namespace GethPlugin public IGethNode WrapGethContainer(GethDeployment startResult) { startResult = SerializeGate.Gate(startResult); - return new GethNode(tools.GetLog(), startResult); + return new DeploymentGethNode(tools.GetLog(), startResult); } private void Log(string msg) diff --git a/ProjectPlugins/GethPlugin/GethStartupConfig.cs b/ProjectPlugins/GethPlugin/GethStartupConfig.cs index 0776541b..51520ca0 100644 --- a/ProjectPlugins/GethPlugin/GethStartupConfig.cs +++ b/ProjectPlugins/GethPlugin/GethStartupConfig.cs @@ -3,6 +3,7 @@ public interface IGethSetup { IGethSetup IsMiner(); + IGethSetup WithBootstrapNode(IGethNode node); IGethSetup WithBootstrapNode(GethBootstrapNode node); IGethSetup WithName(string name); IGethSetup AsPublicTestNet(GethTestNetConfig gethTestNetConfig); @@ -15,6 +16,11 @@ public string? NameOverride { get; private set; } public GethTestNetConfig? IsPublicTestNet { get; private set; } + public IGethSetup WithBootstrapNode(IGethNode node) + { + return WithBootstrapNode(node.GetBootstrapRecord()); + } + public IGethSetup WithBootstrapNode(GethBootstrapNode node) { BootstrapNode = node; @@ -42,14 +48,12 @@ public class GethTestNetConfig { - public GethTestNetConfig(string publicIp, int discoveryPort, int listenPort) + public GethTestNetConfig(int discoveryPort, int listenPort) { - PublicIp = publicIp; DiscoveryPort = discoveryPort; ListenPort = listenPort; } - public string PublicIp { get; } public int DiscoveryPort { get; } public int ListenPort { get; } } diff --git a/ProjectPlugins/MetricsPlugin/PrometheusStarter.cs b/ProjectPlugins/MetricsPlugin/PrometheusStarter.cs index 0f0bc138..53f784e4 100644 --- a/ProjectPlugins/MetricsPlugin/PrometheusStarter.cs +++ b/ProjectPlugins/MetricsPlugin/PrometheusStarter.cs @@ -18,6 +18,8 @@ namespace MetricsPlugin public RunningContainers CollectMetricsFor(IMetricsScrapeTarget[] targets) { + if (!targets.Any()) throw new ArgumentException(nameof(targets) + " must not be empty."); + Log($"Starting metrics server for {targets.Length} targets..."); var startupConfig = new StartupConfig(); startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(targets))); diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 13e93bf2..3976d46c 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -97,5 +97,22 @@ namespace CodexTests.BasicTests CheckLogForErrors(seller, buyer); } + + [Test] + public void GethBootstrapTest() + { + var boot = Ci.StartGethNode(s => s.WithName("boot").IsMiner()); + var disconnected = Ci.StartGethNode(s => s.WithName("disconnected")); + var follow = Ci.StartGethNode(s => s.WithBootstrapNode(boot).WithName("follow")); + + Thread.Sleep(12000); + + var bootN = boot.GetSyncedBlockNumber(); + var discN = disconnected.GetSyncedBlockNumber(); + var followN = follow.GetSyncedBlockNumber(); + + Assert.That(bootN, Is.EqualTo(followN)); + Assert.That(discN, Is.LessThan(bootN)); + } } } diff --git a/Tools/BiblioTech/BaseCodexCommand.cs b/Tools/BiblioTech/BaseCodexCommand.cs deleted file mode 100644 index 0f6780e5..00000000 --- a/Tools/BiblioTech/BaseCodexCommand.cs +++ /dev/null @@ -1,27 +0,0 @@ -using BiblioTech.Options; -using CodexPlugin; -using Core; - -namespace BiblioTech -{ - public abstract class BaseCodexCommand : BaseDeploymentCommand - { - private readonly CoreInterface ci; - - public BaseCodexCommand(CoreInterface ci) - { - this.ci = ci; - } - - protected override async Task ExecuteDeploymentCommand(CommandContext context, CodexDeployment codexDeployment) - { - var codexContainers = codexDeployment.CodexInstances.Select(c => c.Containers).ToArray(); - - var group = ci.WrapCodexContainers(codexContainers); - - await Execute(context, group); - } - - protected abstract Task Execute(CommandContext context, ICodexNodeGroup codexGroup); - } -} diff --git a/Tools/BiblioTech/BaseCommand.cs b/Tools/BiblioTech/BaseCommand.cs index c68aaeb4..dc23d68a 100644 --- a/Tools/BiblioTech/BaseCommand.cs +++ b/Tools/BiblioTech/BaseCommand.cs @@ -9,13 +9,7 @@ namespace BiblioTech public abstract string Name { get; } public abstract string StartingMessage { get; } public abstract string Description { get; } - public virtual CommandOption[] Options - { - get - { - return Array.Empty(); - } - } + public virtual CommandOption[] Options => Array.Empty(); public async Task SlashCommandHandler(SocketSlashCommand command) { @@ -29,7 +23,15 @@ namespace BiblioTech } catch (Exception ex) { - await command.FollowupAsync("Something failed while trying to do that...", ephemeral: true); + if (IsInAdminChannel(command)) + { + var msg = "Failed with exception: " + ex; + await command.FollowupAsync(msg.Substring(0, Math.Min(1900, msg.Length))); + } + else + { + await command.FollowupAsync("Something failed while trying to do that...", ephemeral: true); + } Console.WriteLine(ex); } } diff --git a/Tools/BiblioTech/BaseDeploymentCommand.cs b/Tools/BiblioTech/BaseDeploymentCommand.cs deleted file mode 100644 index 100c39f1..00000000 --- a/Tools/BiblioTech/BaseDeploymentCommand.cs +++ /dev/null @@ -1,36 +0,0 @@ -using BiblioTech.Options; -using CodexPlugin; - -namespace BiblioTech -{ - public abstract class BaseDeploymentCommand : BaseCommand - { - protected override async Task Invoke(CommandContext context) - { - var proceed = await OnInvoke(context); - if (!proceed) return; - - var deployments = Program.DeploymentFilesMonitor.GetDeployments(); - if (deployments.Length == 0) - { - await context.Followup("No deployments are currently available."); - return; - } - if (deployments.Length > 1) - { - await context.Followup("Multiple deployments are online. I don't know which one to pick!"); - return; - } - - var codexDeployment = deployments.Single(); - await ExecuteDeploymentCommand(context, codexDeployment); - } - - protected abstract Task ExecuteDeploymentCommand(CommandContext context, CodexDeployment codexDeployment); - - protected virtual Task OnInvoke(CommandContext context) - { - return Task.FromResult(true); - } - } -} diff --git a/Tools/BiblioTech/BaseGethCommand.cs b/Tools/BiblioTech/BaseGethCommand.cs index 37841b1c..52bdb456 100644 --- a/Tools/BiblioTech/BaseGethCommand.cs +++ b/Tools/BiblioTech/BaseGethCommand.cs @@ -1,27 +1,89 @@ using BiblioTech.Options; using CodexContractsPlugin; -using CodexPlugin; -using Core; using GethPlugin; +using Logging; namespace BiblioTech { - public abstract class BaseGethCommand : BaseDeploymentCommand + public static class GethInput { - private readonly CoreInterface ci; + private const string GethHostVar = "GETH_HOST"; + private const string GethPortVar = "GETH_HTTP_PORT"; + private const string GethPrivKeyVar = "GETH_PRIVATE_KEY"; + private const string MarketplaceAddressVar = "CODEXCONTRACTS_MARKETPLACEADDRESS"; + private const string TokenAddressVar = "CODEXCONTRACTS_TOKENADDRESS"; + private const string AbiVar = "CODEXCONTRACTS_ABI"; - public BaseGethCommand(CoreInterface ci) + static GethInput() { - this.ci = ci; + var error = new List(); + var gethHost = GetEnvVar(error, GethHostVar); + var gethPort = Convert.ToInt32(GetEnvVar(error, GethPortVar)); + var privateKey = GetEnvVar(error, GethPrivKeyVar); + var marketplaceAddress = GetEnvVar(error, MarketplaceAddressVar); + var tokenAddress = GetEnvVar(error, TokenAddressVar); + var abi = GetEnvVar(error, AbiVar); + + if (error.Any()) + { + LoadError = string.Join(", ", error); + } + else + { + GethHost = gethHost!; + GethPort = gethPort; + PrivateKey = privateKey!; + MarketplaceAddress = marketplaceAddress!; + TokenAddress = tokenAddress!; + ABI = abi!; + } } - protected override async Task ExecuteDeploymentCommand(CommandContext context, CodexDeployment codexDeployment) - { - var gethDeployment = codexDeployment.GethDeployment; - var contractsDeployment = codexDeployment.CodexContractsDeployment; + public static string GethHost { get; } = string.Empty; + public static int GethPort { get; } + public static string PrivateKey { get; } = string.Empty; + public static string MarketplaceAddress { get; } = string.Empty; + public static string TokenAddress { get; } = string.Empty; + public static string ABI { get; } = string.Empty; + public static string LoadError { get; } = string.Empty; - var gethNode = ci.WrapGethDeployment(gethDeployment); - var contracts = ci.WrapCodexContractsDeployment(gethNode, contractsDeployment); + private static string? GetEnvVar(List error, string name) + { + var result = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set."); + return result; + } + } + + public abstract class BaseGethCommand : BaseCommand + { + protected override async Task Invoke(CommandContext context) + { + var log = new ConsoleLog(); + + if (!string.IsNullOrEmpty(GethInput.LoadError)) + { + var msg = "Geth input incorrect: " + GethInput.LoadError; + log.Error(msg); + if (IsInAdminChannel(context.Command)) + { + await context.Followup(msg); + } + else + { + await context.Followup("I'm sorry, there seems to be a configuration error."); + } + return; + } + + var contractsDeployment = new CodexContractsDeployment( + marketplaceAddress: GethInput.MarketplaceAddress, + abi: GethInput.ABI, + tokenAddress: GethInput.TokenAddress + ); + + var gethNode = new CustomGethNode(log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey); + var contracts = new CodexContractsAccess(log, gethNode, contractsDeployment); await Execute(context, gethNode, contracts); } diff --git a/Tools/BiblioTech/Commands/AdminCommand.cs b/Tools/BiblioTech/Commands/AdminCommand.cs index d35f9370..698fb006 100644 --- a/Tools/BiblioTech/Commands/AdminCommand.cs +++ b/Tools/BiblioTech/Commands/AdminCommand.cs @@ -1,7 +1,4 @@ using BiblioTech.Options; -using CodexPlugin; -using Core; -using Newtonsoft.Json; namespace BiblioTech.Commands { @@ -9,17 +6,16 @@ namespace BiblioTech.Commands { private readonly ClearUserAssociationCommand clearCommand = new ClearUserAssociationCommand(); private readonly ReportCommand reportCommand = new ReportCommand(); - private readonly DeployListCommand deployListCommand = new DeployListCommand(); - private readonly DeployUploadCommand deployUploadCommand = new DeployUploadCommand(); - private readonly DeployRemoveCommand deployRemoveCommand = new DeployRemoveCommand(); private readonly WhoIsCommand whoIsCommand = new WhoIsCommand(); - private readonly NetInfoCommand netInfoCommand; - private readonly DebugPeerCommand debugPeerCommand; + private readonly AddSprCommand addSprCommand; + private readonly ClearSprsCommand clearSprsCommand; + private readonly GetSprCommand getSprCommand; - public AdminCommand(CoreInterface ci) + public AdminCommand(SprCommand sprCommand) { - netInfoCommand = new NetInfoCommand(ci); - debugPeerCommand = new DebugPeerCommand(ci); + addSprCommand = new AddSprCommand(sprCommand); + clearSprsCommand = new ClearSprsCommand(sprCommand); + getSprCommand = new GetSprCommand(sprCommand); } public override string Name => "admin"; @@ -30,12 +26,10 @@ namespace BiblioTech.Commands { clearCommand, reportCommand, - deployListCommand, - deployUploadCommand, - deployRemoveCommand, whoIsCommand, - netInfoCommand, - debugPeerCommand + addSprCommand, + clearSprsCommand, + getSprCommand }; protected override async Task Invoke(CommandContext context) @@ -54,12 +48,10 @@ namespace BiblioTech.Commands await clearCommand.CommandHandler(context); await reportCommand.CommandHandler(context); - await deployListCommand.CommandHandler(context); - await deployUploadCommand.CommandHandler(context); - await deployRemoveCommand.CommandHandler(context); await whoIsCommand.CommandHandler(context); - await netInfoCommand.CommandHandler(context); - await debugPeerCommand.CommandHandler(context); + await addSprCommand.CommandHandler(context); + await clearSprsCommand.CommandHandler(context); + await getSprCommand.CommandHandler(context); } public class ClearUserAssociationCommand : SubCommandOption @@ -109,108 +101,8 @@ namespace BiblioTech.Commands return; } - var report = string.Join(Environment.NewLine, Program.UserRepo.GetInteractionReport(user)); - if (report.Length > 1900) - { - var filename = $"user-{user.Username}.log"; - await context.FollowupWithAttachement(filename, report); - } - else - { - await context.Followup(report); - } - } - } - - public class DeployListCommand : SubCommandOption - { - public DeployListCommand() - : base("list", "Lists current deployments.") - { - } - - protected override async Task onSubCommand(CommandContext context) - { - var deployments = Program.DeploymentFilesMonitor.GetDeployments(); - - //todo shows old deployments - - if (!deployments.Any()) - { - await context.Followup("No deployments available."); - return; - } - - var nl = Environment.NewLine; - await context.Followup($"Deployments:{nl}{string.Join(nl, deployments.Select(FormatDeployment))}"); - } - - private string FormatDeployment(CodexDeployment deployment) - { - var m = deployment.Metadata; - return $"'{m.Name}' ({m.StartUtc.ToString("o")})"; - } - } - - public class DeployUploadCommand : SubCommandOption - { - private readonly FileAttachementOption fileOption = new FileAttachementOption( - name: "json", - description: "Codex-deployment json to add.", - isRequired: true); - - public DeployUploadCommand() - : base("add", "Upload a new deployment JSON file.") - { - } - - public override CommandOption[] Options => new[] { fileOption }; - - protected override async Task onSubCommand(CommandContext context) - { - var file = await fileOption.Parse(context); - if (file == null) return; - - var result = await Program.DeploymentFilesMonitor.DownloadDeployment(file); - if (result) - { - await context.Followup("Success!"); - } - else - { - await context.Followup("That didn't work."); - } - } - } - - public class DeployRemoveCommand : SubCommandOption - { - private readonly StringOption stringOption = new StringOption( - name: "name", - description: "Name of deployment to remove.", - isRequired: true); - - public DeployRemoveCommand() - : base("remove", "Removes a deployment file.") - { - } - - public override CommandOption[] Options => new[] { stringOption }; - - protected override async Task onSubCommand(CommandContext context) - { - var str = await stringOption.Parse(context); - if (string.IsNullOrEmpty(str)) return; - - var result = Program.DeploymentFilesMonitor.DeleteDeployment(str); - if (result) - { - await context.Followup("Success!"); - } - else - { - await context.Followup("That didn't work."); - } + var report = Program.UserRepo.GetInteractionReport(user); + await context.Followup(report); } } @@ -247,118 +139,76 @@ namespace BiblioTech.Commands } } - public abstract class AdminDeploymentCommand : SubCommandOption + public class AddSprCommand : SubCommandOption { - private readonly CoreInterface ci; + private readonly SprCommand sprCommand; + private readonly StringOption stringOption = new StringOption("spr", "Codex SPR", true); - public AdminDeploymentCommand(CoreInterface ci, string name, string description) - : base(name, description) + public AddSprCommand(SprCommand sprCommand) + : base(name: "addspr", + description: "Adds a Codex SPR, to be given to users with '/boot'.") { - this.ci = ci; + this.sprCommand = sprCommand; } - protected async Task OnDeployment(CommandContext context, Func action) - { - var deployment = Program.DeploymentFilesMonitor.GetDeployments().SingleOrDefault(); - if (deployment == null) - { - await context.Followup("No deployment found."); - return; - } + public override CommandOption[] Options => new[] { stringOption }; - try + protected override async Task onSubCommand(CommandContext context) + { + var spr = await stringOption.Parse(context); + + if (!string.IsNullOrEmpty(spr) ) { - var group = ci.WrapCodexContainers(deployment.CodexInstances.Select(i => i.Containers).ToArray()); - await action(group, deployment.Metadata.Name); + sprCommand.Add(spr); + await context.Followup("A-OK!"); } - catch (Exception ex) + else { - await context.Followup("Failed to wrap nodes with exception: " + ex); + await context.Followup("SPR is null or empty."); } } } - public class NetInfoCommand : AdminDeploymentCommand + public class ClearSprsCommand : SubCommandOption { - public NetInfoCommand(CoreInterface ci) - : base(ci, name: "netinfo", - description: "Fetches info endpoints of codex nodes.") + private readonly SprCommand sprCommand; + private readonly StringOption stringOption = new StringOption("areyousure", "set to 'true' if you are.", true); + + public ClearSprsCommand(SprCommand sprCommand) + : base(name: "clearsprs", + description: "Clears all Codex SPRs in the bot. Users won't be able to use '/boot' till new ones are added.") { + this.sprCommand = sprCommand; + } + + public override CommandOption[] Options => new[] { stringOption }; + + protected override async Task onSubCommand(CommandContext context) + { + var areyousure = await stringOption.Parse(context); + + if (areyousure != "true") return; + + sprCommand.Clear(); + await context.Followup("Cleared all SPRs."); + } + } + + public class GetSprCommand: SubCommandOption + { + private readonly SprCommand sprCommand; + + public GetSprCommand(SprCommand sprCommand) + : base(name: "getsprs", + description: "Shows all Codex SPRs in the bot.") + { + this.sprCommand = sprCommand; } protected override async Task onSubCommand(CommandContext context) { - await OnDeployment(context, async (group, name) => - { - var nl = Environment.NewLine; - var content = new List - { - $"{DateTime.UtcNow.ToString("o")} - {group.Count()} Codex nodes." - }; - - foreach (var node in group) - { - try - { - var info = node.GetDebugInfo(); - var json = JsonConvert.SerializeObject(info, Formatting.Indented); - var jsonInsert = $"{nl}```{nl}{json}{nl}```{nl}"; - content.Add($"Node '{node.GetName()}' responded with {jsonInsert}"); - } - catch (Exception ex) - { - content.Add($"Node '{node.GetName()}' failed to respond with exception: " + ex); - } - } - - var filename = $"netinfo-{NoWhitespaces(name)}.log"; - await context.FollowupWithAttachement(filename, string.Join(nl, content.ToArray())); - }); + await context.Followup("SPRs: " + string.Join(", ", sprCommand.Get().Select(s => $"'{s}'"))); } } - - public class DebugPeerCommand : AdminDeploymentCommand - { - private readonly StringOption peerIdOption = new StringOption("peerid", "id of peer to try and reach.", true); - - public DebugPeerCommand(CoreInterface ci) - : base(ci, name: "debugpeer", - description: "Calls debug/peer on each codex node.") - { - } - - public override CommandOption[] Options => new[] { peerIdOption }; - - protected override async Task onSubCommand(CommandContext context) - { - var peerId = await peerIdOption.Parse(context); - if (string.IsNullOrEmpty(peerId)) return; - - await OnDeployment(context, async (group, name) => - { - await context.Followup($"Calling debug/peer for '{peerId}' on {group.Count()} Codex nodes."); - foreach (var node in group) - { - try - { - var info = node.GetDebugPeer(peerId); - var nl = Environment.NewLine; - var json = JsonConvert.SerializeObject(info, Formatting.Indented); - var jsonInsert = $"{nl}```{nl}{json}{nl}```{nl}"; - await context.Followup($"Node '{node.GetName()}' responded with {jsonInsert}"); - } - catch (Exception ex) - { - await context.Followup($"Node '{node.GetName()}' failed to respond with exception: " + ex); - } - } - }); - } - } - - private static string NoWhitespaces(string s) - { - return s.Replace(" ", "-"); - } } } diff --git a/Tools/BiblioTech/Commands/GetBalanceCommand.cs b/Tools/BiblioTech/Commands/GetBalanceCommand.cs index 05337abf..60118f5b 100644 --- a/Tools/BiblioTech/Commands/GetBalanceCommand.cs +++ b/Tools/BiblioTech/Commands/GetBalanceCommand.cs @@ -1,5 +1,6 @@ using BiblioTech.Options; using CodexContractsPlugin; +using CodexPlugin; using Core; using GethPlugin; @@ -12,8 +13,7 @@ namespace BiblioTech.Commands description: "If set, get balance for another user. (Optional, admin-only)", isRequired: false); - public GetBalanceCommand(CoreInterface ci, UserAssociateCommand userAssociateCommand) - : base(ci) + public GetBalanceCommand(UserAssociateCommand userAssociateCommand) { this.userAssociateCommand = userAssociateCommand; } diff --git a/Tools/BiblioTech/Commands/MintCommand.cs b/Tools/BiblioTech/Commands/MintCommand.cs index ed87a1e5..cc50a2ad 100644 --- a/Tools/BiblioTech/Commands/MintCommand.cs +++ b/Tools/BiblioTech/Commands/MintCommand.cs @@ -1,6 +1,5 @@ using BiblioTech.Options; using CodexContractsPlugin; -using Core; using GethPlugin; namespace BiblioTech.Commands @@ -14,8 +13,7 @@ namespace BiblioTech.Commands isRequired: false); private readonly UserAssociateCommand userAssociateCommand; - public MintCommand(CoreInterface ci, UserAssociateCommand userAssociateCommand) - : base(ci) + public MintCommand(UserAssociateCommand userAssociateCommand) { this.userAssociateCommand = userAssociateCommand; } diff --git a/Tools/BiblioTech/Commands/SprCommand.cs b/Tools/BiblioTech/Commands/SprCommand.cs index 6d52b3d5..4b3235de 100644 --- a/Tools/BiblioTech/Commands/SprCommand.cs +++ b/Tools/BiblioTech/Commands/SprCommand.cs @@ -1,61 +1,48 @@ using BiblioTech.Options; -using CodexPlugin; -using Core; namespace BiblioTech.Commands { - public class SprCommand : BaseCodexCommand + public class SprCommand : BaseCommand { private readonly Random random = new Random(); - private readonly List sprCache = new List(); - private DateTime lastUpdate = DateTime.MinValue; - - public SprCommand(CoreInterface ci) : base(ci) - { - } + private readonly List knownSprs = new List(); public override string Name => "boot"; public override string StartingMessage => RandomBusyMessage.Get(); public override string Description => "Gets an SPR. (Signed peer record, used for bootstrapping.)"; - protected override async Task OnInvoke(CommandContext context) + protected override async Task Invoke(CommandContext context) { - if (ShouldUpdate()) - { - return true; - } - await ReplyWithRandomSpr(context); - return false; } - protected override async Task Execute(CommandContext context, ICodexNodeGroup codexGroup) + public void Add(string spr) { - lastUpdate = DateTime.UtcNow; - sprCache.Clear(); + if (knownSprs.Contains(spr)) return; + knownSprs.Add(spr); + } - var infos = codexGroup.Select(c => c.GetDebugInfo()).ToArray(); - sprCache.AddRange(infos.Select(i => i.spr)); + public void Clear() + { + knownSprs.Clear(); + } - await ReplyWithRandomSpr(context); + public string[] Get() + { + return knownSprs.ToArray(); } private async Task ReplyWithRandomSpr(CommandContext context) { - if (!sprCache.Any()) + if (!knownSprs.Any()) { await context.Followup("I'm sorry, no SPRs are available... :c"); return; } - var i = random.Next(0, sprCache.Count); - var spr = sprCache[i]; - await context.Followup($"Your SPR: '{spr}'"); - } - - private bool ShouldUpdate() - { - return (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10); + var i = random.Next(0, knownSprs.Count); + var spr = knownSprs[i]; + await context.Followup($"Your SPR: `{spr}`"); } } } diff --git a/Tools/BiblioTech/Commands/UserAssociateCommand.cs b/Tools/BiblioTech/Commands/UserAssociateCommand.cs index 2ede721d..c23718ce 100644 --- a/Tools/BiblioTech/Commands/UserAssociateCommand.cs +++ b/Tools/BiblioTech/Commands/UserAssociateCommand.cs @@ -27,8 +27,6 @@ namespace BiblioTech.Commands return; } - // private commands - var result = Program.UserRepo.AssociateUserWithAddress(user, data); if (result) { diff --git a/Tools/BiblioTech/Configuration.cs b/Tools/BiblioTech/Configuration.cs index 10e134ea..eb588705 100644 --- a/Tools/BiblioTech/Configuration.cs +++ b/Tools/BiblioTech/Configuration.cs @@ -19,6 +19,12 @@ namespace BiblioTech [Uniform("admin-channel-name", "ac", "ADMINCHANNELNAME", true, "Name of the Discord server channel where admin commands are allowed.")] public string AdminChannelName { get; set; } = "admin-channel"; + [Uniform("kube-config", "kc", "KUBECONFIG", true, "Path to Kubeconfig file. Use a Kubeconfig with read-only access.")] + public string KubeConfigFile { get; set; } = "null"; + + [Uniform("kube-namespace", "kn", "KUBENAMESPACE", true, "Kubernetes namespace.")] + public string KubeNamespace { get; set; } = string.Empty; + public string EndpointsPath { get diff --git a/Tools/BiblioTech/DeploymentsFilesMonitor.cs b/Tools/BiblioTech/DeploymentsFilesMonitor.cs deleted file mode 100644 index 9278574a..00000000 --- a/Tools/BiblioTech/DeploymentsFilesMonitor.cs +++ /dev/null @@ -1,89 +0,0 @@ -using CodexPlugin; -using Discord; -using Newtonsoft.Json; - -namespace BiblioTech -{ - public class DeploymentsFilesMonitor - { - private readonly List deployments = new List(); - - public void Initialize() - { - LoadDeployments(); - } - - public CodexDeployment[] GetDeployments() - { - return deployments.ToArray(); - } - - public async Task DownloadDeployment(IAttachment file) - { - using var http = new HttpClient(); - var response = await http.GetAsync(file.Url); - var str = await response.Content.ReadAsStringAsync(); - if (string.IsNullOrEmpty(str)) return false; - - try - { - var deploy = JsonConvert.DeserializeObject(str); - if (deploy != null) - { - var targetFile = Path.Combine(Program.Config.EndpointsPath, Guid.NewGuid().ToString().ToLowerInvariant() + ".json"); - File.WriteAllText(targetFile, str); - deployments.Add(deploy); - return true; - } - } - catch { } - return false; - } - - public bool DeleteDeployment(string deploymentName) - { - var path = Program.Config.EndpointsPath; - if (!Directory.Exists(path)) return false; - var files = Directory.GetFiles(path); - - foreach (var file in files) - { - var deploy = ProcessFile(file); - if (deploy != null && deploy.Metadata.Name == deploymentName) - { - File.Delete(file); - deployments.Remove(deploy); - return true; - } - } - return false; - } - - private void LoadDeployments() - { - var path = Program.Config.EndpointsPath; - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - File.WriteAllText(Path.Combine(path, "readme.txt"), "Place codex-deployment.json here."); - return; - } - - var files = Directory.GetFiles(path); - deployments.AddRange(files.Select(ProcessFile).Where(d => d != null).Cast()); - } - - private CodexDeployment? ProcessFile(string filename) - { - try - { - var lines = string.Join(" ", File.ReadAllLines(filename)); - return JsonConvert.DeserializeObject(lines); - } - catch - { - return null; - } - } - } -} diff --git a/Tools/BiblioTech/Options/CommandContext.cs b/Tools/BiblioTech/Options/CommandContext.cs index 01567ce2..42a065ae 100644 --- a/Tools/BiblioTech/Options/CommandContext.cs +++ b/Tools/BiblioTech/Options/CommandContext.cs @@ -4,30 +4,99 @@ namespace BiblioTech.Options { public class CommandContext { + private const string AttachmentFolder = "attachments"; + public CommandContext(SocketSlashCommand command, IReadOnlyCollection options) { Command = command; Options = options; + + var attachmentPath = Path.Combine(Program.Config.DataPath, AttachmentFolder); + if (Directory.Exists(attachmentPath)) + { + Directory.CreateDirectory(attachmentPath); + } } public SocketSlashCommand Command { get; } public IReadOnlyCollection Options { get; } - public async Task Followup(string message) + public async Task Followup(string line) { - await Command.ModifyOriginalResponseAsync(m => - { - m.Content = message; - }); + var array = line.Split(Environment.NewLine); + await Followup(array); } - public async Task FollowupWithAttachement(string filename, string content) + public async Task Followup(string[] lines) { - using var fileStream = new MemoryStream(); - using var streamWriter = new StreamWriter(fileStream); - await streamWriter.WriteAsync(content); + var chunker = new LineChunker(lines); + var chunks = chunker.GetChunks(); + if (!chunks.Any()) return; - await Command.FollowupWithFileAsync(fileStream, filename); + // First chunk is a modification of the original message. + // Everything after that, we must create a new message. + var first = chunks.First(); + chunks.RemoveAt(0); + + await Command.ModifyOriginalResponseAsync(m => + { + m.Content = FormatChunk(first); + }); + + foreach (var remaining in chunks) + { + await Command.FollowupAsync(FormatChunk(remaining)); + } + } + + private string FormatChunk(string[] chunk) + { + return string.Join(Environment.NewLine, chunk); + } + } + + public class LineChunker + { + private readonly List input; + private readonly int maxCharacters; + + public LineChunker(string[] input, int maxCharacters = 1950) + { + this.input = input.ToList(); + this.maxCharacters = maxCharacters; + } + + public List GetChunks() + { + var result = new List(); + while (input.Any()) + { + result.Add(GetChunk()); + } + + return result; + } + + private string[] GetChunk() + { + var totalLength = 0; + var result = new List(); + + while (input.Any()) + { + var nextLine = input[0]; + var nextLength = totalLength + nextLine.Length; + if (nextLength > maxCharacters) + { + return result.ToArray(); + } + + input.RemoveAt(0); + result.Add(nextLine); + totalLength += nextLine.Length; + } + + return result.ToArray(); } } } diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index ecd91475..f759d97c 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -1,9 +1,7 @@ using ArgsUniform; using BiblioTech.Commands; -using Core; using Discord; using Discord.WebSocket; -using Logging; namespace BiblioTech { @@ -12,7 +10,6 @@ namespace BiblioTech private DiscordSocketClient client = null!; public static Configuration Config { get; private set; } = null!; - public static DeploymentsFilesMonitor DeploymentFilesMonitor { get; } = new DeploymentsFilesMonitor(); public static UserRepo UserRepo { get; } = new UserRepo(); public static AdminChecker AdminChecker { get; } = new AdminChecker(); @@ -21,8 +18,6 @@ namespace BiblioTech var uniformArgs = new ArgsUniform(PrintHelp, args); Config = uniformArgs.Parse(); - DeploymentFilesMonitor.Initialize(); - EnsurePath(Config.DataPath); EnsurePath(Config.UserDataPath); EnsurePath(Config.EndpointsPath); @@ -36,25 +31,14 @@ namespace BiblioTech client = new DiscordSocketClient(); client.Log += Log; - ProjectPlugin.Load(); - ProjectPlugin.Load(); - ProjectPlugin.Load(); - - var entryPoint = new EntryPoint(new ConsoleLog(), new KubernetesWorkflow.Configuration( - kubeConfigFile: null, - operationTimeout: TimeSpan.FromMinutes(5), - retryDelay: TimeSpan.FromSeconds(10), - kubernetesNamespace: "not-applicable"), "datafiles"); - - var ci = entryPoint.CreateInterface(); - var associateCommand = new UserAssociateCommand(); + var sprCommand = new SprCommand(); var handler = new CommandHandler(client, - new GetBalanceCommand(ci, associateCommand), - new MintCommand(ci, associateCommand), - new SprCommand(ci), + new GetBalanceCommand(associateCommand), + new MintCommand(associateCommand), + sprCommand, associateCommand, - new AdminCommand(ci) + new AdminCommand(sprCommand) ); await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); diff --git a/Tools/BiblioTech/UserRepo.cs b/Tools/BiblioTech/UserRepo.cs index 1a78ac48..237f0a51 100644 --- a/Tools/BiblioTech/UserRepo.cs +++ b/Tools/BiblioTech/UserRepo.cs @@ -2,7 +2,6 @@ using Discord; using GethPlugin; using Newtonsoft.Json; -using Utils; namespace BiblioTech { @@ -76,17 +75,17 @@ namespace BiblioTech return result.ToArray(); } - public string GetUserReport(IUser user) + public string[] GetUserReport(IUser user) { var userData = GetUserData(user); - if (userData == null) return "User has not joined the test net."; + if (userData == null) return new[] { "User has not joined the test net." }; return userData.CreateOverview(); } - public string GetUserReport(EthAddress ethAddress) + public string[] GetUserReport(EthAddress ethAddress) { var userData = GetUserDataForAddress(ethAddress); - if (userData == null) return "No user is using this eth address."; + if (userData == null) return new[] { "No user is using this eth address." }; return userData.CreateOverview(); } @@ -196,14 +195,15 @@ namespace BiblioTech public List AssociateEvents { get; } public List MintEvents { get; } - public string CreateOverview() + public string[] CreateOverview() { - var nl = Environment.NewLine; - return - $"name: '{Name}' - id:{DiscordId}{nl}" + - $"joined: {CreatedUtc.ToString("o")}{nl}" + - $"current address: {CurrentAddress}{nl}" + - $"{AssociateEvents.Count + MintEvents.Count} total bot events."; + return new[] + { + $"name: '{Name}' - id:{DiscordId}", + $"joined: {CreatedUtc.ToString("o")}", + $"current address: {CurrentAddress}", + $"{AssociateEvents.Count + MintEvents.Count} total bot events." + }; } } diff --git a/Tools/CodexNetDeployer/CodexNodeStarter.cs b/Tools/CodexNetDeployer/CodexNodeStarter.cs index e18720e6..7b540013 100644 --- a/Tools/CodexNetDeployer/CodexNodeStarter.cs +++ b/Tools/CodexNetDeployer/CodexNodeStarter.cs @@ -105,7 +105,6 @@ namespace CodexNetDeployer return new CodexTestNetConfig { - PublicNatIP = config.PublicIP, PublicDiscoveryPort = Convert.ToInt32(discPort), PublicListenPort = Convert.ToInt32(listenPort) }; diff --git a/Tools/CodexNetDeployer/Configuration.cs b/Tools/CodexNetDeployer/Configuration.cs index 8279f969..1d93402f 100644 --- a/Tools/CodexNetDeployer/Configuration.cs +++ b/Tools/CodexNetDeployer/Configuration.cs @@ -86,18 +86,12 @@ namespace CodexNetDeployer [Uniform("public-testnet", "ptn", "PUBLICTESTNET", false, "If true, deployment is created for public exposure. Default is false.")] public bool IsPublicTestNet { get; set; } = false; - [Uniform("public-ip", "pip", "PUBLICIP", false, "Required if public-testnet is true. Public IP address used by nodes for network annoucements.")] - public string PublicIP { get; set; } = string.Empty; - [Uniform("public-discports", "pdps", "PUBLICDISCPORTS", false, "Required if public-testnet is true. Comma-separated port numbers used for discovery. Number must match number of nodes.")] public string PublicDiscPorts { get; set; } = string.Empty; [Uniform("public-listenports", "plps", "PUBLICLISTENPORTS", false, "Required if public-testnet is true. Comma-separated port numbers used for listening. Number must match number of nodes.")] public string PublicListenPorts { get; set; } = string.Empty; - [Uniform("public-gethip", "pgdp", "PUBLICGETHIP", false, "Required if public-testnet is true. Geth's public IP address.")] - public string PublicGethIP { get; set; } = string.Empty; - [Uniform("public-gethdiscport", "pgdp", "PUBLICGETHDISCPORT", false, "Required if public-testnet is true. Single port number used for Geth's public discovery port.")] public int PublicGethDiscPort { get; set; } @@ -150,9 +144,11 @@ namespace CodexNetDeployer if (IsPublicTestNet) { - if (string.IsNullOrEmpty(PublicIP)) errors.Add("Public IP required when deploying public testnet."); - if (PublicDiscPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public discovery-ports provided does not match number of codex nodes."); - if (PublicListenPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public listen-ports provided does not match number of codex nodes."); + if (NumberOfCodexNodes > 0) + { + if (PublicDiscPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public discovery-ports provided does not match number of codex nodes."); + if (PublicListenPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public listen-ports provided does not match number of codex nodes."); + } if (PublicGethDiscPort == 0) errors.Add("Geth public discovery port is not set."); if (PublicGethListenPort == 0) errors.Add("Geth public listen port is not set."); } diff --git a/Tools/CodexNetDeployer/Deployer.cs b/Tools/CodexNetDeployer/Deployer.cs index a0263b57..14617845 100644 --- a/Tools/CodexNetDeployer/Deployer.cs +++ b/Tools/CodexNetDeployer/Deployer.cs @@ -82,7 +82,7 @@ namespace CodexNetDeployer var codexInstances = CreateCodexInstances(startResults); - var discordBotContainer = DeployDiscordBot(ci); + var discordBotContainer = DeployDiscordBot(ci, gethDeployment, contractsDeployment); return new CodexDeployment(codexInstances, gethDeployment, contractsDeployment, metricsService, discordBotContainer, CreateMetadata(startUtc)); } @@ -113,7 +113,6 @@ namespace CodexNetDeployer if (config.IsPublicTestNet) { s.AsPublicTestNet(new GethTestNetConfig( - publicIp: config.PublicGethIP, discoveryPort: config.PublicGethDiscPort, listenPort: config.PublicGethListenPort )); @@ -121,17 +120,29 @@ namespace CodexNetDeployer }); } - private RunningContainers? DeployDiscordBot(CoreInterface ci) + private RunningContainers? DeployDiscordBot(CoreInterface ci, GethDeployment gethDeployment, CodexContractsDeployment contractsDeployment) { if (!config.DeployDiscordBot) return null; Log("Deploying Discord bot..."); + var addr = gethDeployment.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag); + var info = new DiscordBotGethInfo( + host: addr.Host, + port: addr.Port, + privKey: gethDeployment.Account.PrivateKey, + marketplaceAddress: contractsDeployment.MarketplaceAddress, + tokenAddress: contractsDeployment.TokenAddress, + abi: contractsDeployment.Abi + ); + var rc = ci.DeployCodexDiscordBot(new DiscordBotStartupConfig( name: "discordbot-" + config.DeploymentName, token: config.DiscordBotToken, serverName: config.DiscordBotServerName, adminRoleName: config.DiscordBotAdminRoleName, - adminChannelName: config.DiscordBotAdminChannelName) + adminChannelName: config.DiscordBotAdminChannelName, + kubeNamespace: config.KubeNamespace, + gethInfo: info) { DataPath = config.DiscordBotDataPath }); @@ -142,7 +153,7 @@ namespace CodexNetDeployer private RunningContainers? StartMetricsService(CoreInterface ci, List startResults) { - if (!config.MetricsScraper) return null; + if (!config.MetricsScraper || !startResults.Any()) return null; Log("Starting metrics service..."); @@ -176,7 +187,7 @@ namespace CodexNetDeployer private void CheckPeerConnectivity(List codexContainers) { - if (!config.CheckPeerConnection) return; + if (!config.CheckPeerConnection || !codexContainers.Any()) return; Log("Starting peer connectivity check for deployed nodes..."); peerConnectivityChecker.CheckConnectivity(codexContainers); diff --git a/Tools/CodexNetDeployer/deploy-public-testnet.sh b/Tools/CodexNetDeployer/deploy-public-testnet.sh index 3b4178a8..c25d010a 100644 --- a/Tools/CodexNetDeployer/deploy-public-testnet.sh +++ b/Tools/CodexNetDeployer/deploy-public-testnet.sh @@ -16,12 +16,10 @@ dotnet run \ --check-connect=0 \ \ --public-testnet=1 \ - --public-ip=1.2.3.4 \ - --public-discports=20010,20020,20030 \ - --public-listenports=20011,20021,20031 \ - --public-gethip=1.2.3.5 \ - --public-gethdiscport=20040 \ - --public-gethlistenport=20041 \ + --public-discports=30010,30020,30030 \ + --public-listenports=30011,30021,30031 \ + --public-gethdiscport=30040 \ + --public-gethlistenport=30041 \ \ --discord-bot=1 \ --dbot-token=tokenhere \