Compare commits

...
Author SHA1 Message Date
benbierens f9408ab3b5 Merge branch 'feature/public-testnet-deploying' into automated-teststarter 2023-11-03 14:46:17 +01:00
benbierens 438c4271e1 Updates response message instead of deleting it. 2023-11-02 15:04:53 +01:00
benbierens fe11a97458 Fixes initialization issue for deployments monitor 2023-11-02 14:25:17 +01:00
benbierens b8774e7273 Adds SPR-fetching command. 2023-11-02 12:30:48 +01:00
benbierens bc51fc2e30 Fixes faulty persistent volume claim creation 2023-11-02 11:32:24 +01:00
benbierens 5241144e99 Locks up deploy-and-run script after tests are started. 2023-11-01 10:27:31 +01:00
benbierens 49300273e0 Cleanup continuous tests nicely even if no tests were selected for running. 2023-10-31 15:33:00 +01:00
benbierens fcb5a527a9 Fixes pod-readback labeling issue. 2023-10-31 15:04:59 +01:00
benbierens a6f7bc2393 Removes knownPods class. 2023-10-31 14:48:16 +01:00
benbierens ac07327d77 Upgrades volume support for use with deploy-and-run container 2023-10-31 14:20:50 +01:00
benbierens b5e5570145 Creates volume mount for kubeconfig file 2023-10-31 11:38:54 +01:00
benbierens c348ca9849 sets up test starter tool 2023-10-31 11:22:59 +01:00
benbierens bfdbebb36e Adds deploy-and-run plugin 2023-10-31 11:15:08 +01:00
benbierens e87f255f48 Creates dockerimage that will deploy-and-run from environment args 2023-10-31 11:01:10 +01:00
benbierens 85ad0b414f Cleans up some gethnode arguments. 2023-10-30 13:30:14 +01:00
benbierens 901259d0fa Adds debug-peer command 2023-10-29 10:40:01 +01:00
benbierens 62cc9e0587 Formats the json response 2023-10-29 10:02:51 +01:00
benbierens d7827a4531 Forgot to connect command handler. 2023-10-29 09:47:34 +01:00
benbierens f17d123af8 Adds netinfo command 2023-10-29 09:25:50 +01:00
benbierens d5b87253ae Explicitly turn off auto-nat. 2023-10-27 11:21:43 +02:00
benbierens 529f923595 Sleep before fetching debug info. 2023-10-27 10:56:30 +02:00
benbierens a362eb1e96 Separate port for geth listening 2023-10-27 08:33:07 +02:00
benbierens 9b1ab3185f Idiot developer forgets to use variable. 2023-10-25 14:42:53 +02:00
benbierens 939eed544d Fixes error where empty string is passed as datapath 2023-10-25 14:33:47 +02:00
benbierens 3914d58a6a Sets up support for UDP ports and applies them to discovery ports. 2023-10-25 14:23:07 +02:00
benbierens 4102ce0a04 Moves discord bot datafiles to volume 2023-10-25 11:53:33 +02:00
benbierens 69296577f8 adds Whois command 2023-10-25 11:25:27 +02:00
benbierens bd9fc3a3cf Ephemeral messages. 2023-10-25 10:54:26 +02:00
benbierens f148598a21 Restrict admin commands to admin channel 2023-10-25 10:38:21 +02:00
benbierens 6e82d6b1e6 Disallow duplicate eth addresses 2023-10-25 10:25:00 +02:00
benbierens 6d44a0ccfc Important user feedback message update 2023-10-25 09:54:08 +02:00
benbierens fb70daa2e9 Adds discord bot to deploy script 2023-10-25 09:40:47 +02:00
benbierens 14e8222dfe Adds codex debug-info to deployment json. Needed for bootstrapping later on. 2023-10-25 09:14:35 +02:00
benbierens dc0edce251 Moves continuous-tests default log path to persistent volume 2023-10-25 08:56:48 +02:00
benbierens b8b9e6b997 Successful deployment add and remove. 2023-10-24 15:45:40 +02:00
benbierens 29672ece68 remove command 2023-10-24 15:43:07 +02:00
benbierens a0461a446e cleanup of admin commands 2023-10-24 15:25:45 +02:00
benbierens 5aff8c6f6d setting up subcommands. 2023-10-24 14:07:15 +02:00
benbierens dfe477d192 Removes codexdeployment from input of discord bot. 2023-10-24 13:48:24 +02:00
benbierens ade08a27fe Wires codex bot into deployer. 2023-10-24 10:29:07 +02:00
benbierens cc8a860f41 First attempt to send deployment json into discord bot container. 2023-10-24 10:17:57 +02:00
benbierens d4522f0d9c Sets up bot container recipe 2023-10-24 10:06:07 +02:00
benbierens b74349cc68 Setting up codex discord bot plugin 2023-10-24 09:51:29 +02:00
benbierens 49f6c7e37e Merge branch 'master' into feature/public-testnet-deploying 2023-10-24 09:42:37 +02:00
benbierens e073f7a881 Fixes locking and retrying in http class 2023-10-24 09:41:37 +02:00
benbierens 3326c42f7a Switching to ip + port internal addresses 2023-10-24 09:03:56 +02:00
benbierens eb70fe612b trying with podIP for internal addresses. 2023-10-23 16:12:45 +02:00
benbierens b3ba39b2e5 Debug logging for nethereum interactions 2023-10-23 16:04:50 +02:00
benbierens abb9560b6d Better logging of k8s exceptions 2023-10-23 15:49:14 +02:00
benbierens 2fae9505d6 attempt to fix contract deployment in testnet mode 2023-10-23 15:28:20 +02:00
benbierens b2b338d0a5 how did this not get commited? 2023-10-23 14:25:13 +02:00
benbierens 0e087c6fee Logs continuous test config at startup. 2023-10-23 14:17:52 +02:00
benbierens 58816430e3 Adds internal ports to container-port array as long as they have tags. 2023-10-23 14:05:08 +02:00
benbierens 5a021a4bfe Capture only first Geth account. 2023-10-23 13:36:20 +02:00
benbierens a68e849768 Wires up geth public IP and discovery port. 2023-10-23 13:19:57 +02:00
benbierens 020865f5c0 Wires up deployer config 2023-10-23 13:08:49 +02:00
benbierens 4280f910ae Makes making storage available optional. 2023-10-23 12:52:47 +02:00
benbierens ec8a041257 Setting up deployer config for public testnet deploying. 2023-10-23 12:43:50 +02:00
benbierens bcb690d143 Adds option to deploy codex in a public test net. 2023-10-23 12:33:48 +02:00
benbierens 840e794761 wip: updating codex container recipe for public deployment 2023-10-23 11:30:54 +02:00
benbierens 854325f10c Adds option to deploy geth for public testnet. 2023-10-23 11:23:44 +02:00
benbierens 7a6d7d787b Splits up metrics-scraper from metrics-endpoints configuration in deployer. 2023-10-23 10:57:59 +02:00
benbierens cc2e8d5992 Merge branch 'app/discord-bot' 2023-10-23 10:32:23 +02:00
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 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
99 changed files with 2828 additions and 358 deletions
+69 -55
View File
@@ -13,15 +13,15 @@ namespace Core
T HttpGetJson<T>(string route);
TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body);
string HttpPostJson<TRequest>(string route, TRequest body);
string HttpPostString(string route, string body);
TResponse HttpPostString<TResponse>(string route, string body);
string HttpPostStream(string route, Stream stream);
Stream HttpGetStream(string route);
T TryJsonDeserialize<T>(string json);
T Deserialize<T>(string json);
}
internal class Http : IHttp
{
private static readonly object httpLock = new object();
private readonly ILog log;
private readonly ITimeSet timeSet;
private readonly Address address;
@@ -48,70 +48,60 @@ namespace Core
public string HttpGetString(string route)
{
return Retry(() =>
return LockRetry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "");
var result = Time.Wait(client.GetAsync(url));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str; ;
return GetString(route);
}, $"HTTP-GET:{route}");
}
public T HttpGetJson<T>(string route)
{
var json = HttpGetString(route);
return TryJsonDeserialize<T>(json);
return LockRetry(() =>
{
var json = GetString(route);
return Deserialize<T>(json);
}, $"HTTP-GET:{route}");
}
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
{
var response = PostJson(route, body);
var json = Time.Wait(response.Content.ReadAsStringAsync());
if (!response.IsSuccessStatusCode)
return LockRetry(() =>
{
throw new HttpRequestException(json);
}
Log(GetUrl() + route, json);
return TryJsonDeserialize<TResponse>(json);
var response = PostJson(route, body);
var json = Time.Wait(response.Content.ReadAsStringAsync());
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(json);
}
Log(GetUrl() + route, json);
return Deserialize<TResponse>(json);
}, $"HTTP-POST-JSON: {route}");
}
public string HttpPostJson<TRequest>(string route, TRequest body)
{
var response = PostJson(route, body);
return Time.Wait(response.Content.ReadAsStringAsync());
}
public string HttpPostString(string route, string body)
{
return Retry(() =>
return LockRetry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, body);
var content = new StringContent(body);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var result = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}, $"HTTP-POST-STRING: {route}");
var response = PostJson(route, body);
return Time.Wait(response.Content.ReadAsStringAsync());
}, $"HTTP-POST-JSON: {route}");
}
public TResponse HttpPostString<TResponse>(string route, string body)
{
var response = HttpPostString(route, body);
if (response == null) throw new Exception("Received no response.");
var result = JsonConvert.DeserializeObject<TResponse>(response);
if (result == null) throw new Exception("Failed to deserialize response");
return result;
return LockRetry(() =>
{
var response = PostJsonString(route, body);
if (response == null) throw new Exception("Received no response.");
var result = Deserialize<TResponse>(response);
if (result == null) throw new Exception("Failed to deserialize response");
return result;
}, $"HTTO-POST-JSON: {route}");
}
public string HttpPostStream(string route, Stream stream)
{
return Retry(() =>
return LockRetry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
@@ -127,7 +117,7 @@ namespace Core
public Stream HttpGetStream(string route)
{
return Retry(() =>
return LockRetry(() =>
{
var client = GetClient();
var url = GetUrl() + route;
@@ -136,7 +126,7 @@ namespace Core
}, $"HTTP-GET-STREAM: {route}");
}
public T TryJsonDeserialize<T>(string json)
public T Deserialize<T>(string json)
{
var errors = new List<string>();
var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings()
@@ -154,7 +144,7 @@ namespace Core
}
}
});
if (errors.Count() > 0)
if (errors.Count > 0)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}' with exception(s): \n{string.Join("\n", errors)}");
}
@@ -165,16 +155,37 @@ namespace Core
return deserialized;
}
private string GetString(string route)
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "");
var result = Time.Wait(client.GetAsync(url));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private HttpResponseMessage PostJson<TRequest>(string route, TRequest body)
{
return Retry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
return Time.Wait(client.PostAsync(url, content));
}, $"HTTP-POST-JSON: {route}");
using var client = GetClient();
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
return Time.Wait(client.PostAsync(url, content));
}
private string PostJsonString(string route, string body)
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, body);
var content = new StringContent(body);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var result = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private string GetUrl()
@@ -194,9 +205,12 @@ namespace Core
}
}
private T Retry<T>(Func<T> operation, string description)
private T LockRetry<T>(Func<T> operation, string description)
{
return Time.Retry(operation, timeSet.HttpCallRetryTime(), timeSet.HttpCallRetryDelay(), description);
lock (httpLock)
{
return Time.Retry(operation, timeSet.HttpMaxNumberOfRetries(), timeSet.HttpCallRetryDelay(), description);
}
}
private HttpClient GetClient()
+6 -6
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()
@@ -44,9 +44,9 @@
return TimeSpan.FromHours(2);
}
public TimeSpan HttpCallRetryTime()
public int HttpMaxNumberOfRetries()
{
return TimeSpan.FromHours(5);
return 1;
}
public TimeSpan HttpCallRetryDelay()
@@ -59,22 +59,40 @@
public class Port
{
public Port(int number, string tag)
public Port(int number, string tag, PortProtocol protocol)
{
Number = number;
Tag = tag;
Protocol = protocol;
}
public int Number { get; }
public string Tag { get; }
public PortProtocol Protocol { get; }
public bool IsTcp()
{
return Protocol == PortProtocol.TCP;
}
public bool IsUdp()
{
return Protocol == PortProtocol.UDP;
}
public override string ToString()
{
if (string.IsNullOrEmpty(Tag)) return $"untagged-port={Number}";
return $"{Tag}={Number}";
if (string.IsNullOrEmpty(Tag)) return $"untagged-port={Number}/{Protocol}";
return $"{Tag}={Number}/{Protocol}";
}
}
public enum PortProtocol
{
TCP,
UDP
}
public class EnvVar
{
public EnvVar(string name, string value)
@@ -94,15 +112,21 @@
public class VolumeMount
{
public VolumeMount(string volumeName, string mountPath, string resourceQuantity)
public VolumeMount(string volumeName, string mountPath, string? subPath = null, string? resourceQuantity = null, string? secret = null, string? hostPath = null)
{
VolumeName = volumeName;
MountPath = mountPath;
SubPath = subPath;
ResourceQuantity = resourceQuantity;
Secret = secret;
HostPath = hostPath;
}
public string VolumeName { get; }
public string MountPath { get; }
public string ResourceQuantity { get; }
public string? SubPath { get; }
public string? ResourceQuantity { get; }
public string? Secret { get; }
public string? HostPath { get; }
}
}
@@ -50,31 +50,31 @@ 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, PortProtocol protocol = PortProtocol.TCP)
{
return AddExposedPort(factory.CreatePort(tag));
return AddExposedPort(factory.CreatePort(tag, protocol));
}
protected Port AddExposedPort(int number, string tag)
protected Port AddExposedPort(int number, string tag, PortProtocol protocol = PortProtocol.TCP)
{
return AddExposedPort(factory.CreatePort(number, tag));
return AddExposedPort(factory.CreatePort(number, tag, protocol));
}
protected Port AddInternalPort(string tag = "")
protected Port AddInternalPort(string tag = "", PortProtocol protocol = PortProtocol.TCP)
{
var p = factory.CreatePort(tag);
var p = factory.CreatePort(tag, protocol);
internalPorts.Add(p);
return p;
}
protected void AddExposedPortAndVar(string name, string tag)
protected void AddExposedPortAndVar(string name, string tag, PortProtocol protocol = PortProtocol.TCP)
{
AddEnvVar(name, AddExposedPort(tag));
AddEnvVar(name, AddExposedPort(tag, protocol));
}
protected void AddInternalPortAndVar(string name, string tag = "")
protected void AddInternalPortAndVar(string name, string tag = "", PortProtocol protocol = PortProtocol.TCP)
{
AddEnvVar(name, AddInternalPort(tag));
AddEnvVar(name, AddInternalPort(tag, protocol));
}
protected void AddEnvVar(string name, string value)
@@ -97,12 +97,18 @@ namespace KubernetesWorkflow
podAnnotations.Add(name, value);
}
protected void AddVolume(string name, string mountPath, string? subPath = null, string? secret = null, string? hostPath = null)
{
var size = 10.MB().ToSuffixNotation();
volumeMounts.Add(new VolumeMount(name, mountPath, subPath, size, secret, hostPath));
}
protected void AddVolume(string mountPath, ByteSize volumeSize)
{
volumeMounts.Add(new VolumeMount(
$"autovolume-{Guid.NewGuid().ToString().ToLowerInvariant()}",
mountPath,
volumeSize.ToSuffixNotation()));
resourceQuantity: volumeSize.ToSuffixNotation()));
}
protected void Additional(object userData)
+138 -68
View File
@@ -9,15 +9,14 @@ namespace KubernetesWorkflow
{
private readonly ILog log;
private readonly K8sCluster cluster;
private readonly KnownK8sPods knownPods;
private readonly WorkflowNumberSource workflowNumberSource;
private readonly K8sClient client;
private const string podLabelKey = "pod-uuid";
public K8sController(ILog log, K8sCluster cluster, KnownK8sPods knownPods, WorkflowNumberSource workflowNumberSource, string k8sNamespace)
public K8sController(ILog log, K8sCluster cluster, WorkflowNumberSource workflowNumberSource, string k8sNamespace)
{
this.log = log;
this.cluster = cluster;
this.knownPods = knownPods;
this.workflowNumberSource = workflowNumberSource;
client = new K8sClient(cluster.GetK8sClientConfig());
@@ -34,13 +33,30 @@ namespace KubernetesWorkflow
log.Debug();
EnsureNamespace();
var deploymentName = CreateDeployment(containerRecipes, location);
var podLabel = K8sNameUtils.Format(Guid.NewGuid().ToString());
var deploymentName = CreateDeployment(containerRecipes, location, podLabel);
var (serviceName, servicePortsMap) = CreateService(containerRecipes);
var podInfo = FetchNewPod();
var pod = FindPodByLabel(podLabel);
var podInfo = CreatePodInfo(pod);
return new RunningPod(cluster, podInfo, deploymentName, serviceName, servicePortsMap.ToArray());
}
private V1Pod FindPodByLabel(string podLabel)
{
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace));
foreach (var pod in pods.Items)
{
var label = pod.GetLabel(podLabelKey);
if (label == podLabel)
{
return pod;
}
}
throw new Exception("Unable to find pod by label.");
}
public void Stop(RunningPod pod)
{
log.Debug();
@@ -81,10 +97,6 @@ namespace KubernetesWorkflow
{
DeleteNamespace(ns);
}
foreach (var ns in namespaces)
{
WaitUntilNamespaceDeleted(ns);
}
}
public void DeleteNamespace()
@@ -94,7 +106,6 @@ namespace KubernetesWorkflow
{
client.Run(c => c.DeleteNamespace(K8sNamespace, null, null, gracePeriodSeconds: 0));
}
WaitUntilNamespaceDeleted();
}
public void DeleteNamespace(string ns)
@@ -304,7 +315,7 @@ namespace KubernetesWorkflow
#region Deployment management
private string CreateDeployment(ContainerRecipe[] containerRecipes, ILocation location)
private string CreateDeployment(ContainerRecipe[] containerRecipes, ILocation location, string podLabel)
{
var deploymentSpec = new V1Deployment
{
@@ -321,7 +332,7 @@ namespace KubernetesWorkflow
{
Metadata = new V1ObjectMeta
{
Labels = GetSelector(containerRecipes),
Labels = GetSelector(containerRecipes, podLabel),
Annotations = GetAnnotations(containerRecipes)
},
Spec = new V1PodSpec
@@ -368,6 +379,13 @@ namespace KubernetesWorkflow
return containerRecipes.First().PodLabels.GetLabels();
}
private IDictionary<string, string> GetSelector(ContainerRecipe[] containerRecipes, string podLabel)
{
var labels = containerRecipes.First().PodLabels.Clone();
labels.Add(podLabelKey, podLabel);
return labels.GetLabels();
}
private IDictionary<string, string> GetRunnerNamespaceSelector()
{
return new Dictionary<string, string> { { "kubernetes.io/metadata.name", "default" } };
@@ -446,7 +464,8 @@ namespace KubernetesWorkflow
return new V1VolumeMount
{
Name = v.VolumeName,
MountPath = v.MountPath
MountPath = v.MountPath,
SubPath = v.SubPath,
};
}
@@ -462,28 +481,28 @@ namespace KubernetesWorkflow
private V1Volume CreateVolume(VolumeMount v)
{
client.Run(c => c.CreateNamespacedPersistentVolumeClaim(new V1PersistentVolumeClaim
CreatePersistentVolumeClaimIfNeeded(v);
if (!string.IsNullOrEmpty(v.HostPath))
{
ApiVersion = "v1",
Metadata = new V1ObjectMeta
return new V1Volume
{
Name = v.VolumeName
},
Spec = new V1PersistentVolumeClaimSpec
{
AccessModes = new List<string>
Name = v.VolumeName,
HostPath = new V1HostPathVolumeSource
{
"ReadWriteOnce"
},
Resources = new V1ResourceRequirements
{
Requests = new Dictionary<string, ResourceQuantity>
{
{"storage", new ResourceQuantity(v.ResourceQuantity) }
}
Path = v.HostPath
}
}
}, K8sNamespace));
};
}
if (!string.IsNullOrEmpty(v.Secret))
{
return new V1Volume
{
Name = v.VolumeName,
Secret = CreateVolumeSecret(v)
};
}
return new V1Volume
{
@@ -495,6 +514,50 @@ namespace KubernetesWorkflow
};
}
private void CreatePersistentVolumeClaimIfNeeded(VolumeMount v)
{
var pvcs = client.Run(c => c.ListNamespacedPersistentVolumeClaim(K8sNamespace));
if (pvcs != null && pvcs.Items.Any(i => i.Name() == v.VolumeName)) return;
client.Run(c => c.CreateNamespacedPersistentVolumeClaim(new V1PersistentVolumeClaim
{
ApiVersion = "v1",
Metadata = new V1ObjectMeta
{
Name = v.VolumeName,
},
Spec = new V1PersistentVolumeClaimSpec
{
AccessModes = new List<string>
{
"ReadWriteOnce"
},
Resources = CreateVolumeResourceRequirements(v),
},
}, K8sNamespace));
}
private V1SecretVolumeSource CreateVolumeSecret(VolumeMount v)
{
if (string.IsNullOrWhiteSpace(v.Secret)) return null!;
return new V1SecretVolumeSource
{
SecretName = v.Secret
};
}
private V1ResourceRequirements CreateVolumeResourceRequirements(VolumeMount v)
{
if (v.ResourceQuantity == null) return null!;
return new V1ResourceRequirements
{
Requests = new Dictionary<string, ResourceQuantity>()
{
{"storage", new ResourceQuantity(v.ResourceQuantity) }
}
};
}
private List<V1EnvVar> CreateEnv(ContainerRecipe recipe)
{
return recipe.EnvVars.Select(CreateEnvVar).ToList();
@@ -511,23 +574,42 @@ namespace KubernetesWorkflow
private List<V1ContainerPort> CreateContainerPorts(ContainerRecipe recipe)
{
var exposedPorts = recipe.ExposedPorts.Select(p => CreateContainerPort(recipe, p));
var internalPorts = recipe.InternalPorts.Select(p => CreateContainerPort(recipe, p));
var exposedPorts = recipe.ExposedPorts.SelectMany(p => CreateContainerPort(recipe, p));
var internalPorts = recipe.InternalPorts.SelectMany(p => CreateContainerPort(recipe, p));
return exposedPorts.Concat(internalPorts).ToList();
}
private V1ContainerPort CreateContainerPort(ContainerRecipe recipe, Port port)
private List<V1ContainerPort> CreateContainerPort(ContainerRecipe recipe, Port port)
{
var result = new List<V1ContainerPort>();
if (port.IsTcp()) CreateTcpContainerPort(result, recipe, port);
if (port.IsUdp()) CreateUdpContainerPort(result, recipe, port);
return result;
}
private void CreateUdpContainerPort(List<V1ContainerPort> result, ContainerRecipe recipe, Port port)
{
result.Add(CreateContainerPort(recipe, port, "UDP"));
}
private void CreateTcpContainerPort(List<V1ContainerPort> result, ContainerRecipe recipe, Port port)
{
result.Add(CreateContainerPort(recipe, port, "TCP"));
}
private V1ContainerPort CreateContainerPort(ContainerRecipe recipe, Port port, string protocol)
{
return new V1ContainerPort
{
Name = GetNameForPort(recipe, port),
ContainerPort = port.Number
ContainerPort = port.Number,
Protocol = protocol
};
}
private string GetNameForPort(ContainerRecipe recipe, Port port)
{
return $"p{workflowNumberSource.WorkflowNumber}-{recipe.Number}-{port.Number}";
return $"p{workflowNumberSource.WorkflowNumber}-{recipe.Number}-{port.Number}-{port.Protocol.ToString().ToLowerInvariant()}";
}
#endregion
@@ -580,7 +662,7 @@ namespace KubernetesWorkflow
if (matchingServicePorts.Any())
{
// These service ports belongs to this recipe.
var optionals = matchingServicePorts.Select(p => MapNodePortIfAble(p, port.Tag));
var optionals = matchingServicePorts.Select(p => MapNodePortIfAble(p, port.Tag, port.Protocol));
var ports = optionals.Where(p => p != null).Select(p => p!).ToArray();
result.Add(new ContainerRecipePortMapEntry(r.Number, ports));
@@ -589,10 +671,10 @@ namespace KubernetesWorkflow
}
}
private Port? MapNodePortIfAble(V1ServicePort p, string tag)
private Port? MapNodePortIfAble(V1ServicePort p, string tag, PortProtocol protocol)
{
if (p.NodePort == null) return null;
return new Port(p.NodePort.Value, tag);
return new Port(p.NodePort.Value, tag, protocol);
}
private void DeleteService(string serviceName)
@@ -624,18 +706,23 @@ namespace KubernetesWorkflow
var result = new List<V1ServicePort>();
foreach (var port in recipe.ExposedPorts)
{
result.Add(new V1ServicePort
{
Name = GetNameForPort(recipe, port),
Protocol = "TCP",
Port = port.Number,
TargetPort = GetNameForPort(recipe, port),
});
if (port.IsTcp()) CreateServicePort(result, recipe, port, "TCP");
if (port.IsUdp()) CreateServicePort(result, recipe, port, "UDP");
}
return result;
}
private void CreateServicePort(List<V1ServicePort> result, ContainerRecipe recipe, Port port, string protocol)
{
result.Add(new V1ServicePort
{
Name = GetNameForPort(recipe, port),
Protocol = protocol,
Port = port.Number,
TargetPort = GetNameForPort(recipe, port),
});
}
#endregion
#region Waiting
@@ -645,16 +732,6 @@ namespace KubernetesWorkflow
WaitUntil(() => IsNamespaceOnline(K8sNamespace));
}
private void WaitUntilNamespaceDeleted()
{
WaitUntil(() => !IsNamespaceOnline(K8sNamespace));
}
private void WaitUntilNamespaceDeleted(string name)
{
WaitUntil(() => !IsNamespaceOnline(name));
}
private void WaitUntilDeploymentOnline(string deploymentName)
{
WaitUntil(() =>
@@ -704,22 +781,15 @@ namespace KubernetesWorkflow
return new CrashWatcher(log, cluster.GetK8sClientConfig(), K8sNamespace, container);
}
private PodInfo FetchNewPod()
private PodInfo CreatePodInfo(V1Pod pod)
{
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace)).Items;
var newPods = pods.Where(p => !knownPods.Contains(p.Name())).ToArray();
if (newPods.Length != 1) throw new InvalidOperationException("Expected only 1 pod to be created. Test infra failure.");
var newPod = newPods.Single();
var name = newPod.Name();
var ip = newPod.Status.PodIP;
var k8sNodeName = newPod.Spec.NodeName;
var name = pod.Name();
var ip = pod.Status.PodIP;
var k8sNodeName = pod.Spec.NodeName;
if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Invalid pod name received. Test infra failure.");
if (string.IsNullOrEmpty(ip)) throw new InvalidOperationException("Invalid pod IP received. Test infra failure.");
knownPods.Add(name);
return new PodInfo(name, ip, k8sNodeName);
}
}
@@ -1,17 +0,0 @@
namespace KubernetesWorkflow
{
public class KnownK8sPods
{
private readonly List<string> knownActivePodNames = new List<string>();
public bool Contains(string name)
{
return knownActivePodNames.Contains(name);
}
public void Add(string name)
{
knownActivePodNames.Add(name);
}
}
}
@@ -7,14 +7,14 @@ namespace KubernetesWorkflow
{
private NumberSource portNumberSource = new NumberSource(8080);
public Port CreatePort(int number, string tag)
public Port CreatePort(int number, string tag, PortProtocol protocol)
{
return new Port(number, tag);
return new Port(number, tag, protocol);
}
public Port CreatePort(string tag)
public Port CreatePort(string tag, PortProtocol protocol)
{
return new Port(portNumberSource.GetNextNumber(), tag);
return new Port(portNumberSource.GetNextNumber(), tag, protocol);
}
public EnvVar CreateEnvVar(string name, int value)
@@ -29,7 +29,7 @@ namespace KubernetesWorkflow
foreach (var port in container.ContainerPorts)
{
if (PingHost(Format(port.ExternalAddress)))
if (port.ExternalAddress.IsValid() && PingHost(Format(port.ExternalAddress)))
{
return RunnerLocation.ExternalToCluster;
}
@@ -1,5 +1,4 @@
using Newtonsoft.Json;
using Utils;
using Utils;
namespace KubernetesWorkflow
{
@@ -39,13 +38,19 @@ namespace KubernetesWorkflow
public Port[] ServicePorts { get; }
public ContainerPort[] ContainerPorts { get; }
public ContainerPort GetContainerPort(string portTag)
{
return ContainerPorts.Single(c => c.Port.Tag == portTag);
}
public Address GetAddress(string portTag)
{
var containerPort = ContainerPorts.Single(c => c.Port.Tag == portTag);
var containerPort = GetContainerPort(portTag);
if (RunnerLocationUtils.DetermineRunnerLocation(this) == RunnerLocation.InternalToCluster)
{
return containerPort.InternalAddress;
}
if (!containerPort.ExternalAddress.IsValid()) throw new Exception($"Getting address by tag {portTag} resulted in an invalid address.");
return containerPort.ExternalAddress;
}
}
+39 -17
View File
@@ -1,4 +1,5 @@
using Logging;
using Newtonsoft.Json;
using Utils;
namespace KubernetesWorkflow
@@ -21,17 +22,15 @@ namespace KubernetesWorkflow
private readonly ILog log;
private readonly WorkflowNumberSource numberSource;
private readonly K8sCluster cluster;
private readonly KnownK8sPods knownK8SPods;
private readonly string k8sNamespace;
private readonly RecipeComponentFactory componentFactory = new RecipeComponentFactory();
private readonly LocationProvider locationProvider;
internal StartupWorkflow(ILog log, WorkflowNumberSource numberSource, K8sCluster cluster, KnownK8sPods knownK8SPods, string k8sNamespace)
internal StartupWorkflow(ILog log, WorkflowNumberSource numberSource, K8sCluster cluster, string k8sNamespace)
{
this.log = log;
this.numberSource = numberSource;
this.cluster = cluster;
this.knownK8SPods = knownK8SPods;
this.k8sNamespace = k8sNamespace;
locationProvider = new LocationProvider(log, K8s);
@@ -144,7 +143,17 @@ namespace KubernetesWorkflow
result.Add(new ContainerPort(
exposedPort,
GetContainerExternalAddress(pod, servicePorts, exposedPort),
GetContainerInternalAddress(exposedPort)));
GetContainerInternalAddress(pod, exposedPort)));
}
foreach (var internalPort in recipe.InternalPorts)
{
if (!string.IsNullOrEmpty(internalPort.Tag))
{
result.Add(new ContainerPort(
internalPort,
new Address(string.Empty, 0),
GetContainerInternalAddress(pod, internalPort)));
}
}
return result.ToArray();
@@ -159,14 +168,11 @@ namespace KubernetesWorkflow
servicePort.Number);
}
private Address GetContainerInternalAddress(Port exposedPort)
private Address GetContainerInternalAddress(RunningPod pod, Port port)
{
var serviceName = "service-" + numberSource.WorkflowNumber;
var port = exposedPort.Number;
return new Address(
$"http://{serviceName}.{k8sNamespace}.svc.cluster.local",
port);
$"http://{pod.PodInfo.Ip}",
port.Number);
}
private ContainerRecipe[] CreateRecipes(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
@@ -186,17 +192,33 @@ namespace KubernetesWorkflow
private void K8s(Action<K8sController> action)
{
var controller = new K8sController(log, cluster, knownK8SPods, numberSource, k8sNamespace);
action(controller);
controller.Dispose();
try
{
var controller = new K8sController(log, cluster, numberSource, k8sNamespace);
action(controller);
controller.Dispose();
}
catch (k8s.Autorest.HttpOperationException ex)
{
log.Error(JsonConvert.SerializeObject(ex));
throw;
}
}
private T K8s<T>(Func<K8sController, T> action)
{
var controller = new K8sController(log, cluster, knownK8SPods, numberSource, k8sNamespace);
var result = action(controller);
controller.Dispose();
return result;
try
{
var controller = new K8sController(log, cluster, numberSource, k8sNamespace);
var result = action(controller);
controller.Dispose();
return result;
}
catch (k8s.Autorest.HttpOperationException ex)
{
log.Error(JsonConvert.SerializeObject(ex));
throw;
}
}
}
@@ -7,7 +7,6 @@ namespace KubernetesWorkflow
{
private readonly NumberSource numberSource = new NumberSource(0);
private readonly NumberSource containerNumberSource = new NumberSource(0);
private readonly KnownK8sPods knownPods = new KnownK8sPods();
private readonly K8sCluster cluster;
private readonly ILog log;
private readonly Configuration configuration;
@@ -26,7 +25,7 @@ namespace KubernetesWorkflow
var workflowNumberSource = new WorkflowNumberSource(numberSource.GetNextNumber(),
containerNumberSource);
return new StartupWorkflow(log, workflowNumberSource, cluster, knownPods, GetNamespace(namespaceOverride));
return new StartupWorkflow(log, workflowNumberSource, cluster, GetNamespace(namespaceOverride));
}
private string GetNamespace(string? namespaceOverride)
+3 -3
View File
@@ -39,13 +39,12 @@ namespace Logging
LogFile.Write(ApplyReplacements(message));
}
public virtual void Debug(string message = "", int skipFrames = 0)
public void Debug(string message = "", int skipFrames = 0)
{
if (debug)
{
var callerName = DebugStack.GetCallerName(skipFrames);
// We don't use Log because in the debug output we should not have any replacements.
LogFile.Write($"(debug)({callerName}) {message}");
Log($"(debug)({callerName}) {message}");
}
}
@@ -73,6 +72,7 @@ namespace Logging
private string ApplyReplacements(string str)
{
if (debug) return str;
foreach (var replacement in replacements)
{
str = replacement.Apply(str);
-4
View File
@@ -17,10 +17,6 @@
{
}
public override void Debug(string message = "", int skipFrames = 0)
{
}
public override void Error(string message)
{
Console.WriteLine("Error: " + message);
@@ -19,29 +19,34 @@ namespace NethereumWorkflow
public void SendEth(string toAddress, decimal ethAmount)
{
log.Debug();
var receipt = Time.Wait(web3.Eth.GetEtherTransferService().TransferEtherAndWaitForReceiptAsync(toAddress, ethAmount));
if (!receipt.Succeeded()) throw new Exception("Unable to send Eth");
}
public decimal GetEthBalance()
{
log.Debug();
return GetEthBalance(web3.TransactionManager.Account.Address);
}
public decimal GetEthBalance(string address)
{
log.Debug();
var balance = Time.Wait(web3.Eth.GetBalance.SendRequestAsync(address));
return Web3.Convert.FromWei(balance.Value);
}
public TResult Call<TFunction, TResult>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
{
log.Debug(typeof(TFunction).ToString());
var handler = web3.Eth.GetContractQueryHandler<TFunction>();
return Time.Wait(handler.QueryAsync<TResult>(contractAddress, function));
}
public void SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
{
log.Debug();
var handler = web3.Eth.GetContractTransactionHandler<TFunction>();
var receipt = Time.Wait(handler.SendRequestAndWaitForReceiptAsync(contractAddress, function));
if (!receipt.Succeeded()) throw new Exception("Unable to perform contract transaction.");
@@ -20,6 +20,7 @@ namespace NethereumWorkflow
public NethereumInteraction CreateWorkflow()
{
log.Debug("Starting interaction to " + ip + ":" + port);
return new NethereumInteraction(log, CreateWeb3());
}
+5
View File
@@ -15,5 +15,10 @@
{
return $"{Host}:{Port}";
}
public bool IsValid()
{
return !string.IsNullOrEmpty(Host) && Port > 0;
}
}
}
+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);
@@ -7,41 +7,43 @@ namespace CodexContractsPlugin
{
CodexContractsDeployment Deployment { get; }
void MintTestTokens(IGethNode gethNode, IHasEthAddress owner, TestToken testTokens);
void MintTestTokens(IGethNode gethNode, EthAddress ethAddress, TestToken testTokens);
TestToken GetTestTokenBalance(IGethNode gethNode, IHasEthAddress owner);
TestToken GetTestTokenBalance(IGethNode gethNode, EthAddress ethAddress);
void MintTestTokens(IHasEthAddress owner, TestToken testTokens);
void MintTestTokens(EthAddress ethAddress, TestToken testTokens);
TestToken GetTestTokenBalance(IHasEthAddress owner);
TestToken GetTestTokenBalance(EthAddress ethAddress);
}
public class CodexContractsAccess : ICodexContracts
{
private readonly ILog log;
private readonly IGethNode gethNode;
public CodexContractsAccess(ILog log, CodexContractsDeployment deployment)
public CodexContractsAccess(ILog log, IGethNode gethNode, CodexContractsDeployment deployment)
{
this.log = log;
this.gethNode = gethNode;
Deployment = deployment;
}
public CodexContractsDeployment Deployment { get; }
public void MintTestTokens(IGethNode gethNode, IHasEthAddress owner, TestToken testTokens)
public void MintTestTokens(IHasEthAddress owner, TestToken testTokens)
{
MintTestTokens(gethNode, owner.EthAddress, testTokens);
MintTestTokens(owner.EthAddress, testTokens);
}
public void MintTestTokens(IGethNode gethNode, EthAddress ethAddress, TestToken testTokens)
public void MintTestTokens(EthAddress ethAddress, TestToken testTokens)
{
var interaction = new ContractInteractions(log, gethNode);
interaction.MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
}
public TestToken GetTestTokenBalance(IGethNode gethNode, IHasEthAddress owner)
public TestToken GetTestTokenBalance(IHasEthAddress owner)
{
return GetTestTokenBalance(gethNode, owner.EthAddress);
return GetTestTokenBalance(owner.EthAddress);
}
public TestToken GetTestTokenBalance(IGethNode gethNode, EthAddress ethAddress)
public TestToken GetTestTokenBalance(EthAddress ethAddress)
{
var interaction = new ContractInteractions(log, gethNode);
var balance = interaction.GetBalance(Deployment.TokenAddress, ethAddress.Address);
@@ -1,10 +1,11 @@
using KubernetesWorkflow;
using GethPlugin;
using KubernetesWorkflow;
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";
@@ -16,10 +17,10 @@ namespace CodexContractsPlugin
{
var config = startupConfig.Get<CodexContractsContainerConfig>();
var ip = config.GethNode.StartResult.Container.Pod.PodInfo.Ip;
var port = config.GethNode.StartResult.HttpPort.Number;
var containerPort = config.GethNode.StartResult.Container.GetContainerPort(GethContainerRecipe.HttpPortTag);
var address = containerPort.InternalAddress;
AddEnvVar("DISTTEST_NETWORK_URL", $"http://{ip}:{port}");
AddEnvVar("DISTTEST_NETWORK_URL", address.ToString());
AddEnvVar("HARDHAT_NETWORK", "codexdisttestnetwork");
AddEnvVar("KEEP_ALIVE", "1");
}
@@ -35,10 +35,10 @@ namespace CodexContractsPlugin
return starter.Deploy(ci, gethNode);
}
public ICodexContracts WrapDeploy(CodexContractsDeployment deployment)
public ICodexContracts WrapDeploy(IGethNode gethNode, CodexContractsDeployment deployment)
{
deployment = SerializeGate.Gate(deployment);
return starter.Wrap(deployment);
return starter.Wrap(gethNode, deployment);
}
}
}
@@ -31,20 +31,23 @@ namespace CodexContractsPlugin
try
{
return DeployContract(container, workflow, gethNode);
var result = DeployContract(container, workflow, gethNode);
workflow.Stop(containers);
Log("Container stopped.");
return result;
}
catch
catch (Exception ex)
{
Log("Failed to deploy contract.");
Log("Failed to deploy contract: " + ex);
Log("Downloading Codex SmartContracts container log...");
ci.DownloadLog(container);
throw;
}
}
public ICodexContracts Wrap(CodexContractsDeployment deployment)
public ICodexContracts Wrap(IGethNode gethNode, CodexContractsDeployment deployment)
{
return new CodexContractsAccess(tools.GetLog(), deployment);
return new CodexContractsAccess(tools.GetLog(), gethNode, deployment);
}
private CodexContractsDeployment DeployContract(RunningContainer container, IStartupWorkflow workflow, IGethNode gethNode)
@@ -28,6 +28,7 @@ namespace CodexContractsPlugin
public void MintTestTokens(EthAddress address, decimal amount, string tokenAddress)
{
log.Debug($"{amount} -> {address} (token: {tokenAddress})");
MintTokens(address.Address, amount, tokenAddress);
}
@@ -44,6 +45,7 @@ namespace CodexContractsPlugin
public bool IsSynced(string marketplaceAddress, string marketplaceAbi)
{
log.Debug();
try
{
return IsBlockNumberOK() && IsContractAvailable(marketplaceAddress, marketplaceAbi);
@@ -25,6 +25,7 @@ namespace CodexContractsPlugin
var marketplaceAddress = Retry(FetchMarketplaceAddress);
if (string.IsNullOrEmpty(marketplaceAddress)) throw new InvalidOperationException("Unable to fetch marketplace account from codex-contracts node. Test infra failure.");
log.Debug("Got MarketplaceAddress: " + marketplaceAddress);
return marketplaceAddress;
}
@@ -34,6 +35,7 @@ namespace CodexContractsPlugin
var marketplaceAbi = Retry(FetchMarketplaceAbi);
if (string.IsNullOrEmpty(marketplaceAbi)) throw new InvalidOperationException("Unable to fetch marketplace artifacts from codex-contracts node. Test infra failure.");
log.Debug("Got Marketplace ABI: " + marketplaceAbi);
return marketplaceAbi;
}
@@ -10,15 +10,15 @@ namespace CodexContractsPlugin
return Plugin(ci).DeployContracts(ci, gethNode);
}
public static ICodexContracts WrapCodexContractsDeployment(this CoreInterface ci, CodexContractsDeployment deployment)
public static ICodexContracts WrapCodexContractsDeployment(this CoreInterface ci, IGethNode gethNode, CodexContractsDeployment deployment)
{
return Plugin(ci).WrapDeploy(deployment);
return Plugin(ci).WrapDeploy(gethNode, deployment);
}
public static ICodexContracts StartCodexContracts(this CoreInterface ci, IGethNode gethNode)
{
var deployment = DeployCodexContracts(ci, gethNode);
return WrapCodexContractsDeployment(ci, deployment);
return WrapCodexContractsDeployment(ci, gethNode, deployment);
}
private static CodexContractsPlugin Plugin(CoreInterface ci)
@@ -0,0 +1,46 @@
using Core;
using KubernetesWorkflow;
namespace CodexDiscordBotPlugin
{
public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata
{
private readonly IPluginTools tools;
public CodexDiscordBotPlugin(IPluginTools tools)
{
this.tools = tools;
}
public string LogPrefix => "(DiscordBot) ";
public void Announce()
{
tools.GetLog().Log($"Codex DiscordBot (BiblioTech) loaded.");
}
public void AddMetadata(IAddMetadata metadata)
{
metadata.Add("codexdiscordbotid", new DiscordBotContainerRecipe().Image);
}
public void Decommission()
{
}
public RunningContainer Deploy(DiscordBotStartupConfig config)
{
var workflow = tools.CreateWorkflow();
return StartContainer(workflow, config);
}
private RunningContainer StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config)
{
var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config);
var rc = workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig);
return rc.Containers.Single();
}
}
}
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
<ProjectReference Include="..\CodexPlugin\CodexPlugin.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,18 @@
using Core;
using KubernetesWorkflow;
namespace CodexDiscordBotPlugin
{
public static class CoreInterfaceExtensions
{
public static RunningContainer DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config)
{
return Plugin(ci).Deploy(config);
}
private static CodexDiscordBotPlugin Plugin(CoreInterface ci)
{
return ci.GetPlugin<CodexDiscordBotPlugin>();
}
}
}
@@ -0,0 +1,27 @@
using KubernetesWorkflow;
using Utils;
namespace CodexDiscordBotPlugin
{
public class DiscordBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-bibliotech";
public override string Image => "thatbenbierens/codex-discordbot:initial";
protected override void Initialize(StartupConfig startupConfig)
{
var config = startupConfig.Get<DiscordBotStartupConfig>();
AddEnvVar("TOKEN", config.Token);
AddEnvVar("SERVERNAME", config.ServerName);
AddEnvVar("ADMINROLE", config.AdminRoleName);
AddEnvVar("ADMINCHANNELNAME", config.AdminChannelName);
if (!string.IsNullOrEmpty(config.DataPath))
{
AddEnvVar("DATAPATH", config.DataPath);
AddVolume(config.DataPath, 1.GB());
}
}
}
}
@@ -0,0 +1,21 @@
namespace CodexDiscordBotPlugin
{
public class DiscordBotStartupConfig
{
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName)
{
Name = name;
Token = token;
ServerName = serverName;
AdminRoleName = adminRoleName;
AdminChannelName = adminChannelName;
}
public string Name { get; }
public string Token { get; }
public string ServerName { get; }
public string AdminRoleName { get; }
public string AdminChannelName { get; }
public string? DataPath { get; set; }
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ namespace CodexPlugin
};
}
var result = http.TryJsonDeserialize<CodexDebugPeerResponse>(str);
var result = http.Deserialize<CodexDebugPeerResponse>(str);
result.IsPeerFound = true;
return result;
}
@@ -29,20 +29,30 @@ namespace CodexPlugin
var config = startupConfig.Get<CodexStartupConfig>();
AddExposedPortAndVar("CODEX_API_PORT", ApiPortTag);
var apiPort = CreateApiPort(config, ApiPortTag);
AddEnvVar("CODEX_API_PORT", apiPort);
AddEnvVar("CODEX_API_BINDADDR", "0.0.0.0");
var dataDir = $"datadir{ContainerNumber}";
AddEnvVar("CODEX_DATA_DIR", dataDir);
AddVolume($"codex/{dataDir}", GetVolumeCapacity(config));
AddExposedPortAndVar("CODEX_DISC_PORT", DiscoveryPortTag);
var discPort = CreateDiscoveryPort(config);
AddEnvVar("CODEX_DISC_PORT", discPort);
AddEnvVar("CODEX_LOG_LEVEL", config.LogLevelWithTopics());
// This makes the node announce itself to its local (pod) IP address.
AddEnvVar("NAT_IP_AUTO", "true");
if (config.PublicTestNet != null)
{
AddEnvVar("CODEX_NAT", config.PublicTestNet.PublicNatIP);
AddEnvVar("NAT_IP_AUTO", "false");
}
else
{
// This makes the node announce itself to its local (pod) IP address.
AddEnvVar("NAT_IP_AUTO", "true");
}
var listenPort = AddExposedPort(ListenPortTag);
var listenPort = CreateListenPort(config);
AddEnvVar("CODEX_LISTEN_ADDRS", $"/ip4/0.0.0.0/tcp/{listenPort.Number}");
if (!string.IsNullOrEmpty(config.BootstrapSpr))
@@ -67,7 +77,7 @@ namespace CodexPlugin
}
if (config.MetricsEnabled)
{
var metricsPort = AddInternalPort(MetricsPortTag);
var metricsPort = CreateApiPort(config, MetricsPortTag);
AddEnvVar("CODEX_METRICS", "true");
AddEnvVar("CODEX_METRICS_ADDRESS", "0.0.0.0");
AddEnvVar("CODEX_METRICS_PORT", metricsPort);
@@ -110,6 +120,26 @@ namespace CodexPlugin
}
}
private Port CreateApiPort(CodexStartupConfig config, string tag)
{
if (config.PublicTestNet == null) return AddExposedPort(tag);
return AddInternalPort(tag);
}
private Port CreateListenPort(CodexStartupConfig config)
{
if (config.PublicTestNet == null) return AddInternalPort(ListenPortTag);
return AddExposedPort(config.PublicTestNet.PublicListenPort, ListenPortTag);
}
private Port CreateDiscoveryPort(CodexStartupConfig config)
{
if (config.PublicTestNet == null) return AddInternalPort(DiscoveryPortTag, PortProtocol.UDP);
return AddExposedPort(config.PublicTestNet.PublicDiscoveryPort, DiscoveryPortTag, PortProtocol.UDP);
}
private ByteSize GetVolumeCapacity(CodexStartupConfig config)
{
if (config.StorageQuota != null) return config.StorageQuota;
+24 -5
View File
@@ -1,28 +1,46 @@
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(CodexInstance[] codexInstances, GethDeployment gethDeployment, CodexContractsDeployment codexContractsDeployment, RunningContainer? prometheusContainer, RunningContainer? discordBotContainer, DeploymentMetadata metadata)
{
CodexContainers = codexContainers;
CodexInstances = codexInstances;
GethDeployment = gethDeployment;
CodexContractsDeployment = codexContractsDeployment;
PrometheusContainer = prometheusContainer;
DiscordBotContainer = discordBotContainer;
Metadata = metadata;
}
public RunningContainer[] CodexContainers { get; }
public CodexInstance[] CodexInstances { get; }
public GethDeployment GethDeployment { get; }
public CodexContractsDeployment CodexContractsDeployment { get; }
public RunningContainer? PrometheusContainer { get; }
public RunningContainer? DiscordBotContainer { get; }
public DeploymentMetadata Metadata { get; }
}
public class CodexInstance
{
public CodexInstance(RunningContainer container, CodexDebugResponse info)
{
Container = container;
Info = info;
}
public RunningContainer Container { get; }
public CodexDebugResponse Info { get; }
}
public class DeploymentMetadata
{
public DeploymentMetadata(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)
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)
{
Name = name;
StartUtc = startUtc;
FinishedUtc = finishedUtc;
KubeNamespace = kubeNamespace;
@@ -39,6 +57,7 @@ namespace CodexPlugin
BlockMN = blockMN;
}
public string Name { get; }
public DateTime StartUtc { get; }
public DateTime FinishedUtc { get; }
public string KubeNamespace { get; }
+3 -2
View File
@@ -13,8 +13,9 @@ namespace CodexPlugin
string GetName();
CodexDebugResponse GetDebugInfo();
CodexDebugPeerResponse GetDebugPeer(string peerId);
CodexDebugBlockExchangeResponse GetDebugBlockExchange();
CodexDebugRepoStoreResponse[] GetDebugRepoStore();
// 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);
+1 -1
View File
@@ -52,7 +52,7 @@ namespace CodexPlugin
foreach (var node in result)
{
mconfig.GethNode.SendEth(node, mconfig.InitialEth);
mconfig.CodexContracts.MintTestTokens(mconfig.GethNode, node, mconfig.InitialTokens);
mconfig.CodexContracts.MintTestTokens(node, mconfig.InitialTokens);
}
}
+8
View File
@@ -22,6 +22,7 @@ namespace CodexPlugin
/// Provides an invalid proof every N proofs
/// </summary>
ICodexSetup WithSimulateProofFailures(uint failEveryNProofs);
ICodexSetup AsPublicTestNet(CodexTestNetConfig testNetConfig);
}
public class CodexLogCustomTopics
@@ -118,6 +119,12 @@ namespace CodexPlugin
return this;
}
public ICodexSetup AsPublicTestNet(CodexTestNetConfig testNetConfig)
{
PublicTestNet = testNetConfig;
return this;
}
public string Describe()
{
var args = string.Join(',', DescribeArgs());
@@ -126,6 +133,7 @@ namespace CodexPlugin
private IEnumerable<string> DescribeArgs()
{
if (PublicTestNet != null) yield return $"<!>Public TestNet at {PublicTestNet.PublicNatIP}:{PublicTestNet.PublicListenPort}<!>";
yield return $"LogLevel={LogLevelWithTopics()}";
if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}";
if (StorageQuota != null) yield return $"StorageQuota={StorageQuota}";
@@ -18,6 +18,7 @@ namespace CodexPlugin
public bool? EnableValidator { get; set; }
public TimeSpan? BlockMaintenanceInterval { get; set; }
public int? BlockMaintenanceNumber { get; set; }
public CodexTestNetConfig? PublicTestNet { get; set; }
public string LogLevelWithTopics()
{
@@ -61,4 +62,11 @@ namespace CodexPlugin
return level;
}
}
public class CodexTestNetConfig
{
public string PublicNatIP { get; set; } = string.Empty;
public int PublicDiscoveryPort { get; set; }
public int PublicListenPort { get; set; }
}
}
@@ -0,0 +1,13 @@
using Core;
using KubernetesWorkflow;
namespace DeployAndRunPlugin
{
public static class CoreInterfaceExtensions
{
public static RunningContainer DeployAndRunContinuousTests(this CoreInterface ci, RunConfig runConfig)
{
return ci.GetPlugin<DeployAndRunPlugin>().Run(runConfig);
}
}
}
@@ -0,0 +1,49 @@
using KubernetesWorkflow;
namespace DeployAndRunPlugin
{
public class DeployAndRunContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "deploy-and-run";
public override string Image => "thatbenbierens/dist-tests-deployandrun:initial";
protected override void Initialize(StartupConfig config)
{
var setup = config.Get<RunConfig>();
if (setup.CodexImageOverride != null)
{
AddEnvVar("CODEXDOCKERIMAGE", setup.CodexImageOverride);
}
AddEnvVar("DNR_REP", setup.Replications.ToString());
AddEnvVar("DNR_NAME", setup.Name);
AddEnvVar("DNR_FILTER", setup.Filter);
AddEnvVar("DNR_DURATION", setup.Duration.TotalSeconds.ToString());
AddEnvVar("KUBECONFIG", "/opt/kubeconfig.yaml");
AddEnvVar("LOGPATH", "/var/log/codex-continuous-tests");
AddVolume(name: "kubeconfig", mountPath: "/opt/kubeconfig.yaml", subPath: "kubeconfig.yaml", secret: "codex-dist-tests-app-kubeconfig");
AddVolume(name: "logs", mountPath: "/var/log/codex-continuous-tests", hostPath: "/var/log/codex-continuous-tests");
}
}
public class RunConfig
{
public RunConfig(string name, string filter, TimeSpan duration, int replications, string? codexImageOverride = null)
{
Name = name;
Filter = filter;
Duration = duration;
Replications = replications;
CodexImageOverride = codexImageOverride;
}
public string Name { get; }
public string Filter { get; }
public TimeSpan Duration { get; }
public int Replications { get; }
public string? CodexImageOverride { get; }
}
}
@@ -0,0 +1,36 @@
using Core;
using KubernetesWorkflow;
namespace DeployAndRunPlugin
{
public class DeployAndRunPlugin : IProjectPlugin
{
private readonly IPluginTools tools;
public DeployAndRunPlugin(IPluginTools tools)
{
this.tools = tools;
}
public void Announce()
{
tools.GetLog().Log("Deploy-and-Run plugin loaded.");
}
public void Decommission()
{
}
public RunningContainer Run(RunConfig config)
{
var workflow = tools.CreateWorkflow();
var startupConfig = new StartupConfig();
startupConfig.NameOverride = "dnr-" + config.Name;
startupConfig.Add(config);
var location = workflow.GetAvailableLocations().Get("fixed-s-4vcpu-16gb-amd-yz8rd");
var containers = workflow.Start(1, location, new DeployAndRunContainerRecipe(), startupConfig);
return containers.Containers.Single();
}
}
}
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
<ProjectReference Include="..\..\Framework\KubernetesWorkflow\KubernetesWorkflow.csproj" />
</ItemGroup>
</Project>
+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";
}
}
@@ -9,7 +9,9 @@ namespace GethPlugin
public const string HttpPortTag = "http_port";
public const string DiscoveryPortTag = "disc_port";
public const string ListenPortTag = "listen_port";
public const string WsPortTag = "ws_port";
public const string AuthRpcPortTag = "auth_rpc_port";
public const string AccountsFilename = "accounts.csv";
public override string AppName => "geth";
@@ -26,15 +28,16 @@ namespace GethPlugin
private string CreateArgs(GethStartupConfig config)
{
var discovery = AddInternalPort(tag: DiscoveryPortTag);
if (config.IsMiner) AddEnvVar("ENABLE_MINER", "1");
UnlockAccounts(0, 1);
var httpPort = AddExposedPort(tag: HttpPortTag);
var args = $"--http.addr 0.0.0.0 --http.port {httpPort.Number} --port {discovery.Number} --discovery.port {discovery.Number} {defaultArgs}";
var authRpc = AddInternalPort();
var wsPort = AddInternalPort(tag: WsPortTag);
var httpPort = CreateApiPort(config, tag: HttpPortTag);
var discovery = CreateDiscoveryPort(config);
var listen = CreateListenPort(config);
var authRpc = CreateP2pPort(config, tag: AuthRpcPortTag);
var wsPort = CreateP2pPort(config, tag: WsPortTag);
var args = $"--http.addr 0.0.0.0 --http.port {httpPort.Number} --port {listen.Number} --discovery.port {discovery.Number} {GetTestNetArgs(config)} {defaultArgs}";
if (config.BootstrapNode != null)
{
@@ -57,5 +60,44 @@ namespace GethPlugin
AddEnvVar("UNLOCK_START_INDEX", startIndex.ToString());
AddEnvVar("UNLOCK_NUMBER", numberOfAccounts.ToString());
}
private string GetTestNetArgs(GethStartupConfig config)
{
if (config.IsPublicTestNet == null) return string.Empty;
return $"--nat=extip:{config.IsPublicTestNet.PublicIp}";
}
private Port CreateDiscoveryPort(GethStartupConfig config)
{
if (config.IsPublicTestNet == null) return AddInternalPort(DiscoveryPortTag);
return AddExposedPort(config.IsPublicTestNet.DiscoveryPort, DiscoveryPortTag, PortProtocol.UDP);
}
private Port CreateListenPort(GethStartupConfig config)
{
if (config.IsPublicTestNet == null) return AddInternalPort(ListenPortTag);
return AddExposedPort(config.IsPublicTestNet.ListenPort, ListenPortTag);
}
private Port CreateP2pPort(GethStartupConfig config, string tag)
{
if (config.IsPublicTestNet != null)
{
return AddExposedPort(tag);
}
return AddInternalPort(tag);
}
private Port CreateApiPort(GethStartupConfig config, string tag)
{
if (config.IsPublicTestNet != null)
{
return AddInternalPort(tag);
}
return AddExposedPort(tag);
}
}
}
+3 -3
View File
@@ -5,13 +5,13 @@ namespace GethPlugin
{
public class GethDeployment : IHasContainer
{
public GethDeployment(RunningContainer container, Port discoveryPort, Port httpPort, Port wsPort, AllGethAccounts allAccounts, string pubKey)
public GethDeployment(RunningContainer container, Port discoveryPort, Port httpPort, Port wsPort, GethAccount account, string pubKey)
{
Container = container;
DiscoveryPort = discoveryPort;
HttpPort = httpPort;
WsPort = wsPort;
AllAccounts = allAccounts;
Account = account;
PubKey = pubKey;
}
@@ -19,7 +19,7 @@ namespace GethPlugin
public Port DiscoveryPort { get; }
public Port HttpPort { get; }
public Port WsPort { get; }
public AllGethAccounts AllAccounts { get; }
public GethAccount Account { get; }
public string PubKey { get; }
}
}
+1 -3
View File
@@ -29,11 +29,9 @@ namespace GethPlugin
{
this.log = log;
StartResult = startResult;
Account = startResult.AllAccounts.Accounts.First();
}
public GethDeployment StartResult { get; }
public GethAccount Account { get; }
public RunningContainer Container => StartResult.Container;
public Ether GetEthBalance()
@@ -74,7 +72,7 @@ namespace GethPlugin
private NethereumInteraction StartInteraction()
{
var address = StartResult.Container.GetAddress(GethContainerRecipe.HttpPortTag);
var account = Account;
var account = StartResult.Account;
var creator = new NethereumInteractionCreator(log, address.Host, address.Port, account.PrivateKey);
return creator.CreateWorkflow();
+2 -2
View File
@@ -26,7 +26,7 @@ namespace GethPlugin
var container = containers.Containers[0];
var extractor = new GethContainerInfoExtractor(tools.GetLog(), workflow, container);
var accounts = extractor.ExtractAccounts();
var account = extractor.ExtractAccounts().Accounts.First();
var pubKey = extractor.ExtractPubKey();
var discoveryPort = container.Recipe.GetPortByTag(GethContainerRecipe.DiscoveryPortTag);
@@ -38,7 +38,7 @@ namespace GethPlugin
Log($"Geth node started.");
return new GethDeployment(container, discoveryPort, httpPort, wsPort, accounts, pubKey);
return new GethDeployment(container, discoveryPort, httpPort, wsPort, account, pubKey);
}
public IGethNode WrapGethContainer(GethDeployment startResult)
@@ -5,6 +5,7 @@
IGethSetup IsMiner();
IGethSetup WithBootstrapNode(GethBootstrapNode node);
IGethSetup WithName(string name);
IGethSetup AsPublicTestNet(GethTestNetConfig gethTestNetConfig);
}
public class GethStartupConfig : IGethSetup
@@ -12,6 +13,7 @@
public bool IsMiner { get; private set; }
public GethBootstrapNode? BootstrapNode { get; private set; }
public string? NameOverride { get; private set; }
public GethTestNetConfig? IsPublicTestNet { get; private set; }
public IGethSetup WithBootstrapNode(GethBootstrapNode node)
{
@@ -30,6 +32,26 @@
IsMiner = true;
return this;
}
public IGethSetup AsPublicTestNet(GethTestNetConfig gethTestNetConfig)
{
IsPublicTestNet = gethTestNetConfig;
return this;
}
}
public class GethTestNetConfig
{
public GethTestNetConfig(string publicIp, int discoveryPort, int listenPort)
{
PublicIp = publicIp;
DiscoveryPort = discoveryPort;
ListenPort = listenPort;
}
public string PublicIp { get; }
public int DiscoveryPort { get; }
public int ListenPort { get; }
}
public class GethBootstrapNode
@@ -1,5 +1,6 @@
using DistTestCore.Logs;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace ContinuousTests
@@ -40,7 +41,9 @@ namespace ContinuousTests
startupChecker.Check();
var taskFactory = new TaskFactory();
overviewLog.Log("Startup checks passed. Continuous tests starting...");
overviewLog.Log("Startup checks passed. Configuration:");
overviewLog.Log(JsonConvert.SerializeObject(config, Formatting.Indented));
overviewLog.Log("Continuous tests starting...");
overviewLog.Log("");
var allTests = testFactory.CreateTests();
@@ -50,22 +53,25 @@ namespace ContinuousTests
if (!filteredTests.Any())
{
overviewLog.Log("No tests selected.");
return;
Cancellation.Cts.Cancel();
}
var testLoops = filteredTests.Select(t => new TestLoop(entryPointFactory, taskFactory, config, overviewLog, t.GetType(), t.RunTestEvery, startupChecker, cancelToken)).ToArray();
foreach (var testLoop in testLoops)
else
{
if (cancelToken.IsCancellationRequested) break;
var testLoops = filteredTests.Select(t => new TestLoop(entryPointFactory, taskFactory, config, overviewLog, t.GetType(), t.RunTestEvery, startupChecker, cancelToken)).ToArray();
overviewLog.Log("Launching test-loop for " + testLoop.Name);
testLoop.Begin();
Thread.Sleep(TimeSpan.FromSeconds(5));
foreach (var testLoop in testLoops)
{
if (cancelToken.IsCancellationRequested) break;
overviewLog.Log("Launching test-loop for " + testLoop.Name);
testLoop.Begin();
Thread.Sleep(TimeSpan.FromSeconds(5));
}
overviewLog.Log("Finished launching test-loops.");
WaitUntilFinished(overviewLog, statusLog, startTime, testLoops);
overviewLog.Log("Stopping all test-loops...");
}
overviewLog.Log("Finished launching test-loops.");
WaitUntilFinished(overviewLog, statusLog, startTime, testLoops);
overviewLog.Log("Stopping all test-loops...");
taskFactory.WaitAll();
overviewLog.Log("All tasks cancelled.");
@@ -92,6 +98,7 @@ namespace ContinuousTests
{
var testDuration = Time.FormatDuration(DateTime.UtcNow - startTime);
var testData = FormatTestRuns(testLoops);
overviewLog.Log("Total duration: " + testDuration);
if (config.TargetDurationSeconds > 0)
{
+2 -2
View File
@@ -247,9 +247,9 @@ namespace ContinuousTests
private RunningContainer[] SelectRandomContainers()
{
var number = handle.Test.RequiredNumberOfNodes;
if (number == -1) return config.CodexDeployment.CodexContainers;
var containers = config.CodexDeployment.CodexInstances.Select(i => i.Container).ToList();
if (number == -1) return containers.ToArray();
var containers = config.CodexDeployment.CodexContainers.ToList();
var result = new RunningContainer[number];
for (var i = 0; i < number; i++)
{
+5 -4
View File
@@ -39,8 +39,9 @@ namespace ContinuousTests
{
log.Log("");
var deployment = config.CodexDeployment;
foreach (var container in deployment.CodexContainers)
foreach (var instance in deployment.CodexInstances)
{
var container = instance.Container;
log.Log($"Codex environment variables for '{container.Name}':");
log.Log($"Pod name: {container.Pod.PodInfo.Name} - Deployment name: {container.Pod.DeploymentName}");
var codexVars = container.Recipe.EnvVars;
@@ -81,7 +82,7 @@ namespace ContinuousTests
private void CheckCodexNodes(BaseLog log, Configuration config)
{
var nodes = entryPoint.CreateInterface().WrapCodexContainers(config.CodexDeployment.CodexContainers);
var nodes = entryPoint.CreateInterface().WrapCodexContainers(config.CodexDeployment.CodexInstances.Select(i => i.Container).ToArray());
var pass = true;
foreach (var n in nodes)
{
@@ -166,9 +167,9 @@ namespace ContinuousTests
{
errors.Add($"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Test must require > 0 nodes, or -1 to select all nodes.");
}
else if (test.RequiredNumberOfNodes > config.CodexDeployment.CodexContainers.Length)
else if (test.RequiredNumberOfNodes > config.CodexDeployment.CodexInstances.Length)
{
errors.Add($"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Deployment only has {config.CodexDeployment.CodexContainers.Length}");
errors.Add($"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Deployment only has {config.CodexDeployment.CodexInstances.Length}");
}
}
}
@@ -57,8 +57,8 @@ namespace ContinuousTests.Tests
private void LogRepoStore(ICodexNode codexNode)
{
var response = codexNode.GetDebugRepoStore();
Log.Log($"{codexNode.GetName()} has {string.Join(",", response.Select(r => r.cid))}");
//var response = codexNode.GetDebugRepoStore();
//Log.Log($"{codexNode.GetName()} has {string.Join(",", response.Select(r => r.cid))}");
}
private void LogStoredBytes(ICodexNode node)
@@ -90,8 +90,8 @@ namespace ContinuousTests.Tests
private void LogBlockExchangeStatus(ICodexNode codexNode, string msg)
{
var response = codexNode.GetDebugBlockExchange();
Log.Log($"{codexNode.GetName()} {msg}: {JsonConvert.SerializeObject(response)}");
//var response = codexNode.GetDebugBlockExchange();
//Log.Log($"{codexNode.GetName()} {msg}: {JsonConvert.SerializeObject(response)}");
}
}
}
+14 -6
View File
@@ -1,14 +1,16 @@
set -e
replication=5
name=testnamehere
filter=TwoClient
replication=$DNR_REP
name=$DNR_NAME
filter=$DNR_FILTER
duration=$DNR_DURATION
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-$name-tests-$i \
--deploy-file=codex-deployment-$name-$i.json \
@@ -23,7 +25,8 @@ do
--block-ttl=99999999 \
--block-mi=99999999 \
--block-mn=100 \
--metrics=1 \
--metrics-endpoints=1 \
--metrics-scraper=1 \
--check-connect=1 \
-y
@@ -36,12 +39,17 @@ do
screen -d -m dotnet run \
--kube-config=/opt/kubeconfig.yaml \
--codex-deployment=codex-deployment-$name-$i.json \
--log-path=logs-$name-$i \
--log-path=/var/log/codex-continuous-tests/logs-$name-$i \
--data-path=data-$name-$i \
--keep=1 \
--stop=1 \
--filter=$filter \
--cleanup=1 \
--full-container-logs=1 \
--target-duration=172800 # 48 hours
--target-duration=$duration
sleep 30
done
echo "Done! Sleeping indefinitely..."
while true; do sleep 1d; done
+1
View File
@@ -1,6 +1,7 @@
dotnet run \
--kube-config=/opt/kubeconfig.yaml \
--codex-deployment=codex-deployment.json \
--log-path=/var/log/codex-continuous-tests/logs \
--keep=1 \
--stop=10 \
--target-duration=172800 # 48 hours
@@ -42,21 +42,22 @@ namespace CodexTests.BasicTests
{
foreach (var node in nodes)
{
Time.Retry(() => AssertBlockExchangeIsEmpty(node), nameof(AssertExchangeIsEmpty));
// 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.");
}
//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.");
//}
}
}
+5 -5
View File
@@ -60,7 +60,7 @@ namespace CodexTests.BasicTests
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, isValidator: true)
.WithSimulateProofFailures(failEveryNProofs: 3));
AssertBalance(geth, contracts, seller, Is.EqualTo(sellerInitialBalance));
AssertBalance(contracts, seller, Is.EqualTo(sellerInitialBalance));
seller.Marketplace.MakeStorageAvailable(
size: 10.GB(),
minPriceForTotalSpace: 1.TestTokens(),
@@ -73,7 +73,7 @@ namespace CodexTests.BasicTests
.WithBootstrapNode(seller)
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: buyerInitialBalance));
AssertBalance(geth, contracts, buyer, Is.EqualTo(buyerInitialBalance));
AssertBalance(contracts, buyer, Is.EqualTo(buyerInitialBalance));
var contentId = buyer.UploadFile(testFile);
var purchaseContract = buyer.Marketplace.RequestStorage(contentId,
@@ -85,12 +85,12 @@ namespace CodexTests.BasicTests
purchaseContract.WaitForStorageContractStarted(fileSize);
AssertBalance(geth, contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed.");
AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed.");
purchaseContract.WaitForStorageContractFinished();
AssertBalance(geth, contracts, seller, Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage.");
AssertBalance(geth, contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage.");
AssertBalance(contracts, seller, Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage.");
AssertBalance(contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage.");
}
}
}
+2 -2
View File
@@ -77,9 +77,9 @@ namespace CodexTests
return onlineCodexNodes;
}
public void AssertBalance(IGethNode gethNode, ICodexContracts contracts, ICodexNode codexNode, Constraint constraint, string msg = "")
public void AssertBalance(ICodexContracts contracts, ICodexNode codexNode, Constraint constraint, string msg = "")
{
AssertHelpers.RetryAssert(constraint, () => contracts.GetTestTokenBalance(gethNode, codexNode), nameof(AssertBalance) + msg);
AssertHelpers.RetryAssert(constraint, () => contracts.GetTestTokenBalance(codexNode), nameof(AssertBalance) + msg);
}
protected virtual void OnCodexSetup(ICodexSetup setup)
+40
View File
@@ -0,0 +1,40 @@
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);
}
public bool IsAdminChannel(ISocketMessageChannel channel)
{
return channel.Name == Program.Config.AdminChannelName;
}
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();
}
}
}
+27
View File
@@ -0,0 +1,27 @@
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.Container).ToArray();
var group = ci.WrapCodexContainers(codexContainers);
await Execute(context, group);
}
protected abstract Task Execute(CommandContext context, ICodexNodeGroup codexGroup);
}
}
+62
View File
@@ -0,0 +1,62 @@
using Discord.WebSocket;
using BiblioTech.Options;
using Discord;
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
{
var context = new CommandContext(command, command.Data.Options);
await command.RespondAsync(StartingMessage, ephemeral: IsEphemeral(context));
await Invoke(context);
}
catch (Exception ex)
{
await command.FollowupAsync("Something failed while trying to do that...", ephemeral: true);
Console.WriteLine(ex);
}
}
private bool IsEphemeral(CommandContext context)
{
if (IsSenderAdmin(context.Command) && IsInAdminChannel(context.Command)) return false;
return true;
}
protected abstract Task Invoke(CommandContext context);
protected bool IsSenderAdmin(SocketSlashCommand command)
{
return Program.AdminChecker.IsUserAdmin(command.User.Id);
}
protected bool IsInAdminChannel(SocketSlashCommand command)
{
return Program.AdminChecker.IsAdminChannel(command.Channel);
}
protected IUser GetUserFromCommand(UserOption userOption, CommandContext context)
{
var targetUser = userOption.GetUser(context);
if (IsSenderAdmin(context.Command) && targetUser != null) return targetUser;
return context.Command.User;
}
}
}
+36
View File
@@ -0,0 +1,36 @@
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);
}
}
}
+31
View File
@@ -0,0 +1,31 @@
using BiblioTech.Options;
using CodexContractsPlugin;
using CodexPlugin;
using Core;
using GethPlugin;
namespace BiblioTech
{
public abstract class BaseGethCommand : BaseDeploymentCommand
{
private readonly CoreInterface ci;
public BaseGethCommand(CoreInterface ci)
{
this.ci = ci;
}
protected override async Task ExecuteDeploymentCommand(CommandContext context, CodexDeployment codexDeployment)
{
var gethDeployment = codexDeployment.GethDeployment;
var contractsDeployment = codexDeployment.CodexContractsDeployment;
var gethNode = ci.WrapGethDeployment(gethDeployment);
var contracts = ci.WrapCodexContractsDeployment(gethNode, contractsDeployment);
await Execute(context, gethNode, contracts);
}
protected abstract Task Execute(CommandContext context, 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.Build());
}
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);
}
}
}
}
+362
View File
@@ -0,0 +1,362 @@
using BiblioTech.Options;
using CodexPlugin;
using Core;
using Newtonsoft.Json;
namespace BiblioTech.Commands
{
public class AdminCommand : BaseCommand
{
private readonly ClearUserAssociationCommand clearCommand = new ClearUserAssociationCommand();
private readonly ReportCommand reportCommand = new ReportCommand();
private readonly DeployListCommand deployListCommand = new DeployListCommand();
private readonly DeployUploadCommand deployUploadCommand = new DeployUploadCommand();
private readonly DeployRemoveCommand deployRemoveCommand = new DeployRemoveCommand();
private readonly WhoIsCommand whoIsCommand = new WhoIsCommand();
private readonly NetInfoCommand netInfoCommand;
private readonly DebugPeerCommand debugPeerCommand;
public AdminCommand(CoreInterface ci)
{
netInfoCommand = new NetInfoCommand(ci);
debugPeerCommand = new DebugPeerCommand(ci);
}
public override string Name => "admin";
public override string StartingMessage => "...";
public override string Description => "Admins only.";
public override CommandOption[] Options => new CommandOption[]
{
clearCommand,
reportCommand,
deployListCommand,
deployUploadCommand,
deployRemoveCommand,
whoIsCommand,
netInfoCommand,
debugPeerCommand
};
protected override async Task Invoke(CommandContext context)
{
if (!IsSenderAdmin(context.Command))
{
await context.Followup("You're not an admin.");
return;
}
if (!IsInAdminChannel(context.Command))
{
await context.Followup("Please use admin commands only in the admin channel.");
return;
}
await clearCommand.CommandHandler(context);
await reportCommand.CommandHandler(context);
await deployListCommand.CommandHandler(context);
await deployUploadCommand.CommandHandler(context);
await deployRemoveCommand.CommandHandler(context);
await whoIsCommand.CommandHandler(context);
await netInfoCommand.CommandHandler(context);
await debugPeerCommand.CommandHandler(context);
}
public class ClearUserAssociationCommand : SubCommandOption
{
private readonly UserOption userOption = new UserOption("User to clear Eth address for.", true);
public ClearUserAssociationCommand()
: base("clear", "Admin only. Clears current Eth address for a user, allowing them to set a new one.")
{
}
public override CommandOption[] Options => new[] { userOption };
protected override async Task onSubCommand(CommandContext context)
{
var user = userOption.GetUser(context);
if (user == null)
{
await context.Followup("Failed to get user ID");
return;
}
Program.UserRepo.ClearUserAssociatedAddress(user);
await context.Followup("Done.");
}
}
public class ReportCommand : SubCommandOption
{
private readonly UserOption userOption = new UserOption(
description: "User to report history for.",
isRequired: true);
public ReportCommand()
: base("report", "Admin only. Reports bot-interaction history for a user.")
{
}
public override CommandOption[] Options => new[] { userOption };
protected override async Task onSubCommand(CommandContext context)
{
var user = userOption.GetUser(context);
if (user == null)
{
await context.Followup("Failed to get user ID");
return;
}
var report = string.Join(Environment.NewLine, Program.UserRepo.GetInteractionReport(user));
if (report.Length > 1900)
{
var filename = $"user-{user.Username}.log";
await context.FollowupWithAttachement(filename, report);
}
else
{
await context.Followup(report);
}
}
}
public class DeployListCommand : SubCommandOption
{
public DeployListCommand()
: base("list", "Lists current deployments.")
{
}
protected override async Task onSubCommand(CommandContext context)
{
var deployments = Program.DeploymentFilesMonitor.GetDeployments();
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.");
}
}
}
public class WhoIsCommand : SubCommandOption
{
private readonly UserOption userOption = new UserOption("User", isRequired: false);
private readonly EthAddressOption ethAddressOption = new EthAddressOption(isRequired: false);
public WhoIsCommand()
: base(name: "whois",
description: "Fetches info about a user or ethAddress in the testnet.")
{
}
public override CommandOption[] Options => new CommandOption[]
{
userOption,
ethAddressOption
};
protected override async Task onSubCommand(CommandContext context)
{
var user = userOption.GetUser(context);
var ethAddr = await ethAddressOption.Parse(context);
if (user != null)
{
await context.Followup(Program.UserRepo.GetUserReport(user));
}
if (ethAddr != null)
{
await context.Followup(Program.UserRepo.GetUserReport(ethAddr));
}
}
}
public abstract class AdminDeploymentCommand : SubCommandOption
{
private readonly CoreInterface ci;
public AdminDeploymentCommand(CoreInterface ci, string name, string description)
: base(name, description)
{
this.ci = ci;
}
protected async Task OnDeployment(CommandContext context, Func<ICodexNodeGroup, string, Task> action)
{
var deployment = Program.DeploymentFilesMonitor.GetDeployments().SingleOrDefault();
if (deployment == null)
{
await context.Followup("No deployment found.");
return;
}
try
{
var group = ci.WrapCodexContainers(deployment.CodexInstances.Select(i => i.Container).ToArray());
await action(group, deployment.Metadata.Name);
}
catch (Exception ex)
{
await context.Followup("Failed to wrap nodes with exception: " + ex);
}
}
}
public class NetInfoCommand : AdminDeploymentCommand
{
public NetInfoCommand(CoreInterface ci)
: base(ci, name: "netinfo",
description: "Fetches info endpoints of codex nodes.")
{
}
protected override async Task onSubCommand(CommandContext context)
{
await OnDeployment(context, async (group, name) =>
{
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(" ", "-");
}
}
}
@@ -0,0 +1,42 @@
using BiblioTech.Options;
using CodexContractsPlugin;
using Core;
using GethPlugin;
namespace BiblioTech.Commands
{
public class GetBalanceCommand : BaseGethCommand
{
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(CoreInterface ci, UserAssociateCommand userAssociateCommand)
: base(ci)
{
this.userAssociateCommand = userAssociateCommand;
}
public override string Name => "balance";
public override string StartingMessage => RandomBusyMessage.Get();
public override string Description => "Shows Eth and TestToken balance of an eth address.";
public override CommandOption[] Options => new[] { optionalUser };
protected override async Task Execute(CommandContext context, IGethNode gethNode, ICodexContracts contracts)
{
var userId = GetUserFromCommand(optionalUser, context);
var addr = Program.UserRepo.GetCurrentAddressForUser(userId);
if (addr == null)
{
await context.Followup($"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(addr);
await context.Followup($"{context.Command.User.Username} has {eth} and {testTokens}.");
}
}
}
+85
View File
@@ -0,0 +1,85 @@
using BiblioTech.Options;
using CodexContractsPlugin;
using Core;
using GethPlugin;
namespace BiblioTech.Commands
{
public class MintCommand : BaseGethCommand
{
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(CoreInterface ci, UserAssociateCommand userAssociateCommand)
: base(ci)
{
this.userAssociateCommand = userAssociateCommand;
}
public override string Name => "mint";
public override string StartingMessage => RandomBusyMessage.Get();
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(CommandContext context, IGethNode gethNode, ICodexContracts contracts)
{
var userId = GetUserFromCommand(optionalUser, context);
var addr = Program.UserRepo.GetCurrentAddressForUser(userId);
if (addr == null)
{
await context.Followup($"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(contracts, addr, report);
Program.UserRepo.AddMintEventForUser(userId, addr, sentEth, mintedTokens);
await context.Followup(string.Join(Environment.NewLine, report));
}
private TestToken ProcessTokens(ICodexContracts contracts, EthAddress addr, List<string> report)
{
if (ShouldMintTestTokens(contracts, addr))
{
contracts.MintTestTokens(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(ICodexContracts contracts, EthAddress addr)
{
var testTokens = contracts.GetTestTokenBalance(addr);
return testTokens.Amount < 64m;
}
private bool ShouldSendEth(IGethNode gethNode, EthAddress addr)
{
var eth = gethNode.GetEthBalance(addr);
return eth.Eth < 1.0m;
}
}
}
+61
View File
@@ -0,0 +1,61 @@
using BiblioTech.Options;
using CodexPlugin;
using Core;
namespace BiblioTech.Commands
{
public class SprCommand : BaseCodexCommand
{
private readonly Random random = new Random();
private readonly List<string> sprCache = new List<string>();
private DateTime lastUpdate = DateTime.MinValue;
public SprCommand(CoreInterface ci) : base(ci)
{
}
public override string Name => "boot";
public override string StartingMessage => RandomBusyMessage.Get();
public override string Description => "Gets an SPR. (Signed peer record, used for bootstrapping.)";
protected override async Task<bool> OnInvoke(CommandContext context)
{
if (ShouldUpdate())
{
return true;
}
await ReplyWithRandomSpr(context);
return false;
}
protected override async Task Execute(CommandContext context, ICodexNodeGroup codexGroup)
{
lastUpdate = DateTime.UtcNow;
sprCache.Clear();
var infos = codexGroup.Select(c => c.GetDebugInfo()).ToArray();
sprCache.AddRange(infos.Select(i => i.spr));
await ReplyWithRandomSpr(context);
}
private async Task ReplyWithRandomSpr(CommandContext context)
{
if (!sprCache.Any())
{
await context.Followup("I'm sorry, no SPRs are available... :c");
return;
}
var i = random.Next(0, sprCache.Count);
var spr = sprCache[i];
await context.Followup($"Your SPR: '{spr}'");
}
private bool ShouldUpdate()
{
return (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10);
}
}
}
@@ -0,0 +1,43 @@
using BiblioTech.Options;
namespace BiblioTech.Commands
{
public class UserAssociateCommand : BaseCommand
{
private readonly EthAddressOption ethOption = new EthAddressOption(isRequired: false);
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 => RandomBusyMessage.Get();
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(CommandContext context)
{
var user = GetUserFromCommand(optionalUser, context);
var data = await ethOption.Parse(context);
if (data == null) return;
var currentAddress = Program.UserRepo.GetCurrentAddressForUser(user);
if (currentAddress != null && !IsSenderAdmin(context.Command))
{
await context.Followup($"You've already set your Ethereum address to {currentAddress}.");
return;
}
// private commands
var result = Program.UserRepo.AssociateUserWithAddress(user, data);
if (result)
{
await context.Followup("Done! Thank you for joining the test net!");
}
else
{
await context.Followup("That didn't work.");
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
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("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")]
public string DataPath { get; set; } = "datapath";
[Uniform("admin-role", "a", "ADMINROLE", true, "Name of the Discord server admin role")]
public string AdminRoleName { get; set; } = string.Empty;
[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 EndpointsPath
{
get
{
return Path.Combine(DataPath, "endpoints");
}
}
public string UserDataPath
{
get
{
return Path.Combine(DataPath, "users");
}
}
}
}
@@ -0,0 +1,89 @@
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;
}
}
}
}
@@ -0,0 +1,33 @@
using Discord.WebSocket;
namespace BiblioTech.Options
{
public class CommandContext
{
public CommandContext(SocketSlashCommand command, IReadOnlyCollection<SocketSlashCommandDataOption> options)
{
Command = command;
Options = options;
}
public SocketSlashCommand Command { get; }
public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; }
public async Task Followup(string message)
{
await Command.ModifyOriginalResponseAsync(m =>
{
m.Content = message;
});
}
public async Task FollowupWithAttachement(string filename, string content)
{
using var fileStream = new MemoryStream();
using var streamWriter = new StreamWriter(fileStream);
await streamWriter.WriteAsync(content);
await Command.FollowupWithFileAsync(fileStream, filename);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
using Discord;
namespace BiblioTech.Options
{
public abstract 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; }
public virtual SlashCommandOptionBuilder Build()
{
return new SlashCommandOptionBuilder()
.WithName(Name)
.WithDescription(Description)
.WithType(Type)
.WithRequired(IsRequired);
}
}
}
@@ -0,0 +1,42 @@
using GethPlugin;
using Nethereum.Util;
namespace BiblioTech.Options
{
public class EthAddressOption : CommandOption
{
public EthAddressOption(bool isRequired)
: base(name: "ethaddress",
description: "Ethereum address starting with '0x'.",
type: Discord.ApplicationCommandOptionType.String,
isRequired)
{
}
public async Task<EthAddress?> Parse(CommandContext context)
{
var ethOptionData = context.Options.SingleOrDefault(o => o.Name == Name);
if (ethOptionData == null)
{
await context.Followup("EthAddress option not received.");
return null;
}
var ethAddressStr = ethOptionData.Value as string;
if (string.IsNullOrEmpty(ethAddressStr))
{
await context.Followup("EthAddress is null or empty.");
return null;
}
if (!AddressUtil.Current.IsValidAddressLength(ethAddressStr) ||
!AddressUtil.Current.IsValidEthereumAddressHexFormat(ethAddressStr))
// !AddressUtil.Current.IsChecksumAddress(ethAddressStr)) - this might make a good option later, but for now it might just annoy users.
{
await context.Followup("EthAddress is not valid.");
return null;
}
return new EthAddress(ethAddressStr);
}
}
}
@@ -0,0 +1,30 @@
using Discord;
namespace BiblioTech.Options
{
public class FileAttachementOption : CommandOption
{
public FileAttachementOption(string name, string description, bool isRequired)
: base(name, description, type: ApplicationCommandOptionType.Attachment, isRequired)
{
}
public async Task<IAttachment?> Parse(CommandContext context)
{
var fileOptionData = context.Options.SingleOrDefault(o => o.Name == Name);
if (fileOptionData == null)
{
await context.Followup("Attachement option not received.");
return null;
}
var attachement = fileOptionData.Value as IAttachment;
if (attachement == null)
{
await context.Followup("Attachement is null or empty.");
return null;
}
return attachement;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
using Discord;
namespace BiblioTech.Options
{
public class StringOption : CommandOption
{
public StringOption(string name, string description, bool isRequired)
: base(name, description, type: ApplicationCommandOptionType.String, isRequired)
{
}
public async Task<string?> Parse(CommandContext context)
{
var strData = context.Options.SingleOrDefault(o => o.Name == Name);
if (strData == null)
{
await context.Followup("String option not received.");
return null;
}
return strData.Value as string;
}
}
}
@@ -0,0 +1,40 @@
using Discord;
namespace BiblioTech.Options
{
public abstract class SubCommandOption : CommandOption
{
public SubCommandOption(string name, string description)
: base(name, description, type: ApplicationCommandOptionType.SubCommand, isRequired: false)
{
}
public override SlashCommandOptionBuilder Build()
{
var builder = base.Build();
foreach (var option in Options)
{
builder.AddOption(option.Build());
}
return builder;
}
public async Task CommandHandler(CommandContext context)
{
var mine = context.Options.SingleOrDefault(o => o.Name == Name);
if (mine == null) return;
await onSubCommand(new CommandContext(context.Command, mine.Options));
}
public virtual CommandOption[] Options
{
get
{
return Array.Empty<CommandOption>();
}
}
protected abstract Task onSubCommand(CommandContext context);
}
}
+19
View File
@@ -0,0 +1,19 @@
using Discord;
namespace BiblioTech.Options
{
public class UserOption : CommandOption
{
public UserOption(string description, bool isRequired)
: base("user", description, ApplicationCommandOptionType.User, isRequired)
{
}
public IUser? GetUser(CommandContext context)
{
var userOptionData = context.Options.SingleOrDefault(o => o.Name == Name);
if (userOptionData == null) return null;
return userOptionData.Value as IUser;
}
}
}
+83
View File
@@ -0,0 +1,83 @@
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();
DeploymentFilesMonitor.Initialize();
EnsurePath(Config.DataPath);
EnsurePath(Config.UserDataPath);
EnsurePath(Config.EndpointsPath);
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 ci = entryPoint.CreateInterface();
var associateCommand = new UserAssociateCommand();
var handler = new CommandHandler(client,
new GetBalanceCommand(ci, associateCommand),
new MintCommand(ci, associateCommand),
new SprCommand(ci),
associateCommand,
new AdminCommand(ci)
);
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;
}
private static void EnsurePath(string path)
{
if (Directory.Exists(path)) return;
Directory.CreateDirectory(path);
}
}
}
+25
View File
@@ -0,0 +1,25 @@
namespace BiblioTech
{
public static class RandomBusyMessage
{
private static readonly Random random = new Random();
private static readonly string[] messages = new[]
{
"Working on it...",
"Doing that...",
"Hang on...",
"Making it so...",
"Reversing the polarity...",
"Factoring the polynomial...",
"Analyzing the wavelengths...",
"Charging the flux-capacitor...",
"Jumping to hyperspace...",
"Computing the ultimate answer..."
};
public static string Get()
{
return messages[random.Next(messages.Length)];
}
}
}
+237
View File
@@ -0,0 +1,237 @@
using CodexContractsPlugin;
using Discord;
using GethPlugin;
using Newtonsoft.Json;
using Utils;
namespace BiblioTech
{
public class UserRepo
{
private readonly object repoLock = new object();
public bool AssociateUserWithAddress(IUser user, EthAddress address)
{
lock (repoLock)
{
return SetUserAddress(user, address);
}
}
public void ClearUserAssociatedAddress(IUser user)
{
lock (repoLock)
{
SetUserAddress(user, null);
}
}
public void AddMintEventForUser(IUser user, EthAddress usedAddress, Ether eth, TestToken tokens)
{
lock (repoLock)
{
var userData = GetOrCreate(user);
userData.MintEvents.Add(new UserMintEvent(DateTime.UtcNow, usedAddress, eth, tokens));
SaveUserData(userData);
}
}
public EthAddress? GetCurrentAddressForUser(IUser user)
{
lock (repoLock)
{
return GetOrCreate(user).CurrentAddress;
}
}
public string[] GetInteractionReport(IUser user)
{
var result = new List<string>
{
$"User report create on {DateTime.UtcNow.ToString("o")}"
};
lock (repoLock)
{
var userData = GetUserData(user);
if (userData == null)
{
result.Add("User has not joined the test net.");
}
else
{
result.Add("User joined on " + userData.CreatedUtc.ToString("o"));
result.Add("Current address: " + userData.CurrentAddress);
foreach (var ae in userData.AssociateEvents)
{
result.Add($"{ae.Utc.ToString("o")} - Address set to: {ae.NewAddress}");
}
foreach (var me in userData.MintEvents)
{
result.Add($"{me.Utc.ToString("o")} - Minted {me.EthReceived} and {me.TestTokensMinted} to {me.UsedAddress}.");
}
}
}
return result.ToArray();
}
public string GetUserReport(IUser user)
{
var userData = GetUserData(user);
if (userData == null) return "User has not joined the test net.";
return userData.CreateOverview();
}
public string GetUserReport(EthAddress ethAddress)
{
var userData = GetUserDataForAddress(ethAddress);
if (userData == null) return "No user is using this eth address.";
return userData.CreateOverview();
}
private bool SetUserAddress(IUser user, EthAddress? address)
{
if (GetUserDataForAddress(address) != null)
{
return false;
}
var userData = GetOrCreate(user);
userData.CurrentAddress = address;
userData.AssociateEvents.Add(new UserAssociateAddressEvent(DateTime.UtcNow, address));
SaveUserData(userData);
return true;
}
private UserData? GetUserData(IUser user)
{
var filename = GetFilename(user);
if (!File.Exists(filename))
{
return null;
}
return JsonConvert.DeserializeObject<UserData>(File.ReadAllText(filename))!;
}
private UserData GetOrCreate(IUser user)
{
var userData = GetUserData(user);
if (userData == null)
{
return CreateAndSaveNewUserData(user);
}
return userData;
}
private UserData CreateAndSaveNewUserData(IUser user)
{
var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>());
SaveUserData(newUser);
return newUser;
}
private UserData? GetUserDataForAddress(EthAddress? address)
{
if (address == null) return null;
// If this becomes a performance problem, switch to in-memory cached list.
var files = Directory.GetFiles(Program.Config.UserDataPath);
foreach (var file in files)
{
try
{
var user = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(file))!;
if (user.CurrentAddress != null &&
user.CurrentAddress.Address == address.Address)
{
return user;
}
}
catch { }
}
return null;
}
private void SaveUserData(UserData userData)
{
var filename = GetFilename(userData);
if (File.Exists(filename)) File.Delete(filename);
File.WriteAllText(filename, JsonConvert.SerializeObject(userData));
}
private static string GetFilename(IUser user)
{
return GetFilename(user.Id);
}
private static string GetFilename(UserData userData)
{
return GetFilename(userData.DiscordId);
}
private static string GetFilename(ulong discordId)
{
return Path.Combine(Program.Config.UserDataPath, discordId.ToString() + ".json");
}
}
public class UserData
{
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents)
{
DiscordId = discordId;
Name = name;
CreatedUtc = createdUtc;
CurrentAddress = currentAddress;
AssociateEvents = associateEvents;
MintEvents = mintEvents;
}
public ulong DiscordId { get; }
public string Name { get; }
public DateTime CreatedUtc { get; }
public EthAddress? CurrentAddress { get; set; }
public List<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> MintEvents { get; }
public string CreateOverview()
{
var nl = Environment.NewLine;
return
$"name: '{Name}' - id:{DiscordId}{nl}" +
$"joined: {CreatedUtc.ToString("o")}{nl}" +
$"current address: {CurrentAddress}{nl}" +
$"{AssociateEvents.Count + MintEvents.Count} total bot events.";
}
}
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
@@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexDiscordBotPlugin\CodexDiscordBotPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
<ProjectReference Include="..\..\Tests\CodexTests\CodexTests.csproj" />
</ItemGroup>
+41 -13
View File
@@ -38,13 +38,22 @@ namespace CodexNetDeployer
s.WithName(name);
s.WithLogLevel(config.CodexLogLevel, new CodexLogCustomTopics(config.Discv5LogLevel, config.Libp2pLogLevel));
s.WithStorageQuota(config.StorageQuota!.Value.MB());
s.EnableMarketplace(gethNode, contracts, 100.Eth(), config.InitialTestTokens.TestTokens(), validatorsLeft > 0);
s.EnableMetrics();
if (config.ShouldMakeStorageAvailable)
{
s.EnableMarketplace(gethNode, contracts, 100.Eth(), config.InitialTestTokens.TestTokens(), validatorsLeft > 0);
}
if (bootstrapNode != null) s.WithBootstrapNode(bootstrapNode);
if (config.MetricsEndpoints) s.EnableMetrics();
if (config.BlockTTL != Configuration.SecondsIn1Day) s.WithBlockTTL(TimeSpan.FromSeconds(config.BlockTTL));
if (config.BlockMI != Configuration.TenMinutes) s.WithBlockMaintenanceInterval(TimeSpan.FromSeconds(config.BlockMI));
if (config.BlockMN != 1000) s.WithBlockMaintenanceNumber(config.BlockMN);
if (config.IsPublicTestNet)
{
s.AsPublicTestNet(CreatePublicTestNetConfig(i));
}
});
var debugInfo = codexNode.GetDebugInfo();
@@ -52,20 +61,26 @@ namespace CodexNetDeployer
{
Console.Write("Online\t");
var response = codexNode.Marketplace.MakeStorageAvailable(
size: config.StorageSell!.Value.MB(),
minPriceForTotalSpace: config.MinPrice.TestTokens(),
maxCollateral: config.MaxCollateral.TestTokens(),
maxDuration: TimeSpan.FromSeconds(config.MaxDuration));
if (!string.IsNullOrEmpty(response))
if (config.ShouldMakeStorageAvailable)
{
Console.Write("Storage available\tOK" + Environment.NewLine);
var response = codexNode.Marketplace.MakeStorageAvailable(
size: config.StorageSell!.Value.MB(),
minPriceForTotalSpace: config.MinPrice.TestTokens(),
maxCollateral: config.MaxCollateral.TestTokens(),
maxDuration: TimeSpan.FromSeconds(config.MaxDuration));
validatorsLeft--;
if (bootstrapNode == null) bootstrapNode = codexNode;
return new CodexNodeStartResult(codexNode);
if (!string.IsNullOrEmpty(response))
{
Console.Write("Storage available\t");
}
else throw new Exception("Failed to make storage available.");
}
Console.Write("OK" + Environment.NewLine);
validatorsLeft--;
if (bootstrapNode == null) bootstrapNode = codexNode;
return new CodexNodeStartResult(codexNode);
}
}
catch (Exception ex)
@@ -83,6 +98,19 @@ namespace CodexNetDeployer
return null;
}
private CodexTestNetConfig CreatePublicTestNetConfig(int i)
{
var discPort = config.PublicDiscPorts.Split(",")[i];
var listenPort = config.PublicListenPorts.Split(",")[i];
return new CodexTestNetConfig
{
PublicNatIP = config.PublicIP,
PublicDiscoveryPort = Convert.ToInt32(discPort),
PublicListenPort = Convert.ToInt32(listenPort)
};
}
private string GetCodexContainerName(int i)
{
if (i == 0) return "BOOTSTRAP";
+83 -22
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";
@@ -31,9 +34,6 @@ namespace CodexNetDeployer
[Uniform("storage-quota", "sq", "STORAGEQUOTA", true, "Storage quota in megabytes used by each Codex node.")]
public int? StorageQuota { get; set; }
[Uniform("storage-sell", "ss", "STORAGESELL", true, "Number of megabytes of storage quota to make available for selling.")]
public int? StorageSell { get; set; }
[Uniform("log-level", "l", "LOGLEVEL", true, "Log level used by each Codex node. [Trace, Debug*, Info, Warn, Error]")]
public CodexLogLevel CodexLogLevel { get; set; } = CodexLogLevel.Debug;
@@ -43,16 +43,22 @@ namespace CodexNetDeployer
[Uniform("log-level-discv5", "ldv5", "LOGLEVELDISCV5", true, "Log level for all discv5 topics. [Trace, Debug, Info, Warn*, Error]")]
public CodexLogLevel Discv5LogLevel { get; set; } = CodexLogLevel.Warn;
[Uniform("test-tokens", "tt", "TESTTOKENS", true, "Initial amount of test-tokens minted for each Codex node.")]
public int InitialTestTokens { get; set; } = int.MaxValue;
[Uniform("make-storage-available", "msa", "MAKESTORAGEAVAILABLE", true, "Is true, storage will be made available using the next configuration parameters.")]
public bool ShouldMakeStorageAvailable { get; set; } = false;
[Uniform("min-price", "mp", "MINPRICE", true, "Minimum price for the storage space for which contracts will be accepted.")]
[Uniform("storage-sell", "ss", "STORAGESELL", false, "Number of megabytes of storage quota to make available for selling.")]
public int? StorageSell { get; set; }
[Uniform("test-tokens", "tt", "TESTTOKENS", false, "Initial amount of test-tokens minted for each Codex node.")]
public int InitialTestTokens { get; set; }
[Uniform("min-price", "mp", "MINPRICE", false, "Minimum price for the storage space for which contracts will be accepted.")]
public int MinPrice { get; set; }
[Uniform("max-collateral", "mc", "MAXCOLLATERAL", true, "Maximum collateral that will be placed for the total storage space.")]
[Uniform("max-collateral", "mc", "MAXCOLLATERAL", false, "Maximum collateral that will be placed for the total storage space.")]
public int MaxCollateral { get; set; }
[Uniform("max-duration", "md", "MAXDURATION", true, "Maximum duration in seconds for contracts which will be accepted.")]
[Uniform("max-duration", "md", "MAXDURATION", false, "Maximum duration in seconds for contracts which will be accepted.")]
public int MaxDuration { get; set; }
[Uniform("block-ttl", "bt", "BLOCKTTL", false, "Block timeout in seconds. Default is 24 hours.")]
@@ -64,8 +70,11 @@ namespace CodexNetDeployer
[Uniform("block-mn", "bmn", "BLOCKMN", false, "Number of blocks maintained per interval. Default is 1000 blocks.")]
public int BlockMN { get; set; } = 1000;
[Uniform("metrics", "m", "METRICS", false, "[true, false]. Determines if metrics will be recorded. Default is false.")]
public bool Metrics { get; set; } = false;
[Uniform("metrics-endpoints", "me", "METRICSENDPOINTS", false, "[true, false]. Determines if metric endpoints will be enabled. Default is false.")]
public bool MetricsEndpoints { get; set; } = false;
[Uniform("metrics-scraper", "ms", "METRICSSCRAPER", false, "[true, false]. Determines if metrics scraper service will be deployed. (Required for certain tests.) Default is false.")]
public bool MetricsScraper { get; set; } = false;
[Uniform("teststype-podlabel", "ttpl", "TESTSTYPE-PODLABEL", false, "Each kubernetes pod will be created with a label 'teststype' with value 'continuous'. " +
"set this option to override the label value.")]
@@ -74,6 +83,45 @@ namespace CodexNetDeployer
[Uniform("check-connect", "cc", "CHECKCONNECT", false, "If true, deployer check ensure peer-connectivity between all deployed nodes after deployment. Default is false.")]
public bool CheckPeerConnection { get; set; } = false;
[Uniform("public-testnet", "ptn", "PUBLICTESTNET", false, "If true, deployment is created for public exposure. Default is false.")]
public bool IsPublicTestNet { get; set; } = false;
[Uniform("public-ip", "pip", "PUBLICIP", false, "Required if public-testnet is true. Public IP address used by nodes for network annoucements.")]
public string PublicIP { get; set; } = string.Empty;
[Uniform("public-discports", "pdps", "PUBLICDISCPORTS", false, "Required if public-testnet is true. Comma-separated port numbers used for discovery. Number must match number of nodes.")]
public string PublicDiscPorts { get; set; } = string.Empty;
[Uniform("public-listenports", "plps", "PUBLICLISTENPORTS", false, "Required if public-testnet is true. Comma-separated port numbers used for listening. Number must match number of nodes.")]
public string PublicListenPorts { get; set; } = string.Empty;
[Uniform("public-gethip", "pgdp", "PUBLICGETHIP", false, "Required if public-testnet is true. Geth's public IP address.")]
public string PublicGethIP { get; set; } = string.Empty;
[Uniform("public-gethdiscport", "pgdp", "PUBLICGETHDISCPORT", false, "Required if public-testnet is true. Single port number used for Geth's public discovery port.")]
public int PublicGethDiscPort { get; set; }
[Uniform("public-gethlistenport", "pglp", "PUBLICGETHLISTENPORT", false, "Required if public-testnet is true. Single port number used for Geth's public listen port.")]
public int PublicGethListenPort { get; set; }
[Uniform("discord-bot", "dbot", "DISCORDBOT", false, "If true, will deploy discord bot. Default is false.")]
public bool DeployDiscordBot { get; set; } = false;
[Uniform("dbot-token", "dbott", "DBOTTOKEN", false, "Required if discord-bot is true. Discord token used by bot.")]
public string DiscordBotToken { get; set; } = string.Empty;
[Uniform("dbot-servername", "dbotsn", "DBOTSERVERNAME", false, "Required if discord-bot is true. Name of the Discord server.")]
public string DiscordBotServerName { get; set; } = string.Empty;
[Uniform("dbot-adminrolename", "dbotarn", "DBOTADMINROLENAME", false, "Required if discord-bot is true. Name of the Discord role which will have access to admin features.")]
public string DiscordBotAdminRoleName { get; set; } = string.Empty;
[Uniform("dbot-adminchannelname", "dbotacn", "DBOTADMINCHANNELNAME", false, "Required if discord-bot is true. Name of the Discord channel in which admin commands are allowed.")]
public string DiscordBotAdminChannelName { get; set; } = string.Empty;
[Uniform("dbot-datapath", "dbotdp", "DBOTDATAPATH", false, "Optional. Path in container where bot will save all data.")]
public string DiscordBotDataPath { get; set; } = string.Empty;
public List<string> Validate()
{
var errors = new List<string>();
@@ -82,9 +130,6 @@ namespace CodexNetDeployer
StringIsSet(nameof(KubeConfigFile), KubeConfigFile, errors);
StringIsSet(nameof(TestsTypePodLabel), TestsTypePodLabel, errors);
ForEachProperty(
onInt: (n, v) => IntIsOverZero(n, v, errors));
if (NumberOfValidators > NumberOfCodexNodes)
{
errors.Add($"{nameof(NumberOfValidators)} ({NumberOfValidators}) may not be greater than {nameof(NumberOfCodexNodes)} ({NumberOfCodexNodes}).");
@@ -94,17 +139,33 @@ namespace CodexNetDeployer
errors.Add("StorageSell cannot be greater than or equal to StorageQuota.");
}
return errors;
}
private void ForEachProperty(Action<string, int?> onInt)
{
var properties = GetType().GetProperties();
foreach (var p in properties)
if (ShouldMakeStorageAvailable)
{
if (p.PropertyType == typeof(int?)) onInt(p.Name, (int?)p.GetValue(this)!);
if (p.PropertyType == typeof(int)) onInt(p.Name, (int)p.GetValue(this)!);
IntIsOverZero(nameof(StorageSell), StorageSell, errors);
IntIsOverZero(nameof(InitialTestTokens), InitialTestTokens, errors);
IntIsOverZero(nameof(MinPrice), MinPrice, errors);
IntIsOverZero(nameof(MaxCollateral), MaxCollateral, errors);
IntIsOverZero(nameof(MaxDuration), MaxDuration, errors);
}
if (IsPublicTestNet)
{
if (string.IsNullOrEmpty(PublicIP)) errors.Add("Public IP required when deploying public testnet.");
if (PublicDiscPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public discovery-ports provided does not match number of codex nodes.");
if (PublicListenPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public listen-ports provided does not match number of codex nodes.");
if (PublicGethDiscPort == 0) errors.Add("Geth public discovery port is not set.");
if (PublicGethListenPort == 0) errors.Add("Geth public listen port is not set.");
}
if (DeployDiscordBot)
{
StringIsSet(nameof(DiscordBotToken), DiscordBotToken, errors);
StringIsSet(nameof(DiscordBotServerName), DiscordBotServerName, errors);
StringIsSet(nameof(DiscordBotAdminRoleName), DiscordBotAdminRoleName, errors);
StringIsSet(nameof(DiscordBotAdminChannelName), DiscordBotAdminChannelName, errors);
}
return errors;
}
private static void IntIsOverZero(string variable, int? value, List<string> errors)
+65 -7
View File
@@ -1,4 +1,5 @@
using CodexContractsPlugin;
using CodexDiscordBotPlugin;
using CodexPlugin;
using Core;
using GethPlugin;
@@ -25,6 +26,7 @@ namespace CodexNetDeployer
ProjectPlugin.Load<CodexContractsPlugin.CodexContractsPlugin>();
ProjectPlugin.Load<GethPlugin.GethPlugin>();
ProjectPlugin.Load<MetricsPlugin.MetricsPlugin>();
ProjectPlugin.Load<CodexDiscordBotPlugin.CodexDiscordBotPlugin>();
entryPoint = CreateEntryPoint(new NullLog());
}
@@ -55,12 +57,12 @@ namespace CodexNetDeployer
var ci = entryPoint.CreateInterface();
Log("Deploying Geth instance...");
var gethDeployment = ci.DeployGeth(s => s.IsMiner().WithName("geth"));
var gethDeployment = DeployGeth(ci);
var gethNode = ci.WrapGethDeployment(gethDeployment);
Log("Geth started. Deploying Codex contracts...");
var contractsDeployment = ci.DeployCodexContracts(gethNode);
var contracts = ci.WrapCodexContractsDeployment(contractsDeployment);
var contracts = ci.WrapCodexContractsDeployment(gethNode, contractsDeployment);
Log("Codex contracts deployed.");
Log("Starting Codex nodes...");
@@ -78,8 +80,11 @@ namespace CodexNetDeployer
CheckPeerConnectivity(startResults);
CheckContainerRestarts(startResults);
var codexContainers = startResults.Select(s => s.CodexNode.Container).ToArray();
return new CodexDeployment(codexContainers, gethDeployment, metricsService, CreateMetadata(startUtc));
var codexInstances = CreateCodexInstances(startResults);
var discordBotContainer = DeployDiscordBot(ci);
return new CodexDeployment(codexInstances, gethDeployment, contractsDeployment, metricsService, discordBotContainer, CreateMetadata(startUtc));
}
private EntryPoint CreateEntryPoint(ILog log)
@@ -98,9 +103,46 @@ namespace CodexNetDeployer
return result;
}
private GethDeployment DeployGeth(CoreInterface ci)
{
return ci.DeployGeth(s =>
{
s.IsMiner();
s.WithName("geth");
if (config.IsPublicTestNet)
{
s.AsPublicTestNet(new GethTestNetConfig(
publicIp: config.PublicGethIP,
discoveryPort: config.PublicGethDiscPort,
listenPort: config.PublicGethListenPort
));
}
});
}
private RunningContainer? DeployDiscordBot(CoreInterface ci)
{
if (!config.DeployDiscordBot) return null;
Log("Deploying Discord bot...");
var rc = ci.DeployCodexDiscordBot(new DiscordBotStartupConfig(
name: "discordbot-" + config.DeploymentName,
token: config.DiscordBotToken,
serverName: config.DiscordBotServerName,
adminRoleName: config.DiscordBotAdminRoleName,
adminChannelName: config.DiscordBotAdminChannelName)
{
DataPath = config.DiscordBotDataPath
});
Log("Discord bot deployed.");
return rc;
}
private RunningContainer? StartMetricsService(CoreInterface ci, List<CodexNodeStartResult> startResults)
{
if (!config.Metrics) return null;
if (!config.MetricsScraper) return null;
Log("Starting metrics service...");
@@ -111,6 +153,21 @@ namespace CodexNetDeployer
return runningContainer;
}
private CodexInstance[] CreateCodexInstances(List<CodexNodeStartResult> startResults)
{
// When freshly started, the Codex nodes are announcing themselves by an incorrect IP address.
// Only after fully initialized do they update to the provided NAT address.
// Therefore, we wait:
Thread.Sleep(TimeSpan.FromSeconds(5));
return startResults.Select(r => CreateCodexInstance(r.CodexNode)).ToArray();
}
private CodexInstance CreateCodexInstance(ICodexNode node)
{
return new CodexInstance(node.Container, node.GetDebugInfo());
}
private string? GetKubeConfig(string kubeConfigFile)
{
if (string.IsNullOrEmpty(kubeConfigFile) || kubeConfigFile.ToLowerInvariant() == "null") return null;
@@ -151,6 +208,7 @@ namespace CodexNetDeployer
private DeploymentMetadata CreateMetadata(DateTime startUtc)
{
return new DeploymentMetadata(
name: config.DeploymentName,
startUtc: startUtc,
finishedUtc: DateTime.UtcNow,
kubeNamespace: config.KubeNamespace,
@@ -180,9 +238,9 @@ namespace CodexNetDeployer
return TimeSpan.FromSeconds(2);
}
public TimeSpan HttpCallRetryTime()
public int HttpMaxNumberOfRetries()
{
return TimeSpan.FromSeconds(2);
return 2;
}
public TimeSpan HttpCallTimeout()
+4 -2
View File
@@ -26,11 +26,13 @@ public class Program
var deployer = new Deployer(config);
deployer.AnnouncePlugins();
if (!args.Any(a => a == "-y"))
if (config.IsPublicTestNet || !args.Any(a => a == "-y"))
{
if (config.IsPublicTestNet) Console.WriteLine("Deployment is configured as public testnet.");
Console.WriteLine("Does the above config look good? [y/n]");
if (Console.ReadLine()!.ToLowerInvariant() != "y") return;
Console.WriteLine("I think so too.");
if (config.IsPublicTestNet) Console.WriteLine("You better be right about that, cause it's going live right now.");
else Console.WriteLine("I think so too.");
}
var deployment = deployer.Deploy();
@@ -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 \
@@ -6,6 +7,7 @@ dotnet run \
--validators=3 \
--log-level=Trace \
--storage-quota=2048 \
--make-storage-available=1 \
--storage-sell=1024 \
--min-price=1024 \
--max-collateral=1024 \
@@ -13,5 +15,6 @@ dotnet run \
--block-ttl=180 \
--block-mi=120 \
--block-mn=10000 \
--metrics=1 \
--metrics-endpoints=1 \
--metrics-scraper=1 \
--check-connect=1
@@ -0,0 +1,31 @@
dotnet run \
--deploy-name=codex-public-testnet \
--kube-config=/opt/kubeconfig.yaml \
--kube-namespace=codex-public-testnet \
--deploy-file=codex-public-testnet-deployment.json \
--nodes=3 \
--validators=1 \
--log-level=Trace \
--storage-quota=2048 \
--make-storage-available=0 \
--block-ttl=180 \
--block-mi=120 \
--block-mn=10000 \
--metrics-endpoints=1 \
--metrics-scraper=0 \
--check-connect=1 \
\
--public-testnet=1 \
--public-ip=1.2.3.4 \
--public-discports=20010,20020,20030 \
--public-listenports=20011,20021,20031 \
--public-gethip=1.2.3.5 \
--public-gethdiscport=20040 \
--public-gethlistenport=20041 \
\
--discord-bot=1 \
--dbot-token=tokenhere \
--dbot-servername=namehere \
--dbot-adminrolename=alsonamehere \
--dbot-adminchannelname=channelname \
--dbot-datapath=/var/botdata
@@ -0,0 +1,42 @@
using KubernetesWorkflow;
namespace TestClusterStarter
{
public class ClusterTestSetup
{
public ClusterTestSetup(ClusterTestSpec[] specs)
{
Specs = specs;
}
public ClusterTestSpec[] Specs { get; }
}
public class ClusterTestSpec
{
public ClusterTestSpec(string name, string filter, int replication, int durationSeconds, string? codexImageOverride)
{
Name = name;
Filter = filter;
Replication = replication;
DurationSeconds = durationSeconds;
CodexImageOverride = codexImageOverride;
}
public string Name { get; }
public string Filter { get; }
public int Replication { get; }
public int DurationSeconds { get; }
public string? CodexImageOverride { get; }
}
public class ClusterTestDeployment
{
public ClusterTestDeployment(RunningContainer[] containers)
{
Containers = containers;
}
public RunningContainer[] Containers { get; }
}
}
+10
View File
@@ -0,0 +1,10 @@
using ArgsUniform;
namespace TestClusterStarter
{
public class Configuration
{
[Uniform("kube-config", "kc", "KUBECONFIG", true, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
public string KubeConfigFile { get; set; } = "null";
}
}
+50
View File
@@ -0,0 +1,50 @@
using ArgsUniform;
using Core;
using DeployAndRunPlugin;
using KubernetesWorkflow;
using Logging;
using Newtonsoft.Json;
using TestClusterStarter;
public class Program
{
private const string SpecsFile = "TestSpecs.json";
public static void Main(string[] args)
{
var argsUniform = new ArgsUniform<TestClusterStarter.Configuration>(() => { }, args);
var config = argsUniform.Parse();
ProjectPlugin.Load<DeployAndRunPlugin.DeployAndRunPlugin>();
if (!File.Exists(SpecsFile))
{
File.WriteAllText(SpecsFile, JsonConvert.SerializeObject(new ClusterTestSetup(new[]
{
new ClusterTestSpec("example", "peer", 2, Convert.ToInt32(TimeSpan.FromDays(2).TotalSeconds), "imageoverride")
})));
return;
}
var specs = JsonConvert.DeserializeObject<ClusterTestSetup>(File.ReadAllText(SpecsFile))!;
var kConfig = new KubernetesWorkflow.Configuration(config.KubeConfigFile, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(10), kubernetesNamespace: "default");
var entryPoint = new EntryPoint(new ConsoleLog(), kConfig, "datafolder");
var ci = entryPoint.CreateInterface();
var rcs = new List<RunningContainer>();
foreach (var spec in specs.Specs)
{
var rc = ci.DeployAndRunContinuousTests(new RunConfig(
name: spec.Name,
filter: spec.Filter,
duration: TimeSpan.FromSeconds(spec.DurationSeconds),
replications: spec.Replication,
codexImageOverride: spec.CodexImageOverride));
rcs.Add(rc);
}
var deployment = new ClusterTestDeployment(rcs.ToArray());
File.WriteAllText("clustertest-deployment.json", JsonConvert.SerializeObject(deployment, Formatting.Indented));
}
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
<ProjectReference Include="..\..\Framework\KubernetesWorkflow\KubernetesWorkflow.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\DeployAndRunPlugin\DeployAndRunPlugin.csproj" />
</ItemGroup>
</Project>
+28
View File
@@ -43,6 +43,14 @@ 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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexDiscordBotPlugin", "ProjectPlugins\CodexDiscordBotPlugin\CodexDiscordBotPlugin.csproj", "{FB96A58B-F7F0-490A-9A85-72A96A018042}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestClusterStarter", "Tools\TestClusterStarter\TestClusterStarter.csproj", "{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAndRunPlugin", "ProjectPlugins\DeployAndRunPlugin\DeployAndRunPlugin.csproj", "{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -113,6 +121,22 @@ 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
{FB96A58B-F7F0-490A-9A85-72A96A018042}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB96A58B-F7F0-490A-9A85-72A96A018042}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB96A58B-F7F0-490A-9A85-72A96A018042}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB96A58B-F7F0-490A-9A85-72A96A018042}.Release|Any CPU.Build.0 = Release|Any CPU
{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA}.Release|Any CPU.Build.0 = Release|Any CPU
{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -134,6 +158,10 @@ 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}
{FB96A58B-F7F0-490A-9A85-72A96A018042} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124}
{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}
+2
View File
@@ -0,0 +1,2 @@
docker build -f deployandrun.Dockerfile -t thatbenbierens/dist-tests-deployandrun:initial ..
docker push thatbenbierens/dist-tests-deployandrun:initial
+12
View File
@@ -0,0 +1,12 @@
FROM mcr.microsoft.com/dotnet/sdk:7.0
RUN apt-get update && apt-get install -y screen
WORKDIR /app
COPY --chmod=0755 docker/docker-dnr-entrypoint.sh /
COPY ./Tools ./Tools
COPY ./Tests ./Tests
COPY ./Framework ./Framework
COPY ./ProjectPlugins ./ProjectPlugins
ENTRYPOINT ["/docker-dnr-entrypoint.sh"]
CMD ["/bin/bash", "deploy-and-run.sh"]
+9
View File
@@ -9,3 +9,12 @@ services:
- KUBECONFIG=/opt/kubeconfig
- LOGPATH=/opt/logs
- RUNNERLOCATION=ExternalToCluster
continuous-test-run:
image: thatbenbierens/dist-tests-deployandrun:initial
environment:
# - CODEXDOCKERIMAGE=imageoverride
- DNR_REP=3
- DNR_NAME=Tryout
- DNR_FILTER=PeernBeer
- DNR_DURATION=172800
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
echo "Running continuous tests..."
cd /app/Tests/CodexContinuousTests
exec "$@"