Merge branch 'feature/public-testnet-deploying'

This commit is contained in:
benbierens 2023-12-14 15:59:22 +01:00
commit b143136590
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
36 changed files with 518 additions and 544 deletions

View File

@ -84,6 +84,31 @@ namespace KubernetesWorkflow
return result; return result;
} }
public int[] GetUsedExternalPorts()
{
return client.Run(c =>
{
var result = new List<int>();
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) public void DeleteAllNamespacesStartingWith(string prefix)
{ {
log.Debug(); log.Debug();
@ -704,17 +729,17 @@ namespace KubernetesWorkflow
private RunningService? CreateInternalService(ContainerRecipe[] recipes) 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) 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<ContainerRecipe, Port[]> portSelector, string serviceType, string namePostfix) private RunningService? CreateService(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector, string serviceType, string namePostfix, bool isNodePort)
{ {
var ports = CreateServicePorts(recipes, portSelector); var ports = CreateServicePorts(recipes, portSelector, isNodePort);
if (!ports.Any()) return null; if (!ports.Any()) return null;
var serviceSpec = new V1Service var serviceSpec = new V1Service
@ -788,7 +813,7 @@ namespace KubernetesWorkflow
}; };
} }
private List<V1ServicePort> CreateServicePorts(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector) private List<V1ServicePort> CreateServicePorts(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector, bool isNodePort)
{ {
var result = new List<V1ServicePort>(); var result = new List<V1ServicePort>();
foreach (var recipe in recipes) foreach (var recipe in recipes)
@ -796,29 +821,33 @@ namespace KubernetesWorkflow
var ports = portSelector(recipe); var ports = portSelector(recipe);
foreach (var port in ports) foreach (var port in ports)
{ {
result.AddRange(CreateServicePorts(recipe, port)); result.AddRange(CreateServicePorts(recipe, port, isNodePort));
} }
} }
return result; return result;
} }
private List<V1ServicePort> CreateServicePorts(ContainerRecipe recipe, Port recipePort) private List<V1ServicePort> CreateServicePorts(ContainerRecipe recipe, Port recipePort, bool isNodePort)
{ {
var result = new List<V1ServicePort>(); var result = new List<V1ServicePort>();
if (recipePort.IsTcp()) CreateServicePort(result, recipe, recipePort, "TCP"); if (recipePort.IsTcp()) CreateServicePort(result, recipe, recipePort, "TCP", isNodePort);
if (recipePort.IsUdp()) CreateServicePort(result, recipe, recipePort, "UDP"); if (recipePort.IsUdp()) CreateServicePort(result, recipe, recipePort, "UDP", isNodePort);
return result; return result;
} }
private void CreateServicePort(List<V1ServicePort> result, ContainerRecipe recipe, Port port, string protocol) private void CreateServicePort(List<V1ServicePort> result, ContainerRecipe recipe, Port port, string protocol, bool isNodePort)
{ {
result.Add(new V1ServicePort var p = new V1ServicePort
{ {
Name = GetNameForPort(recipe, port), Name = GetNameForPort(recipe, port),
Protocol = protocol, Protocol = protocol,
Port = port.Number, Port = port.Number,
TargetPort = GetNameForPort(recipe, port) TargetPort = GetNameForPort(recipe, port)
}); };
if (isNodePort) p.NodePort = port.Number;
result.Add(p);
} }
#endregion #endregion

View File

@ -56,17 +56,17 @@ namespace KubernetesWorkflow.Recipe
protected Port AddExposedPort(string tag, PortProtocol protocol = PortProtocol.TCP) 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) 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) protected Port AddInternalPort(string tag = "", PortProtocol protocol = PortProtocol.TCP)
{ {
var p = factory.CreatePort(tag, protocol); var p = factory.CreateInternalPort(tag, protocol);
internalPorts.Add(p); internalPorts.Add(p);
return p; return p;
} }

View File

@ -5,16 +5,36 @@ namespace KubernetesWorkflow.Recipe
{ {
public class RecipeComponentFactory 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<int>();
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); 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) public EnvVar CreateEnvVar(string name, int value)

View File

@ -54,12 +54,19 @@ namespace KubernetesWorkflow
{ {
return K8s(controller => return K8s(controller =>
{ {
componentFactory.Update(controller);
var recipes = CreateRecipes(numberOfContainers, recipeFactory, startupConfig); var recipes = CreateRecipes(numberOfContainers, recipeFactory, startupConfig);
var startResult = controller.BringOnline(recipes, location); var startResult = controller.BringOnline(recipes, location);
var containers = CreateContainers(startResult, recipes, startupConfig); var containers = CreateContainers(startResult, recipes, startupConfig);
var rc = new RunningContainers(startupConfig, startResult, containers); var rc = new RunningContainers(startupConfig, startResult, containers);
cluster.Configuration.Hooks.OnContainersStarted(rc); cluster.Configuration.Hooks.OnContainersStarted(rc);
if (startResult.ExternalService != null)
{
componentFactory.Update(controller);
}
return rc; return rc;
}); });
} }
@ -222,7 +229,7 @@ namespace KubernetesWorkflow
} }
catch (k8s.Autorest.HttpOperationException ex) catch (k8s.Autorest.HttpOperationException ex)
{ {
log.Error(JsonConvert.SerializeObject(ex)); log.Error(JsonConvert.SerializeObject(ex.Response));
throw; throw;
} }
} }
@ -238,7 +245,7 @@ namespace KubernetesWorkflow
} }
catch (k8s.Autorest.HttpOperationException ex) catch (k8s.Autorest.HttpOperationException ex)
{ {
log.Error(JsonConvert.SerializeObject(ex)); log.Error(JsonConvert.SerializeObject(ex.Response));
throw; throw;
} }
} }

View File

@ -55,7 +55,9 @@ namespace Logging
public virtual void Error(string message) 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) public virtual void AddStringReplace(string from, string to)

View File

@ -16,6 +16,7 @@
public override void Error(string message) public override void Error(string message)
{ {
Console.WriteLine("Error: " + message);
base.Error(message); base.Error(message);
} }

View File

@ -19,12 +19,24 @@ namespace CodexDiscordBotPlugin
AddEnvVar("SERVERNAME", config.ServerName); AddEnvVar("SERVERNAME", config.ServerName);
AddEnvVar("ADMINROLE", config.AdminRoleName); AddEnvVar("ADMINROLE", config.AdminRoleName);
AddEnvVar("ADMINCHANNELNAME", config.AdminChannelName); 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)) if (!string.IsNullOrEmpty(config.DataPath))
{ {
AddEnvVar("DATAPATH", config.DataPath); AddEnvVar("DATAPATH", config.DataPath);
AddVolume(config.DataPath, 1.GB()); AddVolume(config.DataPath, 1.GB());
} }
AddVolume(name: "kubeconfig", mountPath: "/opt/kubeconfig.yaml", subPath: "kubeconfig.yaml", secret: "discordbot-sa-kubeconfig");
} }
} }
} }

View File

@ -2,13 +2,15 @@
{ {
public class DiscordBotStartupConfig 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; Name = name;
Token = token; Token = token;
ServerName = serverName; ServerName = serverName;
AdminRoleName = adminRoleName; AdminRoleName = adminRoleName;
AdminChannelName = adminChannelName; AdminChannelName = adminChannelName;
KubeNamespace = kubeNamespace;
GethInfo = gethInfo;
} }
public string Name { get; } public string Name { get; }
@ -16,6 +18,28 @@
public string ServerName { get; } public string ServerName { get; }
public string AdminRoleName { get; } public string AdminRoleName { get; }
public string AdminChannelName { get; } public string AdminChannelName { get; }
public string KubeNamespace { get; }
public DiscordBotGethInfo GethInfo { get; }
public string? DataPath { get; set; } 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; }
}
} }

View File

@ -5,9 +5,10 @@ namespace CodexPlugin
public class CodexDebugResponse public class CodexDebugResponse
{ {
public string id { get; set; } = string.Empty; public string id { get; set; } = string.Empty;
public string[] addrs { get; set; } = new string[0]; public string[] addrs { get; set; } = Array.Empty<string>();
public string repo { get; set; } = string.Empty; public string repo { get; set; } = string.Empty;
public string spr { get; set; } = string.Empty; public string spr { get; set; } = string.Empty;
public string[] announceAddresses { get; set; } = Array.Empty<string>();
public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>(); public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>();
public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>(); public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>();
public CodexDebugVersionResponse codex { get; set; } = new(); public CodexDebugVersionResponse codex { get; set; } = new();

View File

@ -48,8 +48,9 @@ namespace CodexPlugin
if (config.PublicTestNet != null) 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_IP_AUTO", "false");
AddEnvVar("NAT_PUBLIC_IP_AUTO", PublicIpService.Address);
} }
else else
{ {

View File

@ -133,7 +133,7 @@ namespace CodexPlugin
private IEnumerable<string> DescribeArgs() private IEnumerable<string> 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()}"; yield return $"LogLevel={LogLevelWithTopics()}";
if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}"; if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}";
if (StorageQuota != null) yield return $"StorageQuota={StorageQuota}"; if (StorageQuota != null) yield return $"StorageQuota={StorageQuota}";

View File

@ -65,7 +65,6 @@ namespace CodexPlugin
public class CodexTestNetConfig public class CodexTestNetConfig
{ {
public string PublicNatIP { get; set; } = string.Empty;
public int PublicDiscoveryPort { get; set; } public int PublicDiscoveryPort { get; set; }
public int PublicListenPort { get; set; } public int PublicListenPort { get; set; }
} }

View File

@ -32,8 +32,11 @@ namespace GethPlugin
private string CreateArgs(GethStartupConfig config) private string CreateArgs(GethStartupConfig config)
{ {
if (config.IsMiner) AddEnvVar("ENABLE_MINER", "1"); if (config.IsMiner)
UnlockAccounts(0, 1); {
AddEnvVar("ENABLE_MINER", "1");
UnlockAccounts(0, 1);
}
var httpPort = CreateApiPort(config, tag: HttpPortTag); var httpPort = CreateApiPort(config, tag: HttpPortTag);
var discovery = CreateDiscoveryPort(config); var discovery = CreateDiscoveryPort(config);
@ -41,16 +44,20 @@ namespace GethPlugin
var authRpc = CreateP2pPort(config, tag: AuthRpcPortTag); var authRpc = CreateP2pPort(config, tag: AuthRpcPortTag);
var wsPort = CreateP2pPort(config, tag: WsPortTag); 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) if (config.BootstrapNode != null)
{ {
var bootPubKey = config.BootstrapNode.PublicKey; var bootPubKey = config.BootstrapNode.PublicKey;
var bootIp = config.BootstrapNode.IpAddress; var bootIp = config.BootstrapNode.IpAddress;
var bootPort = config.BootstrapNode.Port; var bootPort = config.BootstrapNode.Port;
var bootstrapArg = $" --bootnodes enode://{bootPubKey}@{bootIp}:{bootPort} --nat=extip:{bootIp}"; var bootstrapArg = $" --bootnodes enode://{bootPubKey}@{bootIp}:{bootPort}";
args += bootstrapArg; 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}"; 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) private void UnlockAccounts(int startIndex, int numberOfAccounts)
{ {
if (startIndex < 0) throw new ArgumentException(); 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!"); if (startIndex + numberOfAccounts > 1000) throw new ArgumentException("Out of accounts!");
AddEnvVar("UNLOCK_START_INDEX", startIndex.ToString()); AddEnvVar("UNLOCK_START_INDEX", startIndex.ToString());
AddEnvVar("UNLOCK_NUMBER", numberOfAccounts.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) private Port CreateDiscoveryPort(GethStartupConfig config)
{ {
if (config.IsPublicTestNet == null) return AddInternalPort(DiscoveryPortTag); if (config.IsPublicTestNet == null) return AddInternalPort(DiscoveryPortTag);

View File

@ -19,13 +19,14 @@ namespace GethPlugin
void SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new(); void SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
decimal? GetSyncedBlockNumber(); decimal? GetSyncedBlockNumber();
bool IsContractAvailable(string abi, string contractAddress); bool IsContractAvailable(string abi, string contractAddress);
GethBootstrapNode GetBootstrapRecord();
} }
public class GethNode : IGethNode public class DeploymentGethNode : BaseGethNode, IGethNode
{ {
private readonly ILog log; private readonly ILog log;
public GethNode(ILog log, GethDeployment startResult) public DeploymentGethNode(ILog log, GethDeployment startResult)
{ {
this.log = log; this.log = log;
StartResult = startResult; StartResult = startResult;
@ -34,6 +35,59 @@ namespace GethPlugin
public GethDeployment StartResult { get; } public GethDeployment StartResult { get; }
public RunningContainer Container => StartResult.Container; 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() public Ether GetEthBalance()
{ {
return StartInteraction().GetEthBalance().Eth(); return StartInteraction().GetEthBalance().Eth();
@ -69,15 +123,6 @@ namespace GethPlugin
StartInteraction().SendTransaction(contractAddress, function); 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() public decimal? GetSyncedBlockNumber()
{ {
return StartInteraction().GetSyncedBlockNumber(); return StartInteraction().GetSyncedBlockNumber();
@ -87,5 +132,7 @@ namespace GethPlugin
{ {
return StartInteraction().IsContractAvailable(abi, contractAddress); return StartInteraction().IsContractAvailable(abi, contractAddress);
} }
protected abstract NethereumInteraction StartInteraction();
} }
} }

View File

@ -44,7 +44,7 @@ namespace GethPlugin
public IGethNode WrapGethContainer(GethDeployment startResult) public IGethNode WrapGethContainer(GethDeployment startResult)
{ {
startResult = SerializeGate.Gate(startResult); startResult = SerializeGate.Gate(startResult);
return new GethNode(tools.GetLog(), startResult); return new DeploymentGethNode(tools.GetLog(), startResult);
} }
private void Log(string msg) private void Log(string msg)

View File

@ -3,6 +3,7 @@
public interface IGethSetup public interface IGethSetup
{ {
IGethSetup IsMiner(); IGethSetup IsMiner();
IGethSetup WithBootstrapNode(IGethNode node);
IGethSetup WithBootstrapNode(GethBootstrapNode node); IGethSetup WithBootstrapNode(GethBootstrapNode node);
IGethSetup WithName(string name); IGethSetup WithName(string name);
IGethSetup AsPublicTestNet(GethTestNetConfig gethTestNetConfig); IGethSetup AsPublicTestNet(GethTestNetConfig gethTestNetConfig);
@ -15,6 +16,11 @@
public string? NameOverride { get; private set; } public string? NameOverride { get; private set; }
public GethTestNetConfig? IsPublicTestNet { get; private set; } public GethTestNetConfig? IsPublicTestNet { get; private set; }
public IGethSetup WithBootstrapNode(IGethNode node)
{
return WithBootstrapNode(node.GetBootstrapRecord());
}
public IGethSetup WithBootstrapNode(GethBootstrapNode node) public IGethSetup WithBootstrapNode(GethBootstrapNode node)
{ {
BootstrapNode = node; BootstrapNode = node;
@ -42,14 +48,12 @@
public class GethTestNetConfig public class GethTestNetConfig
{ {
public GethTestNetConfig(string publicIp, int discoveryPort, int listenPort) public GethTestNetConfig(int discoveryPort, int listenPort)
{ {
PublicIp = publicIp;
DiscoveryPort = discoveryPort; DiscoveryPort = discoveryPort;
ListenPort = listenPort; ListenPort = listenPort;
} }
public string PublicIp { get; }
public int DiscoveryPort { get; } public int DiscoveryPort { get; }
public int ListenPort { get; } public int ListenPort { get; }
} }

View File

@ -18,6 +18,8 @@ namespace MetricsPlugin
public RunningContainers CollectMetricsFor(IMetricsScrapeTarget[] targets) 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..."); Log($"Starting metrics server for {targets.Length} targets...");
var startupConfig = new StartupConfig(); var startupConfig = new StartupConfig();
startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(targets))); startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(targets)));

View File

@ -97,5 +97,22 @@ namespace CodexTests.BasicTests
CheckLogForErrors(seller, buyer); 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));
}
} }
} }

View File

@ -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);
}
}

View File

@ -9,13 +9,7 @@ namespace BiblioTech
public abstract string Name { get; } public abstract string Name { get; }
public abstract string StartingMessage { get; } public abstract string StartingMessage { get; }
public abstract string Description { get; } public abstract string Description { get; }
public virtual CommandOption[] Options public virtual CommandOption[] Options => Array.Empty<CommandOption>();
{
get
{
return Array.Empty<CommandOption>();
}
}
public async Task SlashCommandHandler(SocketSlashCommand command) public async Task SlashCommandHandler(SocketSlashCommand command)
{ {
@ -29,7 +23,15 @@ namespace BiblioTech
} }
catch (Exception ex) 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); Console.WriteLine(ex);
} }
} }

View File

@ -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<bool> OnInvoke(CommandContext context)
{
return Task.FromResult(true);
}
}
}

View File

@ -1,27 +1,89 @@
using BiblioTech.Options; using BiblioTech.Options;
using CodexContractsPlugin; using CodexContractsPlugin;
using CodexPlugin;
using Core;
using GethPlugin; using GethPlugin;
using Logging;
namespace BiblioTech 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<string>();
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) public static string GethHost { get; } = string.Empty;
{ public static int GethPort { get; }
var gethDeployment = codexDeployment.GethDeployment; public static string PrivateKey { get; } = string.Empty;
var contractsDeployment = codexDeployment.CodexContractsDeployment; 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); private static string? GetEnvVar(List<string> error, string name)
var contracts = ci.WrapCodexContractsDeployment(gethNode, contractsDeployment); {
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); await Execute(context, gethNode, contracts);
} }

View File

@ -1,7 +1,4 @@
using BiblioTech.Options; using BiblioTech.Options;
using CodexPlugin;
using Core;
using Newtonsoft.Json;
namespace BiblioTech.Commands namespace BiblioTech.Commands
{ {
@ -9,17 +6,16 @@ namespace BiblioTech.Commands
{ {
private readonly ClearUserAssociationCommand clearCommand = new ClearUserAssociationCommand(); private readonly ClearUserAssociationCommand clearCommand = new ClearUserAssociationCommand();
private readonly ReportCommand reportCommand = new ReportCommand(); 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 WhoIsCommand whoIsCommand = new WhoIsCommand();
private readonly NetInfoCommand netInfoCommand; private readonly AddSprCommand addSprCommand;
private readonly DebugPeerCommand debugPeerCommand; private readonly ClearSprsCommand clearSprsCommand;
private readonly GetSprCommand getSprCommand;
public AdminCommand(CoreInterface ci) public AdminCommand(SprCommand sprCommand)
{ {
netInfoCommand = new NetInfoCommand(ci); addSprCommand = new AddSprCommand(sprCommand);
debugPeerCommand = new DebugPeerCommand(ci); clearSprsCommand = new ClearSprsCommand(sprCommand);
getSprCommand = new GetSprCommand(sprCommand);
} }
public override string Name => "admin"; public override string Name => "admin";
@ -30,12 +26,10 @@ namespace BiblioTech.Commands
{ {
clearCommand, clearCommand,
reportCommand, reportCommand,
deployListCommand,
deployUploadCommand,
deployRemoveCommand,
whoIsCommand, whoIsCommand,
netInfoCommand, addSprCommand,
debugPeerCommand clearSprsCommand,
getSprCommand
}; };
protected override async Task Invoke(CommandContext context) protected override async Task Invoke(CommandContext context)
@ -54,12 +48,10 @@ namespace BiblioTech.Commands
await clearCommand.CommandHandler(context); await clearCommand.CommandHandler(context);
await reportCommand.CommandHandler(context); await reportCommand.CommandHandler(context);
await deployListCommand.CommandHandler(context);
await deployUploadCommand.CommandHandler(context);
await deployRemoveCommand.CommandHandler(context);
await whoIsCommand.CommandHandler(context); await whoIsCommand.CommandHandler(context);
await netInfoCommand.CommandHandler(context); await addSprCommand.CommandHandler(context);
await debugPeerCommand.CommandHandler(context); await clearSprsCommand.CommandHandler(context);
await getSprCommand.CommandHandler(context);
} }
public class ClearUserAssociationCommand : SubCommandOption public class ClearUserAssociationCommand : SubCommandOption
@ -109,108 +101,8 @@ namespace BiblioTech.Commands
return; return;
} }
var report = string.Join(Environment.NewLine, Program.UserRepo.GetInteractionReport(user)); var report = Program.UserRepo.GetInteractionReport(user);
if (report.Length > 1900) await context.Followup(report);
{
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.");
}
} }
} }
@ -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) public AddSprCommand(SprCommand sprCommand)
: base(name, description) : 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<ICodexNodeGroup, string, Task> action) public override CommandOption[] Options => new[] { stringOption };
{
var deployment = Program.DeploymentFilesMonitor.GetDeployments().SingleOrDefault();
if (deployment == null)
{
await context.Followup("No deployment found.");
return;
}
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()); sprCommand.Add(spr);
await action(group, deployment.Metadata.Name); 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) private readonly SprCommand sprCommand;
: base(ci, name: "netinfo", private readonly StringOption stringOption = new StringOption("areyousure", "set to 'true' if you are.", true);
description: "Fetches info endpoints of codex nodes.")
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) protected override async Task onSubCommand(CommandContext context)
{ {
await OnDeployment(context, async (group, name) => await context.Followup("SPRs: " + string.Join(", ", sprCommand.Get().Select(s => $"'{s}'")));
{
var nl = Environment.NewLine;
var content = new List<string>
{
$"{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()));
});
} }
} }
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(" ", "-");
}
} }
} }

View File

@ -1,5 +1,6 @@
using BiblioTech.Options; using BiblioTech.Options;
using CodexContractsPlugin; using CodexContractsPlugin;
using CodexPlugin;
using Core; using Core;
using GethPlugin; using GethPlugin;
@ -12,8 +13,7 @@ namespace BiblioTech.Commands
description: "If set, get balance for another user. (Optional, admin-only)", description: "If set, get balance for another user. (Optional, admin-only)",
isRequired: false); isRequired: false);
public GetBalanceCommand(CoreInterface ci, UserAssociateCommand userAssociateCommand) public GetBalanceCommand(UserAssociateCommand userAssociateCommand)
: base(ci)
{ {
this.userAssociateCommand = userAssociateCommand; this.userAssociateCommand = userAssociateCommand;
} }

View File

@ -1,6 +1,5 @@
using BiblioTech.Options; using BiblioTech.Options;
using CodexContractsPlugin; using CodexContractsPlugin;
using Core;
using GethPlugin; using GethPlugin;
namespace BiblioTech.Commands namespace BiblioTech.Commands
@ -14,8 +13,7 @@ namespace BiblioTech.Commands
isRequired: false); isRequired: false);
private readonly UserAssociateCommand userAssociateCommand; private readonly UserAssociateCommand userAssociateCommand;
public MintCommand(CoreInterface ci, UserAssociateCommand userAssociateCommand) public MintCommand(UserAssociateCommand userAssociateCommand)
: base(ci)
{ {
this.userAssociateCommand = userAssociateCommand; this.userAssociateCommand = userAssociateCommand;
} }

View File

@ -1,61 +1,48 @@
using BiblioTech.Options; using BiblioTech.Options;
using CodexPlugin;
using Core;
namespace BiblioTech.Commands namespace BiblioTech.Commands
{ {
public class SprCommand : BaseCodexCommand public class SprCommand : BaseCommand
{ {
private readonly Random random = new Random(); private readonly Random random = new Random();
private readonly List<string> sprCache = new List<string>(); private readonly List<string> knownSprs = new List<string>();
private DateTime lastUpdate = DateTime.MinValue;
public SprCommand(CoreInterface ci) : base(ci)
{
}
public override string Name => "boot"; public override string Name => "boot";
public override string StartingMessage => RandomBusyMessage.Get(); public override string StartingMessage => RandomBusyMessage.Get();
public override string Description => "Gets an SPR. (Signed peer record, used for bootstrapping.)"; public override string Description => "Gets an SPR. (Signed peer record, used for bootstrapping.)";
protected override async Task<bool> OnInvoke(CommandContext context) protected override async Task Invoke(CommandContext context)
{ {
if (ShouldUpdate())
{
return true;
}
await ReplyWithRandomSpr(context); await ReplyWithRandomSpr(context);
return false;
} }
protected override async Task Execute(CommandContext context, ICodexNodeGroup codexGroup) public void Add(string spr)
{ {
lastUpdate = DateTime.UtcNow; if (knownSprs.Contains(spr)) return;
sprCache.Clear(); knownSprs.Add(spr);
}
var infos = codexGroup.Select(c => c.GetDebugInfo()).ToArray(); public void Clear()
sprCache.AddRange(infos.Select(i => i.spr)); {
knownSprs.Clear();
}
await ReplyWithRandomSpr(context); public string[] Get()
{
return knownSprs.ToArray();
} }
private async Task ReplyWithRandomSpr(CommandContext context) private async Task ReplyWithRandomSpr(CommandContext context)
{ {
if (!sprCache.Any()) if (!knownSprs.Any())
{ {
await context.Followup("I'm sorry, no SPRs are available... :c"); await context.Followup("I'm sorry, no SPRs are available... :c");
return; return;
} }
var i = random.Next(0, sprCache.Count); var i = random.Next(0, knownSprs.Count);
var spr = sprCache[i]; var spr = knownSprs[i];
await context.Followup($"Your SPR: '{spr}'"); await context.Followup($"Your SPR: `{spr}`");
}
private bool ShouldUpdate()
{
return (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10);
} }
} }
} }

View File

@ -27,8 +27,6 @@ namespace BiblioTech.Commands
return; return;
} }
// private commands
var result = Program.UserRepo.AssociateUserWithAddress(user, data); var result = Program.UserRepo.AssociateUserWithAddress(user, data);
if (result) if (result)
{ {

View File

@ -19,6 +19,12 @@ namespace BiblioTech
[Uniform("admin-channel-name", "ac", "ADMINCHANNELNAME", true, "Name of the Discord server channel where admin commands are allowed.")] [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"; 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 public string EndpointsPath
{ {
get get

View File

@ -1,89 +0,0 @@
using CodexPlugin;
using Discord;
using Newtonsoft.Json;
namespace BiblioTech
{
public class DeploymentsFilesMonitor
{
private readonly List<CodexDeployment> deployments = new List<CodexDeployment>();
public void Initialize()
{
LoadDeployments();
}
public CodexDeployment[] GetDeployments()
{
return deployments.ToArray();
}
public async Task<bool> 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<CodexDeployment>(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<CodexDeployment>());
}
private CodexDeployment? ProcessFile(string filename)
{
try
{
var lines = string.Join(" ", File.ReadAllLines(filename));
return JsonConvert.DeserializeObject<CodexDeployment>(lines);
}
catch
{
return null;
}
}
}
}

View File

@ -4,30 +4,99 @@ namespace BiblioTech.Options
{ {
public class CommandContext public class CommandContext
{ {
private const string AttachmentFolder = "attachments";
public CommandContext(SocketSlashCommand command, IReadOnlyCollection<SocketSlashCommandDataOption> options) public CommandContext(SocketSlashCommand command, IReadOnlyCollection<SocketSlashCommandDataOption> options)
{ {
Command = command; Command = command;
Options = options; Options = options;
var attachmentPath = Path.Combine(Program.Config.DataPath, AttachmentFolder);
if (Directory.Exists(attachmentPath))
{
Directory.CreateDirectory(attachmentPath);
}
} }
public SocketSlashCommand Command { get; } public SocketSlashCommand Command { get; }
public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; } public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; }
public async Task Followup(string message) public async Task Followup(string line)
{ {
await Command.ModifyOriginalResponseAsync(m => var array = line.Split(Environment.NewLine);
{ await Followup(array);
m.Content = message;
});
} }
public async Task FollowupWithAttachement(string filename, string content) public async Task Followup(string[] lines)
{ {
using var fileStream = new MemoryStream(); var chunker = new LineChunker(lines);
using var streamWriter = new StreamWriter(fileStream); var chunks = chunker.GetChunks();
await streamWriter.WriteAsync(content); 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<string> input;
private readonly int maxCharacters;
public LineChunker(string[] input, int maxCharacters = 1950)
{
this.input = input.ToList();
this.maxCharacters = maxCharacters;
}
public List<string[]> GetChunks()
{
var result = new List<string[]>();
while (input.Any())
{
result.Add(GetChunk());
}
return result;
}
private string[] GetChunk()
{
var totalLength = 0;
var result = new List<string>();
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();
} }
} }
} }

View File

@ -1,9 +1,7 @@
using ArgsUniform; using ArgsUniform;
using BiblioTech.Commands; using BiblioTech.Commands;
using Core;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Logging;
namespace BiblioTech namespace BiblioTech
{ {
@ -12,7 +10,6 @@ namespace BiblioTech
private DiscordSocketClient client = null!; private DiscordSocketClient client = null!;
public static Configuration Config { get; private set; } = 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 UserRepo UserRepo { get; } = new UserRepo();
public static AdminChecker AdminChecker { get; } = new AdminChecker(); public static AdminChecker AdminChecker { get; } = new AdminChecker();
@ -21,8 +18,6 @@ namespace BiblioTech
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args); var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
Config = uniformArgs.Parse(); Config = uniformArgs.Parse();
DeploymentFilesMonitor.Initialize();
EnsurePath(Config.DataPath); EnsurePath(Config.DataPath);
EnsurePath(Config.UserDataPath); EnsurePath(Config.UserDataPath);
EnsurePath(Config.EndpointsPath); EnsurePath(Config.EndpointsPath);
@ -36,25 +31,14 @@ namespace BiblioTech
client = new DiscordSocketClient(); client = new DiscordSocketClient();
client.Log += Log; client.Log += Log;
ProjectPlugin.Load<CodexPlugin.CodexPlugin>();
ProjectPlugin.Load<GethPlugin.GethPlugin>();
ProjectPlugin.Load<CodexContractsPlugin.CodexContractsPlugin>();
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 associateCommand = new UserAssociateCommand();
var sprCommand = new SprCommand();
var handler = new CommandHandler(client, var handler = new CommandHandler(client,
new GetBalanceCommand(ci, associateCommand), new GetBalanceCommand(associateCommand),
new MintCommand(ci, associateCommand), new MintCommand(associateCommand),
new SprCommand(ci), sprCommand,
associateCommand, associateCommand,
new AdminCommand(ci) new AdminCommand(sprCommand)
); );
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);

View File

@ -2,7 +2,6 @@
using Discord; using Discord;
using GethPlugin; using GethPlugin;
using Newtonsoft.Json; using Newtonsoft.Json;
using Utils;
namespace BiblioTech namespace BiblioTech
{ {
@ -76,17 +75,17 @@ namespace BiblioTech
return result.ToArray(); return result.ToArray();
} }
public string GetUserReport(IUser user) public string[] GetUserReport(IUser user)
{ {
var userData = GetUserData(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(); return userData.CreateOverview();
} }
public string GetUserReport(EthAddress ethAddress) public string[] GetUserReport(EthAddress ethAddress)
{ {
var userData = GetUserDataForAddress(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(); return userData.CreateOverview();
} }
@ -196,14 +195,15 @@ namespace BiblioTech
public List<UserAssociateAddressEvent> AssociateEvents { get; } public List<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> MintEvents { get; } public List<UserMintEvent> MintEvents { get; }
public string CreateOverview() public string[] CreateOverview()
{ {
var nl = Environment.NewLine; return new[]
return {
$"name: '{Name}' - id:{DiscordId}{nl}" + $"name: '{Name}' - id:{DiscordId}",
$"joined: {CreatedUtc.ToString("o")}{nl}" + $"joined: {CreatedUtc.ToString("o")}",
$"current address: {CurrentAddress}{nl}" + $"current address: {CurrentAddress}",
$"{AssociateEvents.Count + MintEvents.Count} total bot events."; $"{AssociateEvents.Count + MintEvents.Count} total bot events."
};
} }
} }

View File

@ -105,7 +105,6 @@ namespace CodexNetDeployer
return new CodexTestNetConfig return new CodexTestNetConfig
{ {
PublicNatIP = config.PublicIP,
PublicDiscoveryPort = Convert.ToInt32(discPort), PublicDiscoveryPort = Convert.ToInt32(discPort),
PublicListenPort = Convert.ToInt32(listenPort) PublicListenPort = Convert.ToInt32(listenPort)
}; };

View File

@ -86,18 +86,12 @@ namespace CodexNetDeployer
[Uniform("public-testnet", "ptn", "PUBLICTESTNET", false, "If true, deployment is created for public exposure. Default is false.")] [Uniform("public-testnet", "ptn", "PUBLICTESTNET", false, "If true, deployment is created for public exposure. Default is false.")]
public bool IsPublicTestNet { get; set; } = 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.")] [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; 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.")] [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; 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.")] [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; } public int PublicGethDiscPort { get; set; }
@ -150,9 +144,11 @@ namespace CodexNetDeployer
if (IsPublicTestNet) if (IsPublicTestNet)
{ {
if (string.IsNullOrEmpty(PublicIP)) errors.Add("Public IP required when deploying public testnet."); 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 (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 (PublicGethDiscPort == 0) errors.Add("Geth public discovery port is not set.");
if (PublicGethListenPort == 0) errors.Add("Geth public listen port is not set."); if (PublicGethListenPort == 0) errors.Add("Geth public listen port is not set.");
} }

View File

@ -82,7 +82,7 @@ namespace CodexNetDeployer
var codexInstances = CreateCodexInstances(startResults); var codexInstances = CreateCodexInstances(startResults);
var discordBotContainer = DeployDiscordBot(ci); var discordBotContainer = DeployDiscordBot(ci, gethDeployment, contractsDeployment);
return new CodexDeployment(codexInstances, gethDeployment, contractsDeployment, metricsService, discordBotContainer, CreateMetadata(startUtc)); return new CodexDeployment(codexInstances, gethDeployment, contractsDeployment, metricsService, discordBotContainer, CreateMetadata(startUtc));
} }
@ -113,7 +113,6 @@ namespace CodexNetDeployer
if (config.IsPublicTestNet) if (config.IsPublicTestNet)
{ {
s.AsPublicTestNet(new GethTestNetConfig( s.AsPublicTestNet(new GethTestNetConfig(
publicIp: config.PublicGethIP,
discoveryPort: config.PublicGethDiscPort, discoveryPort: config.PublicGethDiscPort,
listenPort: config.PublicGethListenPort 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; if (!config.DeployDiscordBot) return null;
Log("Deploying Discord bot..."); 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( var rc = ci.DeployCodexDiscordBot(new DiscordBotStartupConfig(
name: "discordbot-" + config.DeploymentName, name: "discordbot-" + config.DeploymentName,
token: config.DiscordBotToken, token: config.DiscordBotToken,
serverName: config.DiscordBotServerName, serverName: config.DiscordBotServerName,
adminRoleName: config.DiscordBotAdminRoleName, adminRoleName: config.DiscordBotAdminRoleName,
adminChannelName: config.DiscordBotAdminChannelName) adminChannelName: config.DiscordBotAdminChannelName,
kubeNamespace: config.KubeNamespace,
gethInfo: info)
{ {
DataPath = config.DiscordBotDataPath DataPath = config.DiscordBotDataPath
}); });
@ -142,7 +153,7 @@ namespace CodexNetDeployer
private RunningContainers? StartMetricsService(CoreInterface ci, List<CodexNodeStartResult> startResults) private RunningContainers? StartMetricsService(CoreInterface ci, List<CodexNodeStartResult> startResults)
{ {
if (!config.MetricsScraper) return null; if (!config.MetricsScraper || !startResults.Any()) return null;
Log("Starting metrics service..."); Log("Starting metrics service...");
@ -176,7 +187,7 @@ namespace CodexNetDeployer
private void CheckPeerConnectivity(List<CodexNodeStartResult> codexContainers) private void CheckPeerConnectivity(List<CodexNodeStartResult> codexContainers)
{ {
if (!config.CheckPeerConnection) return; if (!config.CheckPeerConnection || !codexContainers.Any()) return;
Log("Starting peer connectivity check for deployed nodes..."); Log("Starting peer connectivity check for deployed nodes...");
peerConnectivityChecker.CheckConnectivity(codexContainers); peerConnectivityChecker.CheckConnectivity(codexContainers);

View File

@ -16,12 +16,10 @@ dotnet run \
--check-connect=0 \ --check-connect=0 \
\ \
--public-testnet=1 \ --public-testnet=1 \
--public-ip=1.2.3.4 \ --public-discports=30010,30020,30030 \
--public-discports=20010,20020,20030 \ --public-listenports=30011,30021,30031 \
--public-listenports=20011,20021,20031 \ --public-gethdiscport=30040 \
--public-gethip=1.2.3.5 \ --public-gethlistenport=30041 \
--public-gethdiscport=20040 \
--public-gethlistenport=20041 \
\ \
--discord-bot=1 \ --discord-bot=1 \
--dbot-token=tokenhere \ --dbot-token=tokenhere \