2023-03-19 09:49:03 +00:00
|
|
|
|
using k8s;
|
|
|
|
|
using k8s.Models;
|
2023-03-19 10:40:05 +00:00
|
|
|
|
using NUnit.Framework;
|
2023-03-19 09:49:03 +00:00
|
|
|
|
|
2023-03-21 12:20:21 +00:00
|
|
|
|
namespace CodexDistTestCore
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
|
|
|
|
public interface IK8sManager
|
|
|
|
|
{
|
2023-03-21 14:17:48 +00:00
|
|
|
|
IOnlineCodexNodes BringOnline(OfflineCodexNodes node);
|
|
|
|
|
IOfflineCodexNodes BringOffline(IOnlineCodexNodes node);
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class K8sManager : IK8sManager
|
|
|
|
|
{
|
2023-03-21 07:23:15 +00:00
|
|
|
|
public const string K8sNamespace = "codex-test-namespace";
|
2023-03-19 10:18:56 +00:00
|
|
|
|
private readonly CodexDockerImage dockerImage = new CodexDockerImage();
|
2023-03-21 14:17:48 +00:00
|
|
|
|
private readonly NumberSource activeDeploymentOrderNumberSource = new NumberSource(0);
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private readonly List<OnlineCodexNodes> activeCodexNodes = new List<OnlineCodexNodes>();
|
2023-03-20 08:23:32 +00:00
|
|
|
|
private readonly List<string> knownActivePodNames = new List<string>();
|
2023-03-21 07:23:15 +00:00
|
|
|
|
private readonly IFileManager fileManager;
|
2023-03-19 09:49:03 +00:00
|
|
|
|
|
|
|
|
|
public K8sManager(IFileManager fileManager)
|
|
|
|
|
{
|
|
|
|
|
this.fileManager = fileManager;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
public IOnlineCodexNodes BringOnline(OfflineCodexNodes offline)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
|
|
|
|
var client = CreateClient();
|
|
|
|
|
EnsureTestNamespace(client);
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
var containers = CreateContainers(offline.NumberOfNodes);
|
|
|
|
|
var online = containers.Select(c => new OnlineCodexNode(fileManager, c)).ToArray();
|
|
|
|
|
var result = new OnlineCodexNodes(activeDeploymentOrderNumberSource.GetNextNumber(), offline, this, online);
|
|
|
|
|
activeCodexNodes.Add(result);
|
2023-03-19 09:49:03 +00:00
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
CreateDeployment(client, result, offline);
|
|
|
|
|
CreateService(result, client);
|
2023-03-19 09:49:03 +00:00
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
WaitUntilOnline(result, client);
|
|
|
|
|
TestLog.Log($"{offline.NumberOfNodes} Codex nodes online.");
|
2023-03-21 14:17:48 +00:00
|
|
|
|
|
|
|
|
|
return result;
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private CodexNodeContainer[] CreateContainers(int number)
|
|
|
|
|
{
|
|
|
|
|
var factory = new CodexNodeContainerFactory();
|
|
|
|
|
var containers = new List<CodexNodeContainer>();
|
|
|
|
|
for (var i = 0; i < number; i++) containers.Add(factory.CreateNext());
|
|
|
|
|
return containers.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:17:48 +00:00
|
|
|
|
public IOfflineCodexNodes BringOffline(IOnlineCodexNodes node)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
|
|
|
|
var client = CreateClient();
|
|
|
|
|
|
2023-03-19 10:40:05 +00:00
|
|
|
|
var activeNode = GetAndRemoveActiveNodeFor(node);
|
2023-03-19 09:49:03 +00:00
|
|
|
|
|
|
|
|
|
var deploymentName = activeNode.Deployment.Name();
|
|
|
|
|
BringOffline(activeNode, client);
|
|
|
|
|
WaitUntilOffline(deploymentName, client);
|
2023-03-20 10:37:02 +00:00
|
|
|
|
TestLog.Log($"{activeNode.Describe()} offline.");
|
2023-03-19 09:49:03 +00:00
|
|
|
|
|
2023-03-19 10:40:05 +00:00
|
|
|
|
return activeNode.Origin;
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void DeleteAllResources()
|
|
|
|
|
{
|
|
|
|
|
var client = CreateClient();
|
|
|
|
|
|
|
|
|
|
DeleteNamespace(client);
|
|
|
|
|
|
|
|
|
|
WaitUntilZeroPods(client);
|
|
|
|
|
WaitUntilNamespaceDeleted(client);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
public void FetchAllPodsLogs(Action<int, string, Stream> onLog)
|
2023-03-20 10:37:02 +00:00
|
|
|
|
{
|
|
|
|
|
var client = CreateClient();
|
2023-03-21 14:44:21 +00:00
|
|
|
|
foreach (var node in activeCodexNodes)
|
2023-03-20 10:37:02 +00:00
|
|
|
|
{
|
|
|
|
|
var nodeDescription = node.Describe();
|
|
|
|
|
foreach (var podName in node.ActivePodNames)
|
|
|
|
|
{
|
2023-03-21 07:23:15 +00:00
|
|
|
|
var stream = client.ReadNamespacedPodLog(podName, K8sNamespace);
|
2023-03-21 14:44:21 +00:00
|
|
|
|
onLog(node.OrderNumber, $"{nodeDescription}:{podName}", stream);
|
2023-03-20 10:37:02 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private void BringOffline(OnlineCodexNodes online, Kubernetes client)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
2023-03-21 14:44:21 +00:00
|
|
|
|
DeleteDeployment(client, online);
|
|
|
|
|
DeleteService(client, online);
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Waiting
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private void WaitUntilOnline(OnlineCodexNodes online, Kubernetes client)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
2023-03-20 07:13:19 +00:00
|
|
|
|
WaitUntil(() =>
|
|
|
|
|
{
|
2023-03-21 14:44:21 +00:00
|
|
|
|
online.Deployment = client.ReadNamespacedDeployment(online.Deployment.Name(), K8sNamespace);
|
|
|
|
|
return online.Deployment?.Status.AvailableReplicas != null && online.Deployment.Status.AvailableReplicas > 0;
|
2023-03-20 07:13:19 +00:00
|
|
|
|
});
|
2023-03-20 08:23:32 +00:00
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
AssignActivePodNames(online, client);
|
2023-03-20 08:23:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private void AssignActivePodNames(OnlineCodexNodes online, Kubernetes client)
|
2023-03-20 08:23:32 +00:00
|
|
|
|
{
|
2023-03-21 07:23:15 +00:00
|
|
|
|
var pods = client.ListNamespacedPod(K8sNamespace);
|
2023-03-20 08:23:32 +00:00
|
|
|
|
var podNames = pods.Items.Select(p => p.Name());
|
|
|
|
|
foreach (var podName in podNames)
|
|
|
|
|
{
|
|
|
|
|
if (!knownActivePodNames.Contains(podName))
|
|
|
|
|
{
|
|
|
|
|
knownActivePodNames.Add(podName);
|
2023-03-21 14:44:21 +00:00
|
|
|
|
online.ActivePodNames.Add(podName);
|
2023-03-20 08:23:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void WaitUntilOffline(string deploymentName, Kubernetes client)
|
|
|
|
|
{
|
2023-03-19 10:40:05 +00:00
|
|
|
|
WaitUntil(() =>
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
2023-03-21 07:23:15 +00:00
|
|
|
|
var deployment = client.ReadNamespacedDeployment(deploymentName, K8sNamespace);
|
2023-03-19 10:40:05 +00:00
|
|
|
|
return deployment == null || deployment.Status.AvailableReplicas == 0;
|
|
|
|
|
});
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void WaitUntilZeroPods(Kubernetes client)
|
|
|
|
|
{
|
2023-03-21 07:23:15 +00:00
|
|
|
|
WaitUntil(() => !client.ListNamespacedPod(K8sNamespace).Items.Any());
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void WaitUntilNamespaceDeleted(Kubernetes client)
|
|
|
|
|
{
|
2023-03-21 08:20:09 +00:00
|
|
|
|
WaitUntil(() => !IsTestNamespaceOnline(client));
|
2023-03-19 10:40:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void WaitUntil(Func<bool> predicate)
|
|
|
|
|
{
|
|
|
|
|
var start = DateTime.UtcNow;
|
|
|
|
|
var state = predicate();
|
|
|
|
|
while (!state)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
2023-03-19 10:40:05 +00:00
|
|
|
|
if (DateTime.UtcNow - start > Timing.K8sOperationTimeout())
|
|
|
|
|
{
|
|
|
|
|
Assert.Fail("K8s operation timed out.");
|
|
|
|
|
throw new TimeoutException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Timing.WaitForK8sServiceDelay();
|
|
|
|
|
state = predicate();
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Service management
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private void CreateService(OnlineCodexNodes online, Kubernetes client)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
|
|
|
|
var serviceSpec = new V1Service
|
|
|
|
|
{
|
|
|
|
|
ApiVersion = "v1",
|
2023-03-21 14:44:21 +00:00
|
|
|
|
Metadata = online.GetServiceMetadata(),
|
2023-03-19 09:49:03 +00:00
|
|
|
|
Spec = new V1ServiceSpec
|
|
|
|
|
{
|
|
|
|
|
Type = "NodePort",
|
2023-03-21 14:44:21 +00:00
|
|
|
|
Selector = online.GetSelector(),
|
|
|
|
|
Ports = CreateServicePorts(online)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
online.Service = client.CreateNamespacedService(serviceSpec, K8sNamespace);
|
2023-03-21 14:17:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private List<V1ServicePort> CreateServicePorts(OnlineCodexNodes online)
|
2023-03-21 14:17:48 +00:00
|
|
|
|
{
|
|
|
|
|
var result = new List<V1ServicePort>();
|
2023-03-21 14:44:21 +00:00
|
|
|
|
var containers = online.GetContainers();
|
|
|
|
|
foreach (var container in containers)
|
2023-03-21 14:17:48 +00:00
|
|
|
|
{
|
|
|
|
|
result.Add(new V1ServicePort
|
|
|
|
|
{
|
|
|
|
|
Protocol = "TCP",
|
|
|
|
|
Port = 8080,
|
|
|
|
|
TargetPort = container.ContainerPortName,
|
|
|
|
|
NodePort = container.ServicePort
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return result;
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private void DeleteService(Kubernetes client, OnlineCodexNodes online)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
2023-03-21 14:44:21 +00:00
|
|
|
|
if (online.Service == null) return;
|
|
|
|
|
client.DeleteNamespacedService(online.Service.Name(), K8sNamespace);
|
|
|
|
|
online.Service = null;
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Deployment management
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private void CreateDeployment(Kubernetes client, OnlineCodexNodes online, OfflineCodexNodes offline)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
|
|
|
|
var deploymentSpec = new V1Deployment
|
|
|
|
|
{
|
|
|
|
|
ApiVersion = "apps/v1",
|
2023-03-21 14:44:21 +00:00
|
|
|
|
Metadata = online.GetDeploymentMetadata(),
|
2023-03-19 09:49:03 +00:00
|
|
|
|
Spec = new V1DeploymentSpec
|
|
|
|
|
{
|
|
|
|
|
Replicas = 1,
|
|
|
|
|
Selector = new V1LabelSelector
|
|
|
|
|
{
|
2023-03-21 14:44:21 +00:00
|
|
|
|
MatchLabels = online.GetSelector()
|
2023-03-19 09:49:03 +00:00
|
|
|
|
},
|
|
|
|
|
Template = new V1PodTemplateSpec
|
|
|
|
|
{
|
|
|
|
|
Metadata = new V1ObjectMeta
|
|
|
|
|
{
|
2023-03-21 14:44:21 +00:00
|
|
|
|
Labels = online.GetSelector()
|
2023-03-19 09:49:03 +00:00
|
|
|
|
},
|
|
|
|
|
Spec = new V1PodSpec
|
|
|
|
|
{
|
2023-03-21 14:44:21 +00:00
|
|
|
|
Containers = CreateDeploymentContainers(online, offline)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
online.Deployment = client.CreateNamespacedDeployment(deploymentSpec, K8sNamespace);
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private List<V1Container> CreateDeploymentContainers(OnlineCodexNodes online, OfflineCodexNodes offline)
|
2023-03-21 14:17:48 +00:00
|
|
|
|
{
|
|
|
|
|
var result = new List<V1Container>();
|
2023-03-21 14:44:21 +00:00
|
|
|
|
var containers = online.GetContainers();
|
|
|
|
|
foreach (var container in containers)
|
2023-03-21 14:17:48 +00:00
|
|
|
|
{
|
|
|
|
|
result.Add(new V1Container
|
|
|
|
|
{
|
|
|
|
|
Name = container.Name,
|
|
|
|
|
Image = dockerImage.GetImageTag(),
|
|
|
|
|
Ports = new List<V1ContainerPort>
|
|
|
|
|
{
|
|
|
|
|
new V1ContainerPort
|
|
|
|
|
{
|
|
|
|
|
ContainerPort = container.ApiPort,
|
|
|
|
|
Name = container.ContainerPortName
|
|
|
|
|
}
|
|
|
|
|
},
|
2023-03-21 14:44:21 +00:00
|
|
|
|
Env = dockerImage.CreateEnvironmentVariables(offline, container)
|
2023-03-21 14:17:48 +00:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private void DeleteDeployment(Kubernetes client, OnlineCodexNodes online)
|
2023-03-19 09:49:03 +00:00
|
|
|
|
{
|
2023-03-21 14:44:21 +00:00
|
|
|
|
if (online.Deployment == null) return;
|
|
|
|
|
client.DeleteNamespacedDeployment(online.Deployment.Name(), K8sNamespace);
|
|
|
|
|
online.Deployment = null;
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Namespace management
|
|
|
|
|
|
|
|
|
|
private void EnsureTestNamespace(Kubernetes client)
|
|
|
|
|
{
|
2023-03-21 08:20:09 +00:00
|
|
|
|
if (IsTestNamespaceOnline(client)) return;
|
2023-03-19 09:49:03 +00:00
|
|
|
|
|
|
|
|
|
var namespaceSpec = new V1Namespace
|
|
|
|
|
{
|
|
|
|
|
ApiVersion = "v1",
|
|
|
|
|
Metadata = new V1ObjectMeta
|
|
|
|
|
{
|
2023-03-21 07:23:15 +00:00
|
|
|
|
Name = K8sNamespace,
|
|
|
|
|
Labels = new Dictionary<string, string> { { "name", K8sNamespace } }
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
};
|
2023-03-21 08:20:09 +00:00
|
|
|
|
client.CreateNamespace(namespaceSpec);
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DeleteNamespace(Kubernetes client)
|
|
|
|
|
{
|
2023-03-21 08:20:09 +00:00
|
|
|
|
if (IsTestNamespaceOnline(client))
|
|
|
|
|
{
|
|
|
|
|
client.DeleteNamespace(K8sNamespace, null, null, gracePeriodSeconds: 0);
|
|
|
|
|
}
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
private static Kubernetes CreateClient()
|
|
|
|
|
{
|
|
|
|
|
// todo: If the default KubeConfig file does not suffice, change it here:
|
|
|
|
|
var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
|
|
|
|
|
return new Kubernetes(config);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 08:20:09 +00:00
|
|
|
|
private static bool IsTestNamespaceOnline(Kubernetes client)
|
|
|
|
|
{
|
|
|
|
|
return client.ListNamespace().Items.Any(n => n.Metadata.Name == K8sNamespace);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 14:44:21 +00:00
|
|
|
|
private OnlineCodexNodes GetAndRemoveActiveNodeFor(IOnlineCodexNodes node)
|
2023-03-19 10:40:05 +00:00
|
|
|
|
{
|
2023-03-21 14:17:48 +00:00
|
|
|
|
var n = (OnlineCodexNodes)node;
|
2023-03-21 14:44:21 +00:00
|
|
|
|
activeCodexNodes.Remove(n);
|
|
|
|
|
return n;
|
2023-03-19 10:40:05 +00:00
|
|
|
|
}
|
2023-03-19 09:49:03 +00:00
|
|
|
|
}
|
|
|
|
|
}
|