2023-04-12 13:11:36 +00:00
using k8s ;
using k8s.Models ;
2023-04-25 09:31:15 +00:00
using Logging ;
2023-04-17 14:28:07 +00:00
using Utils ;
2023-04-12 13:11:36 +00:00
namespace KubernetesWorkflow
2023-04-12 11:53:55 +00:00
{
public class K8sController
{
2023-04-25 09:31:15 +00:00
private readonly BaseLog log ;
2023-04-12 11:53:55 +00:00
private readonly K8sCluster cluster ;
2023-04-12 13:11:36 +00:00
private readonly KnownK8sPods knownPods ;
private readonly WorkflowNumberSource workflowNumberSource ;
2023-05-04 06:25:48 +00:00
private readonly K8sClient client ;
2023-04-12 11:53:55 +00:00
2023-05-03 12:18:37 +00:00
public K8sController ( BaseLog log , K8sCluster cluster , KnownK8sPods knownPods , WorkflowNumberSource workflowNumberSource , string testNamespace )
2023-04-12 11:53:55 +00:00
{
2023-04-25 09:31:15 +00:00
this . log = log ;
2023-04-12 11:53:55 +00:00
this . cluster = cluster ;
2023-04-12 13:11:36 +00:00
this . knownPods = knownPods ;
this . workflowNumberSource = workflowNumberSource ;
2023-05-04 06:25:48 +00:00
client = new K8sClient ( cluster . GetK8sClientConfig ( ) ) ;
K8sTestNamespace = cluster . Configuration . K8sNamespacePrefix + testNamespace ;
2023-04-12 13:11:36 +00:00
}
public void Dispose ( )
{
client . Dispose ( ) ;
}
public RunningPod BringOnline ( ContainerRecipe [ ] containerRecipes , Location location )
{
2023-04-25 09:31:15 +00:00
log . Debug ( ) ;
2023-06-02 08:04:07 +00:00
DiscoverK8sNodes ( ) ;
2023-04-12 13:11:36 +00:00
EnsureTestNamespace ( ) ;
2023-04-13 09:07:36 +00:00
var deploymentName = CreateDeployment ( containerRecipes , location ) ;
var ( serviceName , servicePortsMap ) = CreateService ( containerRecipes ) ;
2023-06-02 08:04:07 +00:00
var podInfo = FetchNewPod ( ) ;
2023-04-12 13:11:36 +00:00
2023-06-02 08:04:07 +00:00
return new RunningPod ( cluster , podInfo , deploymentName , serviceName , servicePortsMap ) ;
2023-04-13 09:07:36 +00:00
}
public void Stop ( RunningPod pod )
{
2023-04-25 09:31:15 +00:00
log . Debug ( ) ;
2023-04-13 09:07:36 +00:00
if ( ! string . IsNullOrEmpty ( pod . ServiceName ) ) DeleteService ( pod . ServiceName ) ;
DeleteDeployment ( pod . DeploymentName ) ;
WaitUntilDeploymentOffline ( pod . DeploymentName ) ;
2023-06-02 08:04:07 +00:00
WaitUntilPodOffline ( pod . PodInfo . Name ) ;
2023-04-12 13:11:36 +00:00
}
2023-04-13 09:30:19 +00:00
public void DownloadPodLog ( RunningPod pod , ContainerRecipe recipe , ILogHandler logHandler )
{
2023-04-25 09:31:15 +00:00
log . Debug ( ) ;
2023-06-02 08:04:07 +00:00
using var stream = client . Run ( c = > c . ReadNamespacedPodLog ( pod . PodInfo . Name , K8sTestNamespace , recipe . Name ) ) ;
2023-04-13 09:30:19 +00:00
logHandler . Log ( stream ) ;
}
2023-04-14 07:54:07 +00:00
public string ExecuteCommand ( RunningPod pod , string containerName , string command , params string [ ] args )
{
2023-06-01 13:56:26 +00:00
var cmdAndArgs = $"{containerName}: {command} ({string.Join(" , ", args)})" ;
log . Debug ( cmdAndArgs ) ;
2023-05-03 12:18:37 +00:00
var runner = new CommandRunner ( client , K8sTestNamespace , pod , containerName , command , args ) ;
2023-04-14 07:54:07 +00:00
runner . Run ( ) ;
2023-06-01 13:56:26 +00:00
var result = runner . GetStdOut ( ) ;
log . Debug ( $"{cmdAndArgs} = '{result}'" ) ;
return result ;
2023-04-14 07:54:07 +00:00
}
2023-04-12 13:11:36 +00:00
public void DeleteAllResources ( )
{
2023-04-25 09:31:15 +00:00
log . Debug ( ) ;
2023-04-12 13:11:36 +00:00
2023-05-04 06:25:48 +00:00
var all = client . Run ( c = > c . ListNamespace ( ) . Items ) ;
2023-05-03 12:18:37 +00:00
var namespaces = all . Select ( n = > n . Name ( ) ) . Where ( n = > n . StartsWith ( cluster . Configuration . K8sNamespacePrefix ) ) ;
foreach ( var ns in namespaces )
{
DeleteNamespace ( ns ) ;
}
foreach ( var ns in namespaces )
{
WaitUntilNamespaceDeleted ( ns ) ;
}
}
public void DeleteTestNamespace ( )
{
log . Debug ( ) ;
if ( IsTestNamespaceOnline ( ) )
{
2023-05-04 06:25:48 +00:00
client . Run ( c = > c . DeleteNamespace ( K8sTestNamespace , null , null , gracePeriodSeconds : 0 ) ) ;
2023-05-03 12:18:37 +00:00
}
2023-04-12 13:11:36 +00:00
WaitUntilNamespaceDeleted ( ) ;
}
2023-05-03 12:18:37 +00:00
public void DeleteNamespace ( string ns )
{
log . Debug ( ) ;
if ( IsNamespaceOnline ( ns ) )
{
2023-05-04 06:25:48 +00:00
client . Run ( c = > c . DeleteNamespace ( ns , null , null , gracePeriodSeconds : 0 ) ) ;
2023-05-03 12:18:37 +00:00
}
}
2023-06-02 08:04:07 +00:00
#region Discover K8s Nodes
private void DiscoverK8sNodes ( )
{
if ( cluster . AvailableK8sNodes = = null | | ! cluster . AvailableK8sNodes . Any ( ) )
{
cluster . AvailableK8sNodes = GetAvailableK8sNodes ( ) ;
if ( cluster . AvailableK8sNodes . Length < 3 )
{
2023-06-02 08:27:57 +00:00
log . Debug ( $"Warning: For full location support, at least 3 Kubernetes Nodes are required in the cluster. Nodes found: '{string.Join(" , ", cluster.AvailableK8sNodes.Select(p => $" { p . Key } = { p . Value } "))}'." ) ;
2023-06-02 08:04:07 +00:00
}
}
}
2023-06-02 08:27:57 +00:00
private K8sNodeLabel [ ] GetAvailableK8sNodes ( )
2023-06-02 08:04:07 +00:00
{
var nodes = client . Run ( c = > c . ListNode ( ) ) ;
2023-06-02 08:27:57 +00:00
var optionals = nodes . Items . Select ( i = > CreateNodeLabel ( i ) ) ;
return optionals . Where ( n = > n ! = null ) . Select ( n = > n ! ) . ToArray ( ) ;
}
private K8sNodeLabel ? CreateNodeLabel ( V1Node i )
{
var keys = i . Metadata . Labels . Keys ;
var hostnameKey = keys . SingleOrDefault ( k = > k . ToLowerInvariant ( ) . Contains ( "hostname" ) ) ;
if ( hostnameKey ! = null )
{
var hostnameValue = i . Metadata . Labels [ hostnameKey ] ;
return new K8sNodeLabel ( hostnameKey , hostnameValue ) ;
}
return null ;
2023-06-02 08:04:07 +00:00
}
#endregion
2023-04-12 13:11:36 +00:00
#region Namespace management
2023-05-04 06:25:48 +00:00
private string K8sTestNamespace { get ; }
2023-04-12 13:11:36 +00:00
private void EnsureTestNamespace ( )
{
if ( IsTestNamespaceOnline ( ) ) return ;
var namespaceSpec = new V1Namespace
{
ApiVersion = "v1" ,
Metadata = new V1ObjectMeta
{
2023-05-03 12:18:37 +00:00
Name = K8sTestNamespace ,
Labels = new Dictionary < string , string > { { "name" , K8sTestNamespace } }
2023-04-12 13:11:36 +00:00
}
} ;
2023-05-04 06:25:48 +00:00
client . Run ( c = > c . CreateNamespace ( namespaceSpec ) ) ;
2023-04-12 13:11:36 +00:00
WaitUntilNamespaceCreated ( ) ;
2023-05-04 12:55:39 +00:00
CreatePolicy ( ) ;
2023-04-12 13:11:36 +00:00
}
2023-05-03 12:18:37 +00:00
private bool IsTestNamespaceOnline ( )
2023-04-12 13:11:36 +00:00
{
2023-05-03 12:18:37 +00:00
return IsNamespaceOnline ( K8sTestNamespace ) ;
2023-04-12 13:11:36 +00:00
}
2023-05-03 12:18:37 +00:00
private bool IsNamespaceOnline ( string name )
2023-04-12 13:11:36 +00:00
{
2023-05-04 06:25:48 +00:00
return client . Run ( c = > c . ListNamespace ( ) . Items . Any ( n = > n . Metadata . Name = = name ) ) ;
2023-04-12 13:11:36 +00:00
}
2023-05-04 12:55:39 +00:00
private void CreatePolicy ( )
{
client . Run ( c = >
{
var body = new V1NetworkPolicy
{
Metadata = new V1ObjectMeta
{
Name = "isolate-policy" ,
NamespaceProperty = K8sTestNamespace
} ,
Spec = new V1NetworkPolicySpec
{
2023-05-30 19:41:34 +00:00
PodSelector = new V1LabelSelector { } ,
2023-05-04 12:55:39 +00:00
PolicyTypes = new [ ]
{
"Ingress" ,
"Egress"
} ,
Ingress = new List < V1NetworkPolicyIngressRule >
{
new V1NetworkPolicyIngressRule
{
FromProperty = new List < V1NetworkPolicyPeer >
{
new V1NetworkPolicyPeer
{
2023-05-30 19:41:34 +00:00
PodSelector = new V1LabelSelector { }
2023-05-04 12:55:39 +00:00
}
}
2023-05-31 15:36:40 +00:00
} ,
new V1NetworkPolicyIngressRule
{
FromProperty = new List < V1NetworkPolicyPeer >
{
new V1NetworkPolicyPeer
{
NamespaceSelector = new V1LabelSelector
{
MatchLabels = GetRunnerNamespaceSelector ( )
}
}
}
2023-05-04 12:55:39 +00:00
}
} ,
Egress = new List < V1NetworkPolicyEgressRule >
{
2023-05-30 19:41:34 +00:00
new V1NetworkPolicyEgressRule
{
To = new List < V1NetworkPolicyPeer >
{
new V1NetworkPolicyPeer
{
PodSelector = new V1LabelSelector { }
}
}
} ,
2023-05-04 12:55:39 +00:00
new V1NetworkPolicyEgressRule
{
To = new List < V1NetworkPolicyPeer >
{
new V1NetworkPolicyPeer
{
NamespaceSelector = new V1LabelSelector
{
2023-05-30 19:41:34 +00:00
MatchLabels = new Dictionary < string , string > { { "kubernetes.io/metadata.name" , "kube-system" } }
}
} ,
new V1NetworkPolicyPeer
{
PodSelector = new V1LabelSelector
{
MatchLabels = new Dictionary < string , string > { { "k8s-app" , "kube-dns" } }
2023-05-04 12:55:39 +00:00
}
}
2023-05-30 19:41:34 +00:00
} ,
Ports = new List < V1NetworkPolicyPort >
{
new V1NetworkPolicyPort
{
Port = new IntstrIntOrString
{
Value = "53"
} ,
Protocol = "UDP"
}
}
} ,
new V1NetworkPolicyEgressRule
{
To = new List < V1NetworkPolicyPeer >
{
new V1NetworkPolicyPeer
{
IpBlock = new V1IPBlock
{
Cidr = "0.0.0.0/0"
}
}
} ,
Ports = new List < V1NetworkPolicyPort >
{
new V1NetworkPolicyPort
{
Port = new IntstrIntOrString
{
Value = "80"
} ,
Protocol = "TCP"
} ,
new V1NetworkPolicyPort
{
Port = new IntstrIntOrString
{
Value = "443"
} ,
Protocol = "TCP"
}
2023-05-04 12:55:39 +00:00
}
}
2023-05-30 19:41:34 +00:00
2023-05-04 12:55:39 +00:00
}
}
} ;
c . CreateNamespacedNetworkPolicy ( body , K8sTestNamespace ) ;
} ) ;
}
2023-04-12 13:11:36 +00:00
#endregion
#region Deployment management
2023-04-13 09:07:36 +00:00
private string CreateDeployment ( ContainerRecipe [ ] containerRecipes , Location location )
2023-04-12 13:11:36 +00:00
{
var deploymentSpec = new V1Deployment
{
ApiVersion = "apps/v1" ,
Metadata = CreateDeploymentMetadata ( ) ,
Spec = new V1DeploymentSpec
{
Replicas = 1 ,
Selector = new V1LabelSelector
{
MatchLabels = GetSelector ( )
} ,
Template = new V1PodTemplateSpec
{
Metadata = new V1ObjectMeta
{
Labels = GetSelector ( )
} ,
Spec = new V1PodSpec
{
NodeSelector = CreateNodeSelector ( location ) ,
Containers = CreateDeploymentContainers ( containerRecipes )
}
}
}
} ;
2023-05-04 06:25:48 +00:00
client . Run ( c = > c . CreateNamespacedDeployment ( deploymentSpec , K8sTestNamespace ) ) ;
2023-04-13 09:07:36 +00:00
WaitUntilDeploymentOnline ( deploymentSpec . Metadata . Name ) ;
return deploymentSpec . Metadata . Name ;
}
private void DeleteDeployment ( string deploymentName )
{
2023-05-04 06:25:48 +00:00
client . Run ( c = > c . DeleteNamespacedDeployment ( deploymentName , K8sTestNamespace ) ) ;
2023-04-13 09:07:36 +00:00
WaitUntilDeploymentOffline ( deploymentName ) ;
2023-04-12 13:11:36 +00:00
}
private IDictionary < string , string > CreateNodeSelector ( Location location )
{
2023-06-02 08:27:57 +00:00
var nodeLabel = cluster . GetNodeLabelForLocation ( location ) ;
if ( nodeLabel = = null ) return new Dictionary < string , string > ( ) ;
2023-04-12 13:11:36 +00:00
return new Dictionary < string , string >
{
2023-06-02 08:27:57 +00:00
{ nodeLabel . Key , nodeLabel . Value }
2023-04-12 13:11:36 +00:00
} ;
}
private IDictionary < string , string > GetSelector ( )
{
return new Dictionary < string , string > { { "codex-test-node" , "dist-test-" + workflowNumberSource . WorkflowNumber } } ;
}
2023-05-31 15:36:40 +00:00
private IDictionary < string , string > GetRunnerNamespaceSelector ( )
{
return new Dictionary < string , string > { { "kubernetes.io/metadata.name" , "default" } } ;
}
2023-04-12 13:11:36 +00:00
private V1ObjectMeta CreateDeploymentMetadata ( )
{
return new V1ObjectMeta
{
Name = "deploy-" + workflowNumberSource . WorkflowNumber ,
2023-05-04 12:55:39 +00:00
NamespaceProperty = K8sTestNamespace ,
Labels = GetSelector ( )
2023-04-12 13:11:36 +00:00
} ;
}
private List < V1Container > CreateDeploymentContainers ( ContainerRecipe [ ] containerRecipes )
{
return containerRecipes . Select ( r = > CreateDeploymentContainer ( r ) ) . ToList ( ) ;
}
private V1Container CreateDeploymentContainer ( ContainerRecipe recipe )
{
return new V1Container
{
Name = recipe . Name ,
Image = recipe . Image ,
Ports = CreateContainerPorts ( recipe ) ,
Env = CreateEnv ( recipe )
} ;
2023-04-12 11:53:55 +00:00
}
2023-04-12 13:11:36 +00:00
private List < V1EnvVar > CreateEnv ( ContainerRecipe recipe )
2023-04-12 11:53:55 +00:00
{
2023-04-12 13:11:36 +00:00
return recipe . EnvVars . Select ( CreateEnvVar ) . ToList ( ) ;
}
private V1EnvVar CreateEnvVar ( EnvVar envVar )
{
return new V1EnvVar
{
Name = envVar . Name ,
Value = envVar . Value ,
} ;
}
private List < V1ContainerPort > CreateContainerPorts ( ContainerRecipe recipe )
{
var exposedPorts = recipe . ExposedPorts . Select ( p = > CreateContainerPort ( recipe , p ) ) ;
var internalPorts = recipe . InternalPorts . Select ( p = > CreateContainerPort ( recipe , p ) ) ;
return exposedPorts . Concat ( internalPorts ) . ToList ( ) ;
}
private V1ContainerPort CreateContainerPort ( ContainerRecipe recipe , Port port )
{
return new V1ContainerPort
{
Name = GetNameForPort ( recipe , port ) ,
ContainerPort = port . Number
} ;
}
private string GetNameForPort ( ContainerRecipe recipe , Port port )
{
2023-04-13 07:33:10 +00:00
return $"p{workflowNumberSource.WorkflowNumber}-{recipe.Number}-{port.Number}" ;
2023-04-12 13:11:36 +00:00
}
#endregion
#region Service management
2023-04-13 09:07:36 +00:00
private ( string , Dictionary < ContainerRecipe , Port [ ] > ) CreateService ( ContainerRecipe [ ] containerRecipes )
2023-04-12 13:11:36 +00:00
{
var result = new Dictionary < ContainerRecipe , Port [ ] > ( ) ;
2023-06-02 09:07:36 +00:00
var ports = CreateServicePorts ( containerRecipes ) ;
2023-04-12 13:11:36 +00:00
if ( ! ports . Any ( ) )
{
2023-05-30 19:41:34 +00:00
// None of these container-recipes wish to expose anything via a service port.
2023-04-12 13:11:36 +00:00
// So, we don't have to create a service.
2023-04-13 09:07:36 +00:00
return ( string . Empty , result ) ;
2023-04-12 13:11:36 +00:00
}
var serviceSpec = new V1Service
{
ApiVersion = "v1" ,
Metadata = CreateServiceMetadata ( ) ,
Spec = new V1ServiceSpec
{
Type = "NodePort" ,
Selector = GetSelector ( ) ,
Ports = ports
}
} ;
2023-05-04 06:25:48 +00:00
client . Run ( c = > c . CreateNamespacedService ( serviceSpec , K8sTestNamespace ) ) ;
2023-04-13 09:07:36 +00:00
2023-06-02 09:07:36 +00:00
ReadBackServiceAndMapPorts ( serviceSpec , containerRecipes , result ) ;
2023-04-13 09:07:36 +00:00
return ( serviceSpec . Metadata . Name , result ) ;
}
2023-06-02 09:07:36 +00:00
private void ReadBackServiceAndMapPorts ( V1Service serviceSpec , ContainerRecipe [ ] containerRecipes , Dictionary < ContainerRecipe , Port [ ] > result )
{
// For each container-recipe, we need to figure out which service-ports it was assigned by K8s.
var readback = client . Run ( c = > c . ReadNamespacedService ( serviceSpec . Metadata . Name , K8sTestNamespace ) ) ;
foreach ( var r in containerRecipes )
{
if ( r . ExposedPorts . Any ( ) )
{
var firstExposedPort = r . ExposedPorts . First ( ) ;
var portName = GetNameForPort ( r , firstExposedPort ) ;
var matchingServicePorts = readback . Spec . Ports . Where ( p = > p . Name = = portName ) ;
if ( matchingServicePorts . Any ( ) )
{
// These service ports belongs to this recipe.
var optionals = matchingServicePorts . Select ( p = > MapNodePortIfAble ( p , portName ) ) ;
var ports = optionals . Where ( p = > p ! = null ) . Select ( p = > p ! ) . ToArray ( ) ;
result . Add ( r , ports ) ;
}
}
}
}
private Port ? MapNodePortIfAble ( V1ServicePort p , string tag )
{
if ( p . NodePort = = null ) return null ;
return new Port ( p . NodePort . Value , tag ) ;
}
2023-04-13 09:07:36 +00:00
private void DeleteService ( string serviceName )
{
2023-05-04 06:25:48 +00:00
client . Run ( c = > c . DeleteNamespacedService ( serviceName , K8sTestNamespace ) ) ;
2023-04-12 13:11:36 +00:00
}
private V1ObjectMeta CreateServiceMetadata ( )
{
return new V1ObjectMeta
{
2023-04-13 08:11:33 +00:00
Name = "service-" + workflowNumberSource . WorkflowNumber ,
2023-05-03 12:18:37 +00:00
NamespaceProperty = K8sTestNamespace
2023-04-12 13:11:36 +00:00
} ;
}
2023-06-02 09:07:36 +00:00
private List < V1ServicePort > CreateServicePorts ( ContainerRecipe [ ] recipes )
2023-04-12 13:11:36 +00:00
{
var result = new List < V1ServicePort > ( ) ;
foreach ( var recipe in recipes )
{
2023-06-02 09:07:36 +00:00
result . AddRange ( CreateServicePorts ( recipe ) ) ;
2023-04-12 13:11:36 +00:00
}
return result ;
}
2023-06-02 09:07:36 +00:00
private List < V1ServicePort > CreateServicePorts ( ContainerRecipe recipe )
2023-04-12 13:11:36 +00:00
{
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 ) ,
} ) ;
}
return result ;
}
#endregion
#region Waiting
private void WaitUntilNamespaceCreated ( )
{
WaitUntil ( ( ) = > IsTestNamespaceOnline ( ) ) ;
}
private void WaitUntilNamespaceDeleted ( )
{
WaitUntil ( ( ) = > ! IsTestNamespaceOnline ( ) ) ;
}
2023-05-03 12:18:37 +00:00
private void WaitUntilNamespaceDeleted ( string name )
{
WaitUntil ( ( ) = > ! IsNamespaceOnline ( name ) ) ;
}
2023-04-12 13:11:36 +00:00
private void WaitUntilDeploymentOnline ( string deploymentName )
{
WaitUntil ( ( ) = >
{
2023-05-04 06:25:48 +00:00
var deployment = client . Run ( c = > c . ReadNamespacedDeployment ( deploymentName , K8sTestNamespace ) ) ;
2023-04-12 13:11:36 +00:00
return deployment ? . Status . AvailableReplicas ! = null & & deployment . Status . AvailableReplicas > 0 ;
} ) ;
}
2023-04-13 09:07:36 +00:00
private void WaitUntilDeploymentOffline ( string deploymentName )
{
WaitUntil ( ( ) = >
{
2023-05-04 06:25:48 +00:00
var deployments = client . Run ( c = > c . ListNamespacedDeployment ( K8sTestNamespace ) ) ;
2023-04-13 09:07:36 +00:00
var deployment = deployments . Items . SingleOrDefault ( d = > d . Metadata . Name = = deploymentName ) ;
return deployment = = null | | deployment . Status . AvailableReplicas = = 0 ;
} ) ;
}
private void WaitUntilPodOffline ( string podName )
{
WaitUntil ( ( ) = >
{
2023-05-04 06:25:48 +00:00
var pods = client . Run ( c = > c . ListNamespacedPod ( K8sTestNamespace ) ) . Items ;
2023-04-13 09:07:36 +00:00
var pod = pods . SingleOrDefault ( p = > p . Metadata . Name = = podName ) ;
return pod = = null ;
} ) ;
}
2023-04-12 13:11:36 +00:00
private void WaitUntil ( Func < bool > predicate )
{
2023-04-25 09:31:15 +00:00
var sw = Stopwatch . Begin ( log , true ) ;
try
{
Time . WaitUntil ( predicate , cluster . K8sOperationTimeout ( ) , cluster . WaitForK8sServiceDelay ( ) ) ;
}
finally
{
sw . End ( "" , 1 ) ;
}
2023-04-12 13:11:36 +00:00
}
#endregion
2023-06-02 08:04:07 +00:00
private PodInfo FetchNewPod ( )
2023-04-12 13:11:36 +00:00
{
2023-05-04 06:25:48 +00:00
var pods = client . Run ( c = > c . ListNamespacedPod ( K8sTestNamespace ) ) . Items ;
2023-04-12 13:11:36 +00:00
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 ;
2023-06-02 08:04:07 +00:00
var k8sNodeName = newPod . Spec . NodeName ;
2023-04-12 11:53:55 +00:00
2023-04-12 13:11:36 +00:00
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." ) ;
2023-04-12 11:53:55 +00:00
2023-04-12 13:11:36 +00:00
knownPods . Add ( name ) ;
2023-06-02 08:04:07 +00:00
return new PodInfo ( name , ip , k8sNodeName ) ;
2023-04-12 11:53:55 +00:00
}
}
}