Compare commits

..
Author SHA1 Message Date
benbierens 4adce837ec Logs total run duration in overview log. 2023-10-23 10:32:11 +02:00
benbierens e11a7d1600 Gives deployments a name. 2023-10-23 10:19:52 +02:00
benbierens ad70394333 Merge branch 'master' into app/discord-bot 2023-10-23 10:13:23 +02:00
benbierens 50fbf0ad52 Replaces retry-time with maxNumberOfRetries in timesets. 2023-10-23 10:11:02 +02:00
benbierens 45fbd699a9 Disables calls to custom API endpoints. 2023-10-23 09:36:31 +02:00
benbierens bf18fa03a2 adds sleep to the starting of the test screens 2023-10-22 11:29:16 +02:00
benbierens 116f62e73e Adds eth address validation. 2023-10-22 11:26:00 +02:00
benbierens 8ef2e6023e All works 2023-10-22 11:10:45 +02:00
benbierens e16b1ce079 Sets up guild role checking 2023-10-22 10:38:46 +02:00
benbierens 4aa4731480 setting up all the commands 2023-10-22 10:10:52 +02:00
benbierens 8ad2dee67c Adds user repo. 2023-10-22 09:32:03 +02:00
benbierens 869aeb9253 Deals with timeout for operations that may take a while. 2023-10-20 11:20:38 +02:00
benbierens 8910c7ff27 Stores contracts deployment as part of CodexDeployment json. 2023-10-20 10:15:38 +02:00
benbierens 2b10f2ec58 Adds mint command 2023-10-20 10:14:56 +02:00
benbierens 991927b95f Setting up balance-getting command 2023-10-20 09:49:23 +02:00
benbierens b1bd1de027 Merge branch 'feature/multiple-container-addresses' 2023-10-20 08:31:45 +02:00
benbierens 3b258c9e2e Pins contract image to one compatible with current main codex 2023-10-20 08:31:23 +02:00
benbierens 0fd6a6f06e Fixes port tag mismatch 2023-10-19 15:48:49 +02:00
benbierens 2fea475237 multiple service ports 2023-10-19 14:03:36 +02:00
benbierens 45050c34e4 Implements GetAddress method for runningContainers. 2023-10-19 11:18:59 +02:00
benbierens 43fa57dc97 Mandatory port tags for exposed ports 2023-10-19 11:12:08 +02:00
benbierens 3a8bb760ef Adding support for multiple exposed container ports 2023-10-19 11:08:30 +02:00
benbierens 766e2f5c20 Very basic endpoint pinging that might not even work. 2023-10-18 14:59:39 +02:00
benbierens 888b19d8e5 working example of slash commands with arguments 2023-10-18 13:55:56 +02:00
benbierens f33866efc1 setting up slash commands 2023-10-18 13:48:15 +02:00
benbierens 8c7229504e do not print token 2023-10-18 11:21:06 +02:00
benbierens bcb05cd0c9 Dockerizes discord bot 2023-10-18 11:01:24 +02:00
benbierens 7179c70463 Sets up an echo command 2023-10-18 09:10:04 +02:00
benbierens b3da42522f Sets up project 2023-10-18 08:57:59 +02:00
benbierens 6b1102efa7 Adds name argument to deploy-and-run script 2023-10-17 14:32:05 +02:00
benbierens 8c82b4c527 defaults the codex log topics to warn 2023-10-17 13:52:04 +02:00
benbierens d0cafb83a1 Fixes compile error in single test runner 2023-10-16 13:10:45 +02:00
benbierens 8e4d43b73b Adds start and finished times to deployment json. 2023-10-16 11:19:57 +02:00
benbierens 8f37b4cf38 Sets long timeouts for debug/repostore call. 2023-10-10 18:08:21 +02:00
benbierens 1a277ef1b5 Logging repostore content on twoclient test failure 2023-10-10 17:54:19 +02:00
benbierens b81d574a4b adds block exchange tests. Updates namespaces 2023-10-09 16:59:52 +02:00
benbierens 7aae48d489 Merge branch 'experiment/deploy-replication' 2023-10-09 16:37:09 +02:00
benbierens 58016378c4 Increase deployer kubernetes timeout 2023-10-07 07:40:20 +02:00
75 changed files with 1281 additions and 170 deletions
+1 -1
View File
@@ -196,7 +196,7 @@ namespace Core
private T Retry<T>(Func<T> operation, string description)
{
return Time.Retry(operation, timeSet.HttpCallRetryTime(), timeSet.HttpCallRetryDelay(), description);
return Time.Retry(operation, timeSet.HttpMaxNumberOfRetries(), timeSet.HttpCallRetryDelay(), description);
}
private HttpClient GetClient()
+7 -1
View File
@@ -23,6 +23,7 @@ namespace Core
public interface IHttpFactoryTool
{
IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null);
IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, ITimeSet timeSet, string? logAlias = null);
IHttp CreateHttp(Address address, string baseUrl, string? logAlias = null);
}
@@ -53,7 +54,12 @@ namespace Core
public IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
{
return new Http(log, timeSet, address, baseUrl, onClientCreated, logAlias);
return CreateHttp(address, baseUrl, onClientCreated, timeSet, logAlias);
}
public IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, ITimeSet ts, string? logAlias = null)
{
return new Http(log, ts, address, baseUrl, onClientCreated, logAlias);
}
public IHttp CreateHttp(Address address, string baseUrl, string? logAlias = null)
+2 -4
View File
@@ -5,11 +5,9 @@ namespace Core
public static class SerializeGate
{
/// <summary>
/// SerializeGate was added to help ensure deployment objects are serializable
/// and remain viable after deserialization.
/// SerializeGate was added to help ensure deployment objects are serializable and remain viable after deserialization.
/// Tools can be built on top of the core interface that rely on deployment objects being serializable.
/// Insert the serialization gate after deployment but before wrapping to ensure any future changes
/// don't break this requirement.
/// Insert the serialization gate after deployment but before wrapping to ensure any future changes don't break this requirement.
/// </summary>
public static T Gate<T>(T anything)
{
+32 -4
View File
@@ -3,7 +3,7 @@
public interface ITimeSet
{
TimeSpan HttpCallTimeout();
TimeSpan HttpCallRetryTime();
int HttpMaxNumberOfRetries();
TimeSpan HttpCallRetryDelay();
TimeSpan WaitForK8sServiceDelay();
TimeSpan K8sOperationTimeout();
@@ -13,12 +13,12 @@
{
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromMinutes(5);
return TimeSpan.FromMinutes(3);
}
public TimeSpan HttpCallRetryTime()
public int HttpMaxNumberOfRetries()
{
return TimeSpan.FromMinutes(1);
return 3;
}
public TimeSpan HttpCallRetryDelay()
@@ -36,4 +36,32 @@
return TimeSpan.FromMinutes(30);
}
}
public class LongTimeSet : ITimeSet
{
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromHours(2);
}
public int HttpMaxNumberOfRetries()
{
return 1;
}
public TimeSpan HttpCallRetryDelay()
{
return TimeSpan.FromSeconds(2);
}
public TimeSpan WaitForK8sServiceDelay()
{
return TimeSpan.FromSeconds(10);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromMinutes(15);
}
}
}
@@ -24,6 +24,8 @@
{
Name = $"ctnr{Number}";
}
if (exposedPorts.Any(p => string.IsNullOrEmpty(p.Tag))) throw new Exception("Port tags are required for all exposed ports.");
}
public string Name { get; }
@@ -65,6 +67,12 @@
public int Number { get; }
public string Tag { get; }
public override string ToString()
{
if (string.IsNullOrEmpty(Tag)) return $"untagged-port={Number}";
return $"{Tag}={Number}";
}
}
public class EnvVar
@@ -50,12 +50,12 @@ namespace KubernetesWorkflow
protected int Index { get; private set; } = 0;
protected abstract void Initialize(StartupConfig config);
protected Port AddExposedPort(string tag = "")
protected Port AddExposedPort(string tag)
{
return AddExposedPort(factory.CreatePort(tag));
}
protected Port AddExposedPort(int number, string tag = "")
protected Port AddExposedPort(int number, string tag)
{
return AddExposedPort(factory.CreatePort(number, tag));
}
@@ -67,7 +67,7 @@ namespace KubernetesWorkflow
return p;
}
protected void AddExposedPortAndVar(string name, string tag = "")
protected void AddExposedPortAndVar(string name, string tag)
{
AddEnvVar(name, AddExposedPort(tag));
}
@@ -132,11 +132,6 @@ namespace KubernetesWorkflow
private Port AddExposedPort(Port port)
{
if (exposedPorts.Any())
{
throw new NotImplementedException("Current implementation only support 1 exposed port per container recipe. " +
$"Methods for determining container addresses in {nameof(StartupWorkflow)} currently rely on this constraint.");
}
exposedPorts.Add(port);
return port;
}
@@ -572,16 +572,15 @@ namespace KubernetesWorkflow
var readback = client.Run(c => c.ReadNamespacedService(serviceSpec.Metadata.Name, K8sNamespace));
foreach (var r in containerRecipes)
{
if (r.ExposedPorts.Any())
foreach (var port in r.ExposedPorts)
{
var firstExposedPort = r.ExposedPorts.First();
var portName = GetNameForPort(r, firstExposedPort);
var portName = GetNameForPort(r, port);
var matchingServicePorts = readback.Spec.Ports.Where(p => p.Name == portName);
if (matchingServicePorts.Any())
{
// These service ports belongs to this recipe.
var optionals = matchingServicePorts.Select(p => MapNodePortIfAble(p, portName));
var optionals = matchingServicePorts.Select(p => MapNodePortIfAble(p, port.Tag));
var ports = optionals.Where(p => p != null).Select(p => p!).ToArray();
result.Add(new ContainerRecipePortMapEntry(r.Number, ports));
@@ -16,18 +16,26 @@ namespace KubernetesWorkflow
internal static RunnerLocation DetermineRunnerLocation(RunningContainer container)
{
if (knownLocation != null) return knownLocation.Value;
knownLocation = PingForLocation(container);
return knownLocation.Value;
}
private static RunnerLocation PingForLocation(RunningContainer container)
{
if (PingHost(container.Pod.PodInfo.Ip))
{
knownLocation = RunnerLocation.InternalToCluster;
}
else if (PingHost(Format(container.ClusterExternalAddress)))
{
knownLocation = RunnerLocation.ExternalToCluster;
return RunnerLocation.InternalToCluster;
}
if (knownLocation == null) throw new Exception("Unable to determine location relative to kubernetes cluster.");
return knownLocation.Value;
foreach (var port in container.ContainerPorts)
{
if (PingHost(Format(port.ExternalAddress)))
{
return RunnerLocation.ExternalToCluster;
}
}
throw new Exception("Unable to determine location relative to kubernetes cluster.");
}
private static string Format(Address host)
@@ -24,37 +24,46 @@ namespace KubernetesWorkflow
public class RunningContainer
{
public RunningContainer(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts, string name, Address clusterExternalAddress, Address clusterInternalAddress)
public RunningContainer(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts, string name, ContainerPort[] containerPorts)
{
Pod = pod;
Recipe = recipe;
ServicePorts = servicePorts;
Name = name;
ClusterExternalAddress = clusterExternalAddress;
ClusterInternalAddress = clusterInternalAddress;
ContainerPorts = containerPorts;
}
public string Name { get; }
public RunningPod Pod { get; }
public ContainerRecipe Recipe { get; }
public Port[] ServicePorts { get; }
public Address ClusterExternalAddress { get; }
public Address ClusterInternalAddress { get; }
public ContainerPort[] ContainerPorts { get; }
[JsonIgnore]
public Address Address
public Address GetAddress(string portTag)
{
get
var containerPort = ContainerPorts.Single(c => c.Port.Tag == portTag);
if (RunnerLocationUtils.DetermineRunnerLocation(this) == RunnerLocation.InternalToCluster)
{
if (RunnerLocationUtils.DetermineRunnerLocation(this) == RunnerLocation.InternalToCluster)
{
return ClusterInternalAddress;
}
return ClusterExternalAddress;
return containerPort.InternalAddress;
}
return containerPort.ExternalAddress;
}
}
public class ContainerPort
{
public ContainerPort(Port port, Address externalAddress, Address internalAddress)
{
Port = port;
ExternalAddress = externalAddress;
InternalAddress = internalAddress;
}
public Port Port { get; }
public Address ExternalAddress { get; }
public Address InternalAddress { get; }
}
public static class RunningContainersExtensions
{
public static RunningContainer[] Containers(this RunningContainers[] runningContainers)
+4 -6
View File
@@ -19,12 +19,10 @@
public Port[] GetServicePortsForContainerRecipe(ContainerRecipe containerRecipe)
{
if (PortMapEntries.Any(p => p.ContainerNumber == containerRecipe.Number))
{
return PortMapEntries.Single(p => p.ContainerNumber == containerRecipe.Number).Ports;
}
return Array.Empty<Port>();
return PortMapEntries
.Where(p => p.ContainerNumber == containerRecipe.Number)
.SelectMany(p => p.Ports)
.ToArray();
}
}
+24 -21
View File
@@ -118,8 +118,7 @@ namespace KubernetesWorkflow
var name = GetContainerName(r, startupConfig);
return new RunningContainer(runningPod, r, servicePorts, name,
GetContainerExternalAddress(runningPod, servicePorts),
GetContainerInternalAddress(r));
CreateContainerPorts(runningPod, r, servicePorts));
}).ToArray();
}
@@ -137,35 +136,39 @@ namespace KubernetesWorkflow
}
}
private Address GetContainerExternalAddress(RunningPod pod, Port[] servicePorts)
private ContainerPort[] CreateContainerPorts(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts)
{
return new Address(
pod.Cluster.HostAddress,
GetServicePort(servicePorts));
var result = new List<ContainerPort>();
foreach (var exposedPort in recipe.ExposedPorts)
{
result.Add(new ContainerPort(
exposedPort,
GetContainerExternalAddress(pod, servicePorts, exposedPort),
GetContainerInternalAddress(exposedPort)));
}
return result.ToArray();
}
private Address GetContainerInternalAddress(ContainerRecipe recipe)
private static Address GetContainerExternalAddress(RunningPod pod, Port[] servicePorts, Port exposedPort)
{
var servicePort = servicePorts.Single(p => p.Tag == exposedPort.Tag);
return new Address(
pod.Cluster.HostAddress,
servicePort.Number);
}
private Address GetContainerInternalAddress(Port exposedPort)
{
var serviceName = "service-" + numberSource.WorkflowNumber;
var port = GetInternalPort(recipe);
var port = exposedPort.Number;
return new Address(
$"http://{serviceName}.{k8sNamespace}.svc.cluster.local",
port);
}
private static int GetServicePort(Port[] servicePorts)
{
if (servicePorts.Any()) return servicePorts.First().Number;
return 0;
}
private static int GetInternalPort(ContainerRecipe recipe)
{
if (recipe.ExposedPorts.Any()) return recipe.ExposedPorts.First().Number;
return 0;
}
private ContainerRecipe[] CreateRecipes(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
{
log.Debug();
+5
View File
@@ -10,5 +10,10 @@
public string Host { get; }
public int Port { get; }
public override string ToString()
{
return $"{Host}:{Port}";
}
}
}
+18 -12
View File
@@ -46,33 +46,35 @@
public static void Retry(Action action, string description)
{
Retry(action, TimeSpan.FromMinutes(1), description);
Retry(action, 1, description);
}
public static T Retry<T>(Func<T> action, string description)
{
return Retry(action, TimeSpan.FromMinutes(1), description);
return Retry(action, 1, description);
}
public static void Retry(Action action, TimeSpan timeout, string description)
public static void Retry(Action action, int maxRetries, string description)
{
Retry(action, timeout, TimeSpan.FromSeconds(1), description);
Retry(action, maxRetries, TimeSpan.FromSeconds(1), description);
}
public static T Retry<T>(Func<T> action, TimeSpan timeout, string description)
public static T Retry<T>(Func<T> action, int maxRetries, string description)
{
return Retry(action, timeout, TimeSpan.FromSeconds(1), description);
return Retry(action, maxRetries, TimeSpan.FromSeconds(1), description);
}
public static void Retry(Action action, TimeSpan timeout, TimeSpan retryTime, string description)
public static void Retry(Action action, int maxRetries, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var retries = 0;
var exceptions = new List<Exception>();
while (true)
{
if (DateTime.UtcNow - start > timeout)
if (retries > maxRetries)
{
throw new TimeoutException($"Retry '{description}' of {timeout.TotalSeconds} seconds timed out.", new AggregateException(exceptions));
var duration = DateTime.UtcNow - start;
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
}
try
@@ -83,21 +85,24 @@
catch (Exception ex)
{
exceptions.Add(ex);
retries++;
}
Sleep(retryTime);
}
}
public static T Retry<T>(Func<T> action, TimeSpan timeout, TimeSpan retryTime, string description)
public static T Retry<T>(Func<T> action, int maxRetries, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var retries = 0;
var exceptions = new List<Exception>();
while (true)
{
if (DateTime.UtcNow - start > timeout)
if (retries > maxRetries)
{
throw new TimeoutException($"Retry '{description}' of {timeout.TotalSeconds} seconds timed out.", new AggregateException(exceptions));
var duration = DateTime.UtcNow - start;
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
}
try
@@ -107,6 +112,7 @@
catch (Exception ex)
{
exceptions.Add(ex);
retries++;
}
Sleep(retryTime);
@@ -4,7 +4,7 @@ namespace CodexContractsPlugin
{
public class CodexContractsContainerRecipe : ContainerRecipeFactory
{
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:latest-dist-tests";
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:sha-1854dfb-dist-tests";
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
+18 -1
View File
@@ -1,5 +1,6 @@
using Core;
using KubernetesWorkflow;
using Utils;
namespace CodexPlugin
{
@@ -49,6 +50,11 @@ namespace CodexPlugin
return Http().HttpGetJson<CodexDebugBlockExchangeResponse>("debug/blockexchange");
}
public CodexDebugRepoStoreResponse[] GetDebugRepoStore()
{
return LongHttp().HttpGetJson<CodexDebugRepoStoreResponse[]>("debug/repostore");
}
public CodexDebugThresholdBreaches GetDebugThresholdBreaches()
{
return Http().HttpGetJson<CodexDebugThresholdBreaches>("debug/loop");
@@ -93,7 +99,17 @@ namespace CodexPlugin
private IHttp Http()
{
return tools.CreateHttp(Container.Address, baseUrl: "/api/codex/v1", CheckContainerCrashed, Container.Name);
return tools.CreateHttp(GetAddress(), baseUrl: "/api/codex/v1", CheckContainerCrashed, Container.Name);
}
private IHttp LongHttp()
{
return tools.CreateHttp(GetAddress(), baseUrl: "/api/codex/v1", CheckContainerCrashed, new LongTimeSet(), Container.Name);
}
private Address GetAddress()
{
return Container.GetAddress(CodexContainerRecipe.ApiPortTag);
}
private void CheckContainerCrashed(HttpClient client)
@@ -106,6 +122,7 @@ namespace CodexPlugin
var log = tools.GetLog();
var file = log.CreateSubfile();
log.Log($"Container {Container.Name} has crashed. Downloading crash log to '{file.FullFilename}'...");
file.Write($"Container Crash Log for {Container.Name}.");
using var reader = new StreamReader(crashLog);
var line = reader.ReadLine();
@@ -165,4 +165,9 @@ namespace CodexPlugin
public string wantType { get; set; } = string.Empty;
public bool sendDontHave { get; set; }
}
public class CodexDebugRepoStoreResponse
{
public string cid { get; set; } = string.Empty;
}
}
@@ -8,8 +8,10 @@ namespace CodexPlugin
private readonly MarketplaceStarter marketplaceStarter = new MarketplaceStarter();
private const string DefaultDockerImage = "codexstorage/nim-codex:latest-dist-tests";
public const string MetricsPortTag = "metrics_port";
public const string DiscoveryPortTag = "discovery-port";
public const string ApiPortTag = "codex_api_port";
public const string ListenPortTag = "codex_listen_port";
public const string MetricsPortTag = "codex_metrics_port";
public const string DiscoveryPortTag = "codex_discovery_port";
// Used by tests for time-constraint assertions.
public static readonly TimeSpan MaxUploadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
@@ -27,20 +29,20 @@ namespace CodexPlugin
var config = startupConfig.Get<CodexStartupConfig>();
AddExposedPortAndVar("CODEX_API_PORT");
AddExposedPortAndVar("CODEX_API_PORT", ApiPortTag);
AddEnvVar("CODEX_API_BINDADDR", "0.0.0.0");
var dataDir = $"datadir{ContainerNumber}";
AddEnvVar("CODEX_DATA_DIR", dataDir);
AddVolume($"codex/{dataDir}", GetVolumeCapacity(config));
AddInternalPortAndVar("CODEX_DISC_PORT", DiscoveryPortTag);
AddExposedPortAndVar("CODEX_DISC_PORT", DiscoveryPortTag);
AddEnvVar("CODEX_LOG_LEVEL", config.LogLevelWithTopics());
// This makes the node announce itself to its local (pod) IP address.
AddEnvVar("NAT_IP_AUTO", "true");
var listenPort = AddInternalPort();
var listenPort = AddExposedPort(ListenPortTag);
AddEnvVar("CODEX_LISTEN_ADDRS", $"/ip4/0.0.0.0/tcp/{listenPort.Number}");
if (!string.IsNullOrEmpty(config.BootstrapSpr))
+12 -5
View File
@@ -1,29 +1,34 @@
using GethPlugin;
using CodexContractsPlugin;
using GethPlugin;
using KubernetesWorkflow;
namespace CodexPlugin
{
public class CodexDeployment
{
public CodexDeployment(RunningContainer[] codexContainers, GethDeployment gethDeployment, RunningContainer? prometheusContainer, DeploymentMetadata metadata)
public CodexDeployment(RunningContainer[] codexContainers, GethDeployment gethDeployment, CodexContractsDeployment codexContractsDeployment, RunningContainer? prometheusContainer, DeploymentMetadata metadata)
{
CodexContainers = codexContainers;
GethDeployment = gethDeployment;
CodexContractsDeployment = codexContractsDeployment;
PrometheusContainer = prometheusContainer;
Metadata = metadata;
}
public RunningContainer[] CodexContainers { get; }
public GethDeployment GethDeployment { get; }
public CodexContractsDeployment CodexContractsDeployment { get; }
public RunningContainer? PrometheusContainer { get; }
public DeploymentMetadata Metadata { get; }
}
public class DeploymentMetadata
{
public DeploymentMetadata(string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI, int blockMN)
public DeploymentMetadata(string name, DateTime startUtc, DateTime finishedUtc, string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI, int blockMN)
{
DeployDateTimeUtc = DateTime.UtcNow;
Name = name;
StartUtc = startUtc;
FinishedUtc = finishedUtc;
KubeNamespace = kubeNamespace;
NumberOfCodexNodes = numberOfCodexNodes;
NumberOfValidators = numberOfValidators;
@@ -38,7 +43,9 @@ namespace CodexPlugin
BlockMN = blockMN;
}
public DateTime DeployDateTimeUtc { get; }
public string Name { get; }
public DateTime StartUtc { get; }
public DateTime FinishedUtc { get; }
public string KubeNamespace { get; }
public int NumberOfCodexNodes { get; }
public int NumberOfValidators { get; }
+8 -1
View File
@@ -13,7 +13,9 @@ namespace CodexPlugin
string GetName();
CodexDebugResponse GetDebugInfo();
CodexDebugPeerResponse GetDebugPeer(string peerId);
CodexDebugBlockExchangeResponse GetDebugBlockExchange();
// These debug methods are not available in master-line Codex. Use only for custom builds.
//CodexDebugBlockExchangeResponse GetDebugBlockExchange();
//CodexDebugRepoStoreResponse[] GetDebugRepoStore();
ContentId UploadFile(TrackedFile file);
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
void ConnectToPeer(ICodexNode node);
@@ -87,6 +89,11 @@ namespace CodexPlugin
return CodexAccess.GetDebugBlockExchange();
}
public CodexDebugRepoStoreResponse[] GetDebugRepoStore()
{
return CodexAccess.GetDebugRepoStore();
}
public ContentId UploadFile(TrackedFile file)
{
using var fileStream = File.OpenRead(file.Filename);
@@ -8,7 +8,7 @@ namespace CodexPlugin
public string? NameOverride { get; set; }
public ILocation Location { get; set; } = KnownLocations.UnspecifiedLocation;
public CodexLogLevel LogLevel { get; set; }
public CodexLogCustomTopics? CustomTopics { get; set; }
public CodexLogCustomTopics? CustomTopics { get; set; } = new CodexLogCustomTopics(CodexLogLevel.Warn, CodexLogLevel.Warn);
public ByteSize? StorageQuota { get; set; }
public bool MetricsEnabled { get; set; }
public MarketplaceInitialConfig? MarketplaceConfig { get; set; }
@@ -205,7 +205,7 @@ namespace CodexPlugin
if (DateTime.UtcNow - waitStart > timeout)
{
FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within timeout. {statusJson}");
FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within {Time.FormatDuration(timeout)} timeout. {statusJson}");
}
}
log.Log($"Contract '{desiredState}'.");
+5
View File
@@ -13,5 +13,10 @@
}
public string Address { get; }
public override string ToString()
{
return Address;
}
}
}
@@ -28,7 +28,7 @@
public override string ToString()
{
return $"{Wei} Wei";
return $"{Eth} Eth";
}
}
+1 -1
View File
@@ -73,7 +73,7 @@ namespace GethPlugin
private NethereumInteraction StartInteraction()
{
var address = StartResult.Container.Address;
var address = StartResult.Container.GetAddress(GethContainerRecipe.HttpPortTag);
var account = Account;
var creator = new NethereumInteractionCreator(log, address.Host, address.Port, account.PrivateKey);
+1 -1
View File
@@ -13,7 +13,7 @@ namespace MetricsPlugin
public MetricsQuery(IPluginTools tools, RunningContainer runningContainer)
{
RunningContainer = runningContainer;
http = tools.CreateHttp(RunningContainer.Address, "api/v1");
http = tools.CreateHttp(RunningContainer.GetAddress(PrometheusContainerRecipe.PortTag), "api/v1");
log = tools.GetLog();
}
@@ -7,11 +7,13 @@ namespace MetricsPlugin
public override string AppName => "prometheus";
public override string Image => "codexstorage/dist-tests-prometheus:latest";
public const string PortTag = "prometheus_port_tag";
protected override void Initialize(StartupConfig startupConfig)
{
var config = startupConfig.Get<PrometheusStartupConfig>();
AddExposedPortAndVar("PROM_PORT");
AddExposedPortAndVar("PROM_PORT", PortTag);
AddEnvVar("PROM_CONFIG", config.PrometheusConfigBase64);
}
}
@@ -92,6 +92,7 @@ namespace ContinuousTests
{
var testDuration = Time.FormatDuration(DateTime.UtcNow - startTime);
var testData = FormatTestRuns(testLoops);
overviewLog.Log("Total duration: " + testDuration);
if (config.TargetDurationSeconds > 0)
{
+1 -1
View File
@@ -106,7 +106,7 @@ namespace ContinuousTests
var effectiveStart = testStart.Subtract(TimeSpan.FromSeconds(30));
if (config.FullContainerLogs)
{
effectiveStart = config.CodexDeployment.Metadata.DeployDateTimeUtc.Subtract(TimeSpan.FromSeconds(30));
effectiveStart = config.CodexDeployment.Metadata.StartUtc.Subtract(TimeSpan.FromSeconds(30));
}
var effectiveEnd = DateTime.UtcNow;
var elasticSearchLogDownloader = new ElasticSearchLogDownloader(entryPoint.Tools, fixtureLog);
+3 -2
View File
@@ -87,7 +87,8 @@ namespace ContinuousTests
{
cancelToken.ThrowIfCancellationRequested();
log.Log($"Checking {n.Container.Name} @ '{n.Container.Address.Host}:{n.Container.Address.Port}'...");
var address = n.Container.GetAddress(CodexContainerRecipe.ApiPortTag);
log.Log($"Checking {n.Container.Name} @ '{address}'...");
if (EnsureOnline(log, n))
{
@@ -95,7 +96,7 @@ namespace ContinuousTests
}
else
{
log.Error($"No response from '{n.Container.Address.Host}'.");
log.Error($"No response from '{address}'.");
pass = false;
}
}
@@ -1,6 +1,7 @@
using CodexPlugin;
using FileUtils;
using Logging;
using Newtonsoft.Json;
using NUnit.Framework;
using Utils;
@@ -21,6 +22,9 @@ namespace ContinuousTests.Tests
[TestMoment(t: Zero)]
public void UploadTestFile()
{
LogBlockExchangeStatus(Nodes[0], "Before upload");
LogBlockExchangeStatus(Nodes[1], "Before upload");
file = FileManager.GenerateFile(size);
LogStoredBytes(Nodes[0]);
@@ -34,9 +38,27 @@ namespace ContinuousTests.Tests
{
TrackedFile? dl = null;
LogBytesPerMillisecond(() => dl = Nodes[1].DownloadContent(cid!));
try
{
LogBytesPerMillisecond(() => dl = Nodes[1].DownloadContent(cid!));
file.AssertIsEqual(dl);
file.AssertIsEqual(dl);
}
catch
{
LogRepoStore(Nodes[0]);
LogRepoStore(Nodes[1]);
throw;
}
LogBlockExchangeStatus(Nodes[0], "After download");
LogBlockExchangeStatus(Nodes[1], "After download");
}
private void LogRepoStore(ICodexNode codexNode)
{
//var response = codexNode.GetDebugRepoStore();
//Log.Log($"{codexNode.GetName()} has {string.Join(",", response.Select(r => r.cid))}");
}
private void LogStoredBytes(ICodexNode node)
@@ -65,5 +87,11 @@ namespace ContinuousTests.Tests
var bytesPerMs = totalBytes / totalMs;
Log.Log($"Bytes per millisecond: {bytesPerMs}");
}
private void LogBlockExchangeStatus(ICodexNode codexNode, string msg)
{
//var response = codexNode.GetDebugBlockExchange();
//Log.Log($"{codexNode.GetName()} {msg}: {JsonConvert.SerializeObject(response)}");
}
}
}
+12 -7
View File
@@ -1,15 +1,18 @@
set -e
replication=5
name=testnamehere
filter=TwoClient
echo "Deploying..."
cd ../../Tools/CodexNetDeployer
for i in $( seq 0 $replication)
do
dotnet run \
--deploy-name=codex-continuous-$name-$i \
--kube-config=/opt/kubeconfig.yaml \
--kube-namespace=codex-continuous-tests-$i \
--deploy-file=codex-deployment-$i.json \
--kube-namespace=codex-continuous-$name-tests-$i \
--deploy-file=codex-deployment-$name-$i.json \
--nodes=5 \
--validators=3 \
--log-level=Trace \
@@ -25,7 +28,7 @@ do
--check-connect=1 \
-y
cp codex-deployment-$i.json ../../Tests/CodexContinuousTests
cp codex-deployment-$name-$i.json ../../Tests/CodexContinuousTests
done
echo "Starting tests..."
cd ../../Tests/CodexContinuousTests
@@ -33,13 +36,15 @@ for i in $( seq 0 $replication)
do
screen -d -m dotnet run \
--kube-config=/opt/kubeconfig.yaml \
--codex-deployment=codex-deployment-$i.json \
--log-path=logs-$i \
--data-path=data-$i \
--codex-deployment=codex-deployment-$name-$i.json \
--log-path=logs-$name-$i \
--data-path=data-$name-$i \
--keep=1 \
--stop=1 \
--filter=TwoClient \
--filter=$filter \
--cleanup=1 \
--full-container-logs=1 \
--target-duration=172800 # 48 hours
sleep 30
done
@@ -1,7 +1,7 @@
using CodexTests;
using DistTestCore;
using FileUtils;
using NUnit.Framework;
using Tests;
using Utils;
namespace CodexLongTests.BasicTests
@@ -1,8 +1,8 @@
using CodexPlugin;
using CodexTests;
using DistTestCore;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using Tests;
using Utils;
namespace CodexLongTests.BasicTests
@@ -1,6 +1,6 @@
using DistTestCore;
using CodexTests;
using DistTestCore;
using NUnit.Framework;
using Tests;
namespace CodexLongTests.BasicTests
{
@@ -1,8 +1,8 @@
using CodexPlugin;
using CodexTests;
using DistTestCore;
using FileUtils;
using NUnit.Framework;
using Tests;
using Utils;
namespace CodexLongTests.BasicTests
@@ -1,6 +1,6 @@
using DistTestCore;
using CodexTests;
using DistTestCore;
using NUnit.Framework;
using Tests;
using Utils;
namespace CodexLongTests.DownloadConnectivityTests
+1 -1
View File
@@ -1,7 +1,7 @@
using CodexPlugin;
using NUnit.Framework;
namespace Tests
namespace CodexTests
{
public class AutoBootstrapDistTest : CodexDistTest
{
@@ -0,0 +1,63 @@
using CodexPlugin;
using NUnit.Framework;
using Utils;
namespace CodexTests.BasicTests
{
[TestFixture]
public class BlockExchangeTests : CodexDistTest
{
[Test]
public void EmptyAfterExchange()
{
var bootstrap = AddCodex(s => s.WithName("bootstrap"));
var node = AddCodex(s => s.WithName("node").WithBootstrapNode(bootstrap));
AssertExchangeIsEmpty(bootstrap, node);
var file = GenerateTestFile(1.MB());
var cid = bootstrap.UploadFile(file);
node.DownloadContent(cid);
AssertExchangeIsEmpty(bootstrap, node);
}
[Test]
public void EmptyAfterExchangeWithBystander()
{
var bootstrap = AddCodex(s => s.WithName("bootstrap"));
var node = AddCodex(s => s.WithName("node").WithBootstrapNode(bootstrap));
var bystander = AddCodex(s => s.WithName("bystander").WithBootstrapNode(bootstrap));
AssertExchangeIsEmpty(bootstrap, node, bystander);
var file = GenerateTestFile(1.MB());
var cid = bootstrap.UploadFile(file);
node.DownloadContent(cid);
AssertExchangeIsEmpty(bootstrap, node, bystander);
}
private void AssertExchangeIsEmpty(params ICodexNode[] nodes)
{
foreach (var node in nodes)
{
// API Call not available in master-line Codex image.
//Time.Retry(() => AssertBlockExchangeIsEmpty(node), nameof(AssertExchangeIsEmpty));
}
}
//private void AssertBlockExchangeIsEmpty(ICodexNode node)
//{
// var msg = $"BlockExchange for {node.GetName()}: ";
// var response = node.GetDebugBlockExchange();
// foreach (var peer in response.peers)
// {
// var activeWants = peer.wants.Where(w => !w.cancel).ToArray();
// Assert.That(activeWants.Length, Is.EqualTo(0), msg + "thinks a peer has active wants.");
// }
// Assert.That(response.taskQueue, Is.EqualTo(0), msg + "has tasks in queue.");
// Assert.That(response.pendingBlocks, Is.EqualTo(0), msg + "has pending blocks.");
//}
}
}
@@ -7,7 +7,7 @@ using MetricsPlugin;
using NUnit.Framework;
using Utils;
namespace Tests.BasicTests
namespace CodexTests.BasicTests
{
[Ignore("Used for debugging continuous tests")]
[TestFixture]
@@ -87,7 +87,7 @@ namespace Tests.BasicTests
//CreatePeerConnectionTestHelpers().AssertFullyConnected(GetAllOnlineCodexNodes());
//CheckRoutingTables(GetAllOnlineCodexNodes());
var node = RandomUtils.PickOneRandom(nodes.ToList());
var node = nodes.ToList().PickOneRandom();
var file = GenerateTestFile(50.MB());
node.UploadFile(file);
+3 -3
View File
@@ -6,7 +6,7 @@ using MetricsPlugin;
using NUnit.Framework;
using Utils;
namespace Tests.BasicTests
namespace CodexTests.BasicTests
{
[TestFixture]
public class ExampleTests : CodexDistTest
@@ -59,7 +59,7 @@ namespace Tests.BasicTests
.WithStorageQuota(11.GB())
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, isValidator: true)
.WithSimulateProofFailures(failEveryNProofs: 3));
AssertBalance(geth, contracts, seller, Is.EqualTo(sellerInitialBalance));
seller.Marketplace.MakeStorageAvailable(
size: 10.GB(),
@@ -72,7 +72,7 @@ namespace Tests.BasicTests
var buyer = AddCodex(s => s
.WithBootstrapNode(seller)
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: buyerInitialBalance));
AssertBalance(geth, contracts, buyer, Is.EqualTo(buyerInitialBalance));
var contentId = buyer.UploadFile(testFile);
@@ -3,7 +3,7 @@ using DistTestCore;
using NUnit.Framework;
using Utils;
namespace Tests.BasicTests
namespace CodexTests.BasicTests
{
// Warning!
// This is a test to check network-isolation in the test-infrastructure.
@@ -3,7 +3,7 @@ using DistTestCore;
using NUnit.Framework;
using Utils;
namespace Tests.BasicTests
namespace CodexTests.BasicTests
{
[TestFixture]
public class OneClientTests : DistTest
@@ -1,8 +1,7 @@
using DistTestCore;
using NUnit.Framework;
using NUnit.Framework;
using Utils;
namespace Tests.BasicTests
namespace CodexTests.BasicTests
{
[TestFixture]
public class ThreeClientTest : AutoBootstrapDistTest
@@ -3,7 +3,7 @@ using DistTestCore;
using NUnit.Framework;
using Utils;
namespace Tests.BasicTests
namespace CodexTests.BasicTests
{
[TestFixture]
public class TwoClientTests : DistTest
+1 -1
View File
@@ -9,7 +9,7 @@ using GethPlugin;
using NUnit.Framework;
using NUnit.Framework.Constraints;
namespace Tests
namespace CodexTests
{
public class CodexDistTest : DistTest
{
@@ -1,4 +1,5 @@
using CodexContractsPlugin;
using CodexTests;
using GethPlugin;
using NUnit.Framework;
using Utils;
+1 -1
View File
@@ -3,7 +3,7 @@ using Logging;
using MetricsPlugin;
using NUnit.Framework.Constraints;
namespace Tests
namespace CodexTests
{
public static class MetricsAccessExtensions
{
+1 -1
View File
@@ -1,6 +1,6 @@
using NUnit.Framework;
[assembly: LevelOfParallelism(1)]
namespace Tests
namespace CodexTests
{
}
@@ -1,4 +1,5 @@
using CodexPlugin;
using CodexTests;
using NUnit.Framework;
namespace Tests.PeerDiscoveryTests
@@ -1,4 +1,5 @@
using CodexContractsPlugin;
using CodexTests;
using GethPlugin;
using NUnit.Framework;
-32
View File
@@ -1,32 +0,0 @@
using Core;
namespace DistTestCore
{
public class LongTimeSet : ITimeSet
{
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromHours(2);
}
public TimeSpan HttpCallRetryTime()
{
return TimeSpan.FromHours(5);
}
public TimeSpan HttpCallRetryDelay()
{
return TimeSpan.FromSeconds(2);
}
public TimeSpan WaitForK8sServiceDelay()
{
return TimeSpan.FromSeconds(10);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromMinutes(15);
}
}
}
+35
View File
@@ -0,0 +1,35 @@
using Discord.WebSocket;
namespace BiblioTech
{
public class AdminChecker
{
private SocketGuild guild = null!;
private ulong[] adminIds = Array.Empty<ulong>();
private DateTime lastUpdate = DateTime.MinValue;
public void SetGuild(SocketGuild guild)
{
this.guild = guild;
}
public bool IsUserAdmin(ulong userId)
{
if (ShouldUpdate()) UpdateAdminIds();
return adminIds.Contains(userId);
}
private bool ShouldUpdate()
{
return !adminIds.Any() || (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10);
}
private void UpdateAdminIds()
{
lastUpdate = DateTime.UtcNow;
var adminRole = guild.Roles.Single(r => r.Name == Program.Config.AdminRoleName);
adminIds = adminRole.Members.Select(m => m.Id).ToArray();
}
}
}
+66
View File
@@ -0,0 +1,66 @@
using Discord.WebSocket;
using Discord;
using BiblioTech.Commands;
namespace BiblioTech
{
public abstract class BaseCommand
{
public abstract string Name { get; }
public abstract string StartingMessage { get; }
public abstract string Description { get; }
public virtual CommandOption[] Options
{
get
{
return Array.Empty<CommandOption>();
}
}
public async Task SlashCommandHandler(SocketSlashCommand command)
{
if (command.CommandName != Name) return;
try
{
await command.RespondAsync(StartingMessage);
await Invoke(command);
}
catch (Exception ex)
{
await command.FollowupAsync("Something failed while trying to do that...");
Console.WriteLine(ex);
}
}
protected abstract Task Invoke(SocketSlashCommand command);
protected bool IsSenderAdmin(SocketSlashCommand command)
{
return Program.AdminChecker.IsUserAdmin(command.User.Id);
}
protected ulong GetUserId(UserOption userOption, SocketSlashCommand command)
{
var targetUser = userOption.GetOptionUserId(command);
if (IsSenderAdmin(command) && targetUser != null) return targetUser.Value;
return command.User.Id;
}
}
public class CommandOption
{
public CommandOption(string name, string description, ApplicationCommandOptionType type, bool isRequired)
{
Name = name;
Description = description;
Type = type;
IsRequired = isRequired;
}
public string Name { get; }
public string Description { get; }
public ApplicationCommandOptionType Type { get; }
public bool IsRequired { get; }
}
}
+45
View File
@@ -0,0 +1,45 @@
using CodexContractsPlugin;
using Core;
using Discord.WebSocket;
using GethPlugin;
namespace BiblioTech
{
public abstract class BaseNetCommand : BaseCommand
{
private readonly DeploymentsFilesMonitor monitor;
private readonly CoreInterface ci;
public BaseNetCommand(DeploymentsFilesMonitor monitor, CoreInterface ci)
{
this.monitor = monitor;
this.ci = ci;
}
protected override async Task Invoke(SocketSlashCommand command)
{
var deployments = monitor.GetDeployments();
if (deployments.Length == 0)
{
await command.FollowupAsync("No deployments are currently available.");
return;
}
if (deployments.Length > 1)
{
await command.FollowupAsync("Multiple deployments are online. I don't know which one to pick!");
return;
}
var codexDeployment = deployments.Single();
var gethDeployment = codexDeployment.GethDeployment;
var contractsDeployment = codexDeployment.CodexContractsDeployment;
var gethNode = ci.WrapGethDeployment(gethDeployment);
var contracts = ci.WrapCodexContractsDeployment(contractsDeployment);
await Execute(command, gethNode, contracts);
}
protected abstract Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts);
}
}
+16
View File
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.12.0" />
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
</ItemGroup>
</Project>
+63
View File
@@ -0,0 +1,63 @@
using Discord.Net;
using Discord.WebSocket;
using Discord;
using Newtonsoft.Json;
namespace BiblioTech
{
public class CommandHandler
{
private readonly DiscordSocketClient client;
private readonly BaseCommand[] commands;
public CommandHandler(DiscordSocketClient client, params BaseCommand[] commands)
{
this.client = client;
this.commands = commands;
client.Ready += Client_Ready;
client.SlashCommandExecuted += SlashCommandHandler;
}
private async Task Client_Ready()
{
var guild = client.Guilds.Single(g => g.Name == Program.Config.ServerName);
Program.AdminChecker.SetGuild(guild);
var builders = commands.Select(c =>
{
var builder = new SlashCommandBuilder()
.WithName(c.Name)
.WithDescription(c.Description);
foreach (var option in c.Options)
{
builder.AddOption(option.Name, option.Type, option.Description, isRequired: option.IsRequired);
}
return builder;
});
try
{
foreach (var builder in builders)
{
await guild.CreateApplicationCommandAsync(builder.Build());
}
}
catch (HttpException exception)
{
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
Console.WriteLine(json);
}
}
private async Task SlashCommandHandler(SocketSlashCommand command)
{
foreach (var cmd in commands)
{
await cmd.SlashCommandHandler(command);
}
}
}
}
@@ -0,0 +1,35 @@
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class ClearUserAssociationCommand : BaseCommand
{
private readonly UserOption user = new UserOption(
description: "User to clear Eth address for.",
isRequired: true);
public override string Name => "clear";
public override string StartingMessage => "Hold on...";
public override string Description => "Admin only. Clears current Eth address for a user, allowing them to set a new one.";
public override CommandOption[] Options => new[] { user };
protected override async Task Invoke(SocketSlashCommand command)
{
if (!IsSenderAdmin(command))
{
await command.FollowupAsync("You're not an admin.");
return;
}
var userId = user.GetOptionUserId(command);
if (userId == null)
{
await command.FollowupAsync("Failed to get user ID");
return;
}
Program.UserRepo.ClearUserAssociatedAddress(userId.Value);
await command.FollowupAsync("Done."); ;
}
}
}
@@ -0,0 +1,38 @@
using CodexPlugin;
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class DeploymentsCommand : BaseCommand
{
private readonly DeploymentsFilesMonitor monitor;
public DeploymentsCommand(DeploymentsFilesMonitor monitor)
{
this.monitor = monitor;
}
public override string Name => "deployments";
public override string StartingMessage => "Fetching deployments information...";
public override string Description => "Lists active TestNet deployments";
protected override async Task Invoke(SocketSlashCommand command)
{
var deployments = monitor.GetDeployments();
if (!deployments.Any())
{
await command.FollowupAsync("No deployments available.");
return;
}
await command.FollowupAsync($"Deployments: {string.Join(", ", deployments.Select(FormatDeployment))}");
}
private string FormatDeployment(CodexDeployment deployment)
{
var m = deployment.Metadata;
return $"{m.Name} ({m.StartUtc.ToString("o")})";
}
}
}
@@ -0,0 +1,43 @@
using Discord.WebSocket;
using GethPlugin;
using Nethereum.Util;
namespace BiblioTech.Commands
{
public class EthAddressOption : CommandOption
{
public EthAddressOption()
: base(name: "ethaddress",
description: "Ethereum address starting with '0x'.",
type: Discord.ApplicationCommandOptionType.String,
isRequired: true)
{
}
public async Task<EthAddress?> Parse(SocketSlashCommand command)
{
var ethOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name);
if (ethOptionData == null)
{
await command.FollowupAsync("EthAddress option not received.");
return null;
}
var ethAddressStr = ethOptionData.Value as string;
if (string.IsNullOrEmpty(ethAddressStr))
{
await command.FollowupAsync("EthAddress is null or empty.");
return null;
}
if (!AddressUtil.Current.IsValidAddressLength(ethAddressStr) ||
!AddressUtil.Current.IsValidEthereumAddressHexFormat(ethAddressStr) ||
!AddressUtil.Current.IsChecksumAddress(ethAddressStr))
{
await command.FollowupAsync("EthAddress is not valid.");
return null;
}
return new EthAddress(ethAddressStr);
}
}
}
@@ -0,0 +1,42 @@
using CodexContractsPlugin;
using Core;
using Discord.WebSocket;
using GethPlugin;
namespace BiblioTech.Commands
{
public class GetBalanceCommand : BaseNetCommand
{
private readonly UserAssociateCommand userAssociateCommand;
private readonly UserOption optionalUser = new UserOption(
description: "If set, get balance for another user. (Optional, admin-only)",
isRequired: false);
public GetBalanceCommand(DeploymentsFilesMonitor monitor, CoreInterface ci, UserAssociateCommand userAssociateCommand)
: base(monitor, ci)
{
this.userAssociateCommand = userAssociateCommand;
}
public override string Name => "balance";
public override string StartingMessage => "Fetching balance...";
public override string Description => "Shows Eth and TestToken balance of an eth address.";
public override CommandOption[] Options => new[] { optionalUser };
protected override async Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts)
{
var userId = GetUserId(optionalUser, command);
var addr = Program.UserRepo.GetCurrentAddressForUser(userId);
if (addr == null)
{
await command.FollowupAsync($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
return;
}
var eth = gethNode.GetEthBalance(addr);
var testTokens = contracts.GetTestTokenBalance(gethNode, addr);
await command.FollowupAsync($"{command.User.Username} has {eth} and {testTokens}.");
}
}
}
+85
View File
@@ -0,0 +1,85 @@
using CodexContractsPlugin;
using Core;
using Discord.WebSocket;
using GethPlugin;
namespace BiblioTech.Commands
{
public class MintCommand : BaseNetCommand
{
private readonly Ether defaultEthToSend = 10.Eth();
private readonly TestToken defaultTestTokensToMint = 1024.TestTokens();
private readonly UserOption optionalUser = new UserOption(
description: "If set, mint tokens for this user. (Optional, admin-only)",
isRequired: false);
private readonly UserAssociateCommand userAssociateCommand;
public MintCommand(DeploymentsFilesMonitor monitor, CoreInterface ci, UserAssociateCommand userAssociateCommand)
: base(monitor, ci)
{
this.userAssociateCommand = userAssociateCommand;
}
public override string Name => "mint";
public override string StartingMessage => "Minting some tokens...";
public override string Description => "Mint some TestTokens and send some Eth to the user if their balance is low.";
public override CommandOption[] Options => new[] { optionalUser };
protected override async Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts)
{
var userId = GetUserId(optionalUser, command);
var addr = Program.UserRepo.GetCurrentAddressForUser(userId);
if (addr == null)
{
await command.FollowupAsync($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
return;
}
var report = new List<string>();
var sentEth = ProcessEth(gethNode, addr, report);
var mintedTokens = ProcessTokens(gethNode, contracts, addr, report);
Program.UserRepo.AddMintEventForUser(userId, addr, sentEth, mintedTokens);
await command.FollowupAsync(string.Join(Environment.NewLine, report));
}
private TestToken ProcessTokens(IGethNode gethNode, ICodexContracts contracts, EthAddress addr, List<string> report)
{
if (ShouldMintTestTokens(gethNode, contracts, addr))
{
contracts.MintTestTokens(gethNode, addr, defaultTestTokensToMint);
report.Add($"Minted {defaultTestTokensToMint}.");
return defaultTestTokensToMint;
}
report.Add("TestToken balance over threshold.");
return 0.TestTokens();
}
private Ether ProcessEth(IGethNode gethNode, EthAddress addr, List<string> report)
{
if (ShouldSendEth(gethNode, addr))
{
gethNode.SendEth(addr, defaultEthToSend);
report.Add($"Sent {defaultEthToSend}.");
return defaultEthToSend;
}
report.Add("Eth balance is over threshold.");
return 0.Eth();
}
private bool ShouldMintTestTokens(IGethNode gethNode, ICodexContracts contracts, EthAddress addr)
{
var testTokens = contracts.GetTestTokenBalance(gethNode, addr);
return testTokens.Amount < 64m;
}
private bool ShouldSendEth(IGethNode gethNode, EthAddress addr)
{
var eth = gethNode.GetEthBalance(addr);
return eth.Eth < 1.0m;
}
}
}
@@ -0,0 +1,35 @@
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class ReportHistoryCommand : BaseCommand
{
private readonly UserOption user = new UserOption(
description: "User to report history for.",
isRequired: true);
public override string Name => "report";
public override string StartingMessage => "Getting that data...";
public override string Description => "Admin only. Reports bot-interaction history for a user.";
public override CommandOption[] Options => new[] { user };
protected override async Task Invoke(SocketSlashCommand command)
{
if (!IsSenderAdmin(command))
{
await command.FollowupAsync("You're not an admin.");
return;
}
var userId = user.GetOptionUserId(command);
if (userId == null)
{
await command.FollowupAsync("Failed to get user ID");
return;
}
var report = Program.UserRepo.GetInteractionReport(userId.Value);
await command.FollowupAsync(string.Join(Environment.NewLine, report));
}
}
}
@@ -0,0 +1,34 @@
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class UserAssociateCommand : BaseCommand
{
private readonly EthAddressOption ethOption = new EthAddressOption();
private readonly UserOption optionalUser = new UserOption(
description: "If set, associates Ethereum address for another user. (Optional, admin-only)",
isRequired: false);
public override string Name => "set";
public override string StartingMessage => "hold on...";
public override string Description => "Associates a Discord user with an Ethereum address.";
public override CommandOption[] Options => new CommandOption[] { ethOption, optionalUser };
protected override async Task Invoke(SocketSlashCommand command)
{
var userId = GetUserId(optionalUser, command);
var data = await ethOption.Parse(command);
if (data == null) return;
var currentAddress = Program.UserRepo.GetCurrentAddressForUser(userId);
if (currentAddress != null && !IsSenderAdmin(command))
{
await command.FollowupAsync($"You've already set your Ethereum address to {currentAddress}.");
return;
}
Program.UserRepo.AssociateUserWithAddress(userId, data);
await command.FollowupAsync("Done! Thank you for joining the test net!");
}
}
}
+22
View File
@@ -0,0 +1,22 @@
using Discord;
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class UserOption : CommandOption
{
public UserOption(string description, bool isRequired)
: base("user", description, ApplicationCommandOptionType.User, isRequired)
{
}
public ulong? GetOptionUserId(SocketSlashCommand command)
{
var userOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name);
if (userOptionData == null) return null;
var user = userOptionData.Value as IUser;
if (user == null) return null;
return user.Id;
}
}
}
+22
View File
@@ -0,0 +1,22 @@
using ArgsUniform;
namespace BiblioTech
{
public class Configuration
{
[Uniform("token", "t", "TOKEN", true, "Discord Application Token")]
public string ApplicationToken { get; set; } = string.Empty;
[Uniform("server-name", "sn", "SERVERNAME", true, "Name of the Discord server")]
public string ServerName { get; set; } = string.Empty;
[Uniform("endpoints", "e", "ENDPOINTS", false, "Path where endpoint JSONs are located. Also accepts codex-deployment JSONs.")]
public string EndpointsPath { get; set; } = "endpoints";
[Uniform("userdata", "u", "USERDATA", false, "Path where user data files will be saved.")]
public string UserDataPath { get; set; } = "userdata";
[Uniform("admin-role", "a", "ADMINROLE", true, "Name of the Discord server admin role")]
public string AdminRoleName { get; set; } = string.Empty;
}
}
@@ -0,0 +1,51 @@
using CodexPlugin;
using Newtonsoft.Json;
namespace BiblioTech
{
public class DeploymentsFilesMonitor
{
private DateTime lastUpdate = DateTime.MinValue;
private CodexDeployment[] deployments = Array.Empty<CodexDeployment>();
public CodexDeployment[] GetDeployments()
{
if (ShouldUpdate()) UpdateDeployments();
return deployments;
}
private void UpdateDeployments()
{
lastUpdate = DateTime.UtcNow;
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 = files.Select(ProcessFile).Where(d => d != null).Cast<CodexDeployment>().ToArray();
}
private CodexDeployment? ProcessFile(string filename)
{
try
{
var lines = string.Join(" ", File.ReadAllLines(filename));
return JsonConvert.DeserializeObject<CodexDeployment>(lines);
}
catch
{
return null;
}
}
private bool ShouldUpdate()
{
return !deployments.Any() || (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10);
}
}
}
+79
View File
@@ -0,0 +1,79 @@
using ArgsUniform;
using BiblioTech.Commands;
using Core;
using Discord;
using Discord.WebSocket;
using Logging;
namespace BiblioTech
{
public class Program
{
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();
public static Task Main(string[] args)
{
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
Config = uniformArgs.Parse();
if (!Directory.Exists(Config.UserDataPath))
{
Directory.CreateDirectory(Config.UserDataPath);
}
return new Program().MainAsync();
}
public async Task MainAsync()
{
Console.WriteLine("Starting Codex Discord Bot...");
client = new DiscordSocketClient();
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 monitor = new DeploymentsFilesMonitor();
var ci = entryPoint.CreateInterface();
var associateCommand = new UserAssociateCommand();
var handler = new CommandHandler(client,
new ClearUserAssociationCommand(),
new GetBalanceCommand(monitor, ci, associateCommand),
new MintCommand(monitor, ci, associateCommand),
new ReportHistoryCommand(),
associateCommand,
new DeploymentsCommand(monitor)
);
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
await client.StartAsync();
Console.WriteLine("Running...");
await Task.Delay(-1);
}
private static void PrintHelp()
{
Console.WriteLine("BiblioTech - Codex Discord Bot");
}
private Task Log(LogMessage msg)
{
Console.WriteLine(msg.ToString());
return Task.CompletedTask;
}
}
}
+165
View File
@@ -0,0 +1,165 @@
using CodexContractsPlugin;
using GethPlugin;
using Newtonsoft.Json;
namespace BiblioTech
{
public class UserRepo
{
private readonly object repoLock = new object();
public void AssociateUserWithAddress(ulong discordId, EthAddress address)
{
lock (repoLock)
{
SetUserAddress(discordId, address);
}
}
public void ClearUserAssociatedAddress(ulong discordId)
{
lock (repoLock)
{
SetUserAddress(discordId, null);
}
}
public void AddMintEventForUser(ulong discordId, EthAddress usedAddress, Ether eth, TestToken tokens)
{
lock (repoLock)
{
var user = GetOrCreate(discordId);
user.MintEvents.Add(new UserMintEvent(DateTime.UtcNow, usedAddress, eth, tokens));
SaveUser(user);
}
}
public EthAddress? GetCurrentAddressForUser(ulong discordId)
{
lock (repoLock)
{
return GetOrCreate(discordId).CurrentAddress;
}
}
public string[] GetInteractionReport(ulong discordId)
{
var result = new List<string>();
lock (repoLock)
{
var filename = GetFilename(discordId);
if (!File.Exists(filename))
{
result.Add("User has not joined the test net.");
}
else
{
var user = JsonConvert.DeserializeObject<User>(File.ReadAllText(filename));
if (user == null)
{
result.Add("Failed to load user records.");
}
else
{
result.Add("User joined on " + user.CreatedUtc.ToString("o"));
result.Add("Current address: " + user.CurrentAddress);
foreach (var ae in user.AssociateEvents)
{
result.Add($"{ae.Utc.ToString("o")} - Address set to: {ae.NewAddress}");
}
foreach (var me in user.MintEvents)
{
result.Add($"{me.Utc.ToString("o")} - Minted {me.EthReceived} and {me.TestTokensMinted} to {me.UsedAddress}.");
}
}
}
}
return result.ToArray();
}
private void SetUserAddress(ulong discordId, EthAddress? address)
{
var user = GetOrCreate(discordId);
user.CurrentAddress = address;
user.AssociateEvents.Add(new UserAssociateAddressEvent(DateTime.UtcNow, address));
SaveUser(user);
}
private User GetOrCreate(ulong discordId)
{
var filename = GetFilename(discordId);
if (!File.Exists(filename))
{
return CreateAndSaveNewUser(discordId);
}
return JsonConvert.DeserializeObject<User>(File.ReadAllText(filename))!;
}
private User CreateAndSaveNewUser(ulong discordId)
{
var newUser = new User(discordId, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>());
SaveUser(newUser);
return newUser;
}
private void SaveUser(User user)
{
var filename = GetFilename(user.DiscordId);
if (File.Exists(filename)) File.Delete(filename);
File.WriteAllText(filename, JsonConvert.SerializeObject(user));
}
private static string GetFilename(ulong discordId)
{
return Path.Combine(Program.Config.UserDataPath, discordId.ToString() + ".json");
}
}
public class User
{
public User(ulong discordId, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents)
{
DiscordId = discordId;
CreatedUtc = createdUtc;
CurrentAddress = currentAddress;
AssociateEvents = associateEvents;
MintEvents = mintEvents;
}
public ulong DiscordId { get; }
public DateTime CreatedUtc { get; }
public EthAddress? CurrentAddress { get; set; }
public List<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> MintEvents { get; }
}
public class UserAssociateAddressEvent
{
public UserAssociateAddressEvent(DateTime utc, EthAddress? newAddress)
{
Utc = utc;
NewAddress = newAddress;
}
public DateTime Utc { get; }
public EthAddress? NewAddress { get; }
}
public class UserMintEvent
{
public UserMintEvent(DateTime utc, EthAddress usedAddress, Ether ethReceived, TestToken testTokensMinted)
{
Utc = utc;
UsedAddress = usedAddress;
EthReceived = ethReceived;
TestTokensMinted = testTokensMinted;
}
public DateTime Utc { get; }
public EthAddress UsedAddress { get; }
public Ether EthReceived { get; }
public TestToken TestTokensMinted { get; }
}
}
+2
View File
@@ -0,0 +1,2 @@
docker build -f docker/Dockerfile -t thatbenbierens/codex-discordbot:initial ../..
docker push thatbenbierens/codex-discordbot:initial
+7
View File
@@ -0,0 +1,7 @@
FROM mcr.microsoft.com/dotnet/sdk:7.0
WORKDIR app
COPY ./Tools/BiblioTech ./Tools/BiblioTech
COPY ./Framework ./Framework
COPY ./ProjectPlugins ./ProjectPlugins
CMD ["dotnet", "run", "--project", "Tools/BiblioTech"]
@@ -0,0 +1,7 @@
services:
bibliotech-discordbot:
image: thatbenbierens/codex-discordbot:initial
environment:
- TOKEN=tokenplz
- SERVERNAME=ThatBen's server
- ADMINROLE=adminers
+3
View File
@@ -8,6 +8,9 @@ namespace CodexNetDeployer
public const int SecondsIn1Day = 24 * 60 * 60;
public const int TenMinutes = 10 * 60;
[Uniform("deploy-name", "nm", "DEPLOYNAME", false, "Name of the deployment. (optional)")]
public string DeploymentName { get; set; } = "unnamed";
[Uniform("kube-config", "kc", "KUBECONFIG", false, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
public string KubeConfigFile { get; set; } = "null";
+36 -4
View File
@@ -51,6 +51,7 @@ namespace CodexNetDeployer
localCodexBuilder.Build();
Log("Initializing...");
var startUtc = DateTime.UtcNow;
var ci = entryPoint.CreateInterface();
Log("Deploying Geth instance...");
@@ -78,7 +79,7 @@ namespace CodexNetDeployer
CheckContainerRestarts(startResults);
var codexContainers = startResults.Select(s => s.CodexNode.Container).ToArray();
return new CodexDeployment(codexContainers, gethDeployment, metricsService, CreateMetadata());
return new CodexDeployment(codexContainers, gethDeployment, contractsDeployment, metricsService, CreateMetadata(startUtc));
}
private EntryPoint CreateEntryPoint(ILog log)
@@ -87,11 +88,11 @@ namespace CodexNetDeployer
var configuration = new KubernetesWorkflow.Configuration(
kubeConfig,
operationTimeout: TimeSpan.FromSeconds(300),
operationTimeout: TimeSpan.FromMinutes(10),
retryDelay: TimeSpan.FromSeconds(10),
kubernetesNamespace: config.KubeNamespace);
var result = new EntryPoint(log, configuration, string.Empty);
var result = new EntryPoint(log, configuration, string.Empty, new FastHttpTimeSet());
configuration.Hooks = new K8sHook(config.TestsTypePodLabel, result.GetPluginMetadata());
return result;
@@ -147,9 +148,12 @@ namespace CodexNetDeployer
}
}
private DeploymentMetadata CreateMetadata()
private DeploymentMetadata CreateMetadata(DateTime startUtc)
{
return new DeploymentMetadata(
name: config.DeploymentName,
startUtc: startUtc,
finishedUtc: DateTime.UtcNow,
kubeNamespace: config.KubeNamespace,
numberOfCodexNodes: config.NumberOfCodexNodes!.Value,
numberOfValidators: config.NumberOfValidators!.Value,
@@ -169,4 +173,32 @@ namespace CodexNetDeployer
Console.WriteLine(msg);
}
}
public class FastHttpTimeSet : ITimeSet
{
public TimeSpan HttpCallRetryDelay()
{
return TimeSpan.FromSeconds(2);
}
public int HttpMaxNumberOfRetries()
{
return 2;
}
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromSeconds(10);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromMinutes(10);
}
public TimeSpan WaitForK8sServiceDelay()
{
return TimeSpan.FromSeconds(30);
}
}
}
@@ -1,4 +1,5 @@
dotnet run \
--deploy-name=codex-continuous-test-deployment \
--kube-config=/opt/kubeconfig.yaml \
--kube-namespace=codex-continuous-tests \
--deploy-file=codex-deployment.json \
+7
View File
@@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistTestCore", "Tests\DistT
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexNetDeployer", "Tools\CodexNetDeployer\CodexNetDeployer.csproj", "{3417D508-E2F4-4974-8988-BB124046D9E2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BiblioTech", "Tools\BiblioTech\BiblioTech.csproj", "{078ABA6D-A04E-4F62-A44C-EA66F1B66548}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -113,6 +115,10 @@ Global
{3417D508-E2F4-4974-8988-BB124046D9E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3417D508-E2F4-4974-8988-BB124046D9E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3417D508-E2F4-4974-8988-BB124046D9E2}.Release|Any CPU.Build.0 = Release|Any CPU
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Debug|Any CPU.Build.0 = Debug|Any CPU
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Release|Any CPU.ActiveCfg = Release|Any CPU
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -134,6 +140,7 @@ Global
{562EC700-6984-4C9A-83BF-3BF4E3EB1A64} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
{E849B7BA-FDCC-4CFF-998F-845ED2F1BF40} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
{3417D508-E2F4-4974-8988-BB124046D9E2} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{078ABA6D-A04E-4F62-A44C-EA66F1B66548} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}