From 38c7c3474fceb22dd499933965cb7dec0e80da6f Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Mon, 14 Sep 2026 20:33:13 -0700 Subject: [PATCH 1/3] testkube: expose container SSH for nodeport instances --- v1/providers/testkube/client.go | 3 ++ v1/providers/testkube/instance.go | 46 +++++++++++++++++++++----- v1/providers/testkube/instance_test.go | 17 +++++++++- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/v1/providers/testkube/client.go b/v1/providers/testkube/client.go index d89b413..5687c33 100644 --- a/v1/providers/testkube/client.go +++ b/v1/providers/testkube/client.go @@ -23,6 +23,9 @@ const ( servicePortName = "ssh" servicePort = int32(22) containerSSHPort = int32(22) + + nestedContainerSSHPortName = "container-ssh" + nestedContainerSSHPort = int32(2222) ) type TestKubeAuthMode string diff --git a/v1/providers/testkube/instance.go b/v1/providers/testkube/instance.go index 162f318..85d3d51 100644 --- a/v1/providers/testkube/instance.go +++ b/v1/providers/testkube/instance.go @@ -90,6 +90,24 @@ func (c *TestKubeClient) createInstanceAsK8sResources(ctx context.Context, attrs } else { serviceAnnotations = annotations } + servicePorts := []corev1.ServicePort{ + { + Name: servicePortName, + Protocol: corev1.ProtocolTCP, + Port: servicePort, + TargetPort: intstr.FromInt32(containerSSHPort), + }, + } + if instanceTypeSpec.serviceType == corev1.ServiceTypeNodePort { + // Container-mode environments publish their nested SSH server on the VM's + // port 2222. Expose it only for the explicit NodePort test instance types. + servicePorts = append(servicePorts, corev1.ServicePort{ + Name: nestedContainerSSHPortName, + Protocol: corev1.ProtocolTCP, + Port: nestedContainerSSHPort, + TargetPort: intstr.FromInt32(nestedContainerSSHPort), + }) + } // Create the service. k8sService, err := c.k8sClient. @@ -105,14 +123,7 @@ func (c *TestKubeClient) createInstanceAsK8sResources(ctx context.Context, attrs Spec: corev1.ServiceSpec{ Type: instanceTypeSpec.serviceType, Selector: selectorLabels(string(cloudID)), - Ports: []corev1.ServicePort{ - { - Name: servicePortName, - Protocol: corev1.ProtocolTCP, - Port: servicePort, - TargetPort: intstr.FromInt32(containerSSHPort), - }, - }, + Ports: servicePorts, }, }, metav1.CreateOptions{}) if err != nil { @@ -413,6 +424,15 @@ func populateNetwork(service *corev1.Service, hostIP string, instance *cloudv1.I } // Set the SSH port to the first node port. instance.SSHPort = int(service.Spec.Ports[0].NodePort) + for _, port := range service.Spec.Ports[1:] { + if port.NodePort == 0 { + continue + } + instance.InternalPortMappings = append(instance.InternalPortMappings, cloudv1.PortMapping{ + FromPort: int(port.Port), + ToPort: int(port.NodePort), + }) + } if hostIP != "" { instance.PublicIP = hostIP instance.PublicDNS = hostIP @@ -463,7 +483,15 @@ func statusFromResources(pod *corev1.Pod, service *corev1.Service) cloudv1.Statu } func nodePortReady(pod *corev1.Pod, service *corev1.Service) bool { - return pod.Status.HostIP != "" && len(service.Spec.Ports) > 0 && service.Spec.Ports[0].NodePort != 0 + if pod.Status.HostIP == "" || len(service.Spec.Ports) == 0 { + return false + } + for _, port := range service.Spec.Ports { + if port.NodePort == 0 { + return false + } + } + return true } func loadBalancerReady(service *corev1.Service) bool { diff --git a/v1/providers/testkube/instance_test.go b/v1/providers/testkube/instance_test.go index a7879ca..6374167 100644 --- a/v1/providers/testkube/instance_test.go +++ b/v1/providers/testkube/instance_test.go @@ -76,6 +76,7 @@ func TestInstanceLifecycle(t *testing.T) { //nolint:funlen // ok service, err := client.k8sClient.CoreV1().Services(client.namespace).Get(ctx, string(instance.CloudID), metav1.GetOptions{}) require.NoError(t, err) require.Equal(t, awsLoadBalancerConnectionIdleTimeout, service.Annotations[annotationAWSLoadBalancerConnectionIdleTimeout]) + require.Len(t, service.Spec.Ports, 1) pod, err := client.k8sClient.CoreV1().Pods(client.namespace).Get(ctx, string(instance.CloudID), metav1.GetOptions{}) require.NoError(t, err) require.NotContains(t, pod.Annotations, annotationAWSLoadBalancerConnectionIdleTimeout) @@ -138,6 +139,7 @@ func TestNodePortInstanceLifecycle(t *testing.T) { require.NoError(t, err) require.Equal(t, corev1.ServiceTypeNodePort, service.Spec.Type) require.NotContains(t, service.Annotations, annotationAWSLoadBalancerConnectionIdleTimeout) + require.Len(t, service.Spec.Ports, 2) service.Spec.Ports[0].NodePort = 32421 _, err = client.k8sClient.CoreV1().Services(client.namespace).Update(ctx, service, metav1.UpdateOptions{}) require.NoError(t, err) @@ -150,12 +152,21 @@ func TestNodePortInstanceLifecycle(t *testing.T) { _, err = client.k8sClient.CoreV1().Pods(client.namespace).UpdateStatus(ctx, pod, metav1.UpdateOptions{}) require.NoError(t, err) + pending, err := client.GetInstance(ctx, instance.CloudID) + require.NoError(t, err) + require.Equal(t, cloudv1.LifecycleStatusPending, pending.Status.LifecycleStatus) + + service.Spec.Ports[1].NodePort = 32422 + _, err = client.k8sClient.CoreV1().Services(client.namespace).Update(ctx, service, metav1.UpdateOptions{}) + require.NoError(t, err) + running, err := client.GetInstance(ctx, instance.CloudID) require.NoError(t, err) require.Equal(t, cloudv1.LifecycleStatusRunning, running.Status.LifecycleStatus) require.Equal(t, "192.168.49.2", running.PublicIP) require.Equal(t, "192.168.49.2", running.PublicDNS) require.Equal(t, 32421, running.SSHPort) + require.Equal(t, []cloudv1.PortMapping{{FromPort: 2222, ToPort: 32422}}, running.InternalPortMappings) } func TestScenarioEnvironment(t *testing.T) { @@ -304,7 +315,10 @@ func TestPopulateNetworkNodePort(t *testing.T) { Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeNodePort, ClusterIP: "10.96.119.41", - Ports: []corev1.ServicePort{{Port: 22, NodePort: 32421}}, + Ports: []corev1.ServicePort{ + {Port: 22, NodePort: 32421}, + {Port: 2222, NodePort: 32422}, + }, }, }, "192.168.49.2", instance) @@ -312,6 +326,7 @@ func TestPopulateNetworkNodePort(t *testing.T) { require.Equal(t, "192.168.49.2", instance.PublicIP) require.Equal(t, "192.168.49.2", instance.PublicDNS) require.Equal(t, 32421, instance.SSHPort) + require.Equal(t, []cloudv1.PortMapping{{FromPort: 2222, ToPort: 32422}}, instance.InternalPortMappings) } func setPodReady(t *testing.T, client *TestKubeClient, instanceID cloudv1.CloudProviderInstanceID) { From 7f5ffed556d94dba530c4d7cb04c3bf4b4719d62 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Mon, 14 Sep 2026 21:02:57 -0700 Subject: [PATCH 2/3] lint --- v1/providers/testkube/instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v1/providers/testkube/instance.go b/v1/providers/testkube/instance.go index 85d3d51..4f985cc 100644 --- a/v1/providers/testkube/instance.go +++ b/v1/providers/testkube/instance.go @@ -75,7 +75,7 @@ func (c *TestKubeClient) CreateInstance(ctx context.Context, attrs cloudv1.Creat return instance, nil } -func (c *TestKubeClient) createInstanceAsK8sResources(ctx context.Context, attrs cloudv1.CreateInstanceAttrs, instanceTypeSpec instanceTypeSpec) (*cloudv1.Instance, error) { +func (c *TestKubeClient) createInstanceAsK8sResources(ctx context.Context, attrs cloudv1.CreateInstanceAttrs, instanceTypeSpec instanceTypeSpec) (*cloudv1.Instance, error) { //nolint:funlen // ok // Create a "cloud ID" to emulate a provider-provided instance ID. cloudID := makeCloudID(c.refID, attrs.RefID) From 5b2887828bf75d2ce83291e44c2bddb38d615697 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Mon, 14 Sep 2026 21:12:02 -0700 Subject: [PATCH 3/3] simplify --- v1/providers/testkube/instance.go | 8 ++------ v1/providers/testkube/instance_test.go | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/v1/providers/testkube/instance.go b/v1/providers/testkube/instance.go index 4f985cc..ee637d5 100644 --- a/v1/providers/testkube/instance.go +++ b/v1/providers/testkube/instance.go @@ -97,16 +97,12 @@ func (c *TestKubeClient) createInstanceAsK8sResources(ctx context.Context, attrs Port: servicePort, TargetPort: intstr.FromInt32(containerSSHPort), }, - } - if instanceTypeSpec.serviceType == corev1.ServiceTypeNodePort { - // Container-mode environments publish their nested SSH server on the VM's - // port 2222. Expose it only for the explicit NodePort test instance types. - servicePorts = append(servicePorts, corev1.ServicePort{ + { Name: nestedContainerSSHPortName, Protocol: corev1.ProtocolTCP, Port: nestedContainerSSHPort, TargetPort: intstr.FromInt32(nestedContainerSSHPort), - }) + }, } // Create the service. diff --git a/v1/providers/testkube/instance_test.go b/v1/providers/testkube/instance_test.go index 6374167..0d889d4 100644 --- a/v1/providers/testkube/instance_test.go +++ b/v1/providers/testkube/instance_test.go @@ -76,7 +76,7 @@ func TestInstanceLifecycle(t *testing.T) { //nolint:funlen // ok service, err := client.k8sClient.CoreV1().Services(client.namespace).Get(ctx, string(instance.CloudID), metav1.GetOptions{}) require.NoError(t, err) require.Equal(t, awsLoadBalancerConnectionIdleTimeout, service.Annotations[annotationAWSLoadBalancerConnectionIdleTimeout]) - require.Len(t, service.Spec.Ports, 1) + require.Len(t, service.Spec.Ports, 2) pod, err := client.k8sClient.CoreV1().Pods(client.namespace).Get(ctx, string(instance.CloudID), metav1.GetOptions{}) require.NoError(t, err) require.NotContains(t, pod.Annotations, annotationAWSLoadBalancerConnectionIdleTimeout)