Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/build_images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,17 @@ jobs:
ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/interlink/interlink:latest
file: ./docker/Dockerfile.interlink
platforms: linux/amd64, linux/arm64, linux/aarch64

- name: Build container base image ssh-tunnel
uses: docker/build-push-action@v6
with:
context: ./
outputs: "type=registry,push=true"
tags: |
ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/interlink/ssh-tunnel:${{ env.RELEASE_VERSION }}
ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/interlink/ssh-tunnel:latest
file: ./docker/Dockerfile.ssh-tunnel
platforms: linux/amd64, linux/arm64, linux/aarch64

virtual-kubelet-refresh-token:
runs-on: ubuntu-latest
#env:
Expand Down
15 changes: 15 additions & 0 deletions docker/Dockerfile.ssh-tunnel
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Image for the SSH port-forward shadow pod (Network.ShadowMode: ssh).
#
# It runs `ssh -N -L ...` against an HPC login node, so it needs an ssh client and,
# for GSSAPI sites, the Kerberos client tools to obtain and renew a ticket from a
# keytab. socat provides the local listeners used by Network.SSH.ForwardMode "exec",
# where the login node grants no forwarding privilege. Nothing from this repository
# is installed: the shadow runs no interLink binary, only stock ssh.
FROM debian:bookworm-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
openssh-client krb5-user socat ca-certificates \
&& rm -rf /var/lib/apt/lists/*

CMD ["/bin/sh"]
6 changes: 6 additions & 0 deletions pkg/interlink/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ type PodStatus struct {
PodNamespace string `json:"namespace"`
// JobID is the remote system's job identifier (e.g., SLURM job ID, container ID)
JobID string `json:"JID"`
// NodeName is the remote compute node the pod was allocated on, as the plugin's
// site resolves it (typically a fully qualified hostname reachable from the login
// node). Plugins report it once the workload is actually running; it is empty
// while the job is still queued, and empty for plugins that do not track it.
// interLink uses it to point per-pod shadow tunnels at the right host.
NodeName string `json:"nodeName,omitempty"`
// Containers holds the status of all regular containers in the pod
Containers []v1.ContainerStatus `json:"containers"`
// InitContainers holds the status of all init containers in the pod
Expand Down
100 changes: 100 additions & 0 deletions pkg/virtualkubelet/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,104 @@ type Network struct {
Slirp4netnsURL string `yaml:"Slirp4netnsURL,omitempty"`
// UnsharedMode is the flag for unshared network mode in slirp4netns
UnshareMode string `yaml:"UnshareMode,omitempty"`
// ShadowMode selects which shadow implementation is rendered for offloaded pods
// with exposed ports: "wstunnel" (default) or "ssh".
ShadowMode string `yaml:"ShadowMode,omitempty"`
// SSH configures the SSH port-forward shadow, used when ShadowMode is "ssh"
SSH SSHTunnel `yaml:"SSH,omitempty"`
}

// Shadow implementations selectable through Network.ShadowMode.
const (
// ShadowModeWstunnel exposes the offloaded pod's ports by having the workload
// dial out to a public ingress and run a wstunnel client. This is the default.
ShadowModeWstunnel = "wstunnel"
// ShadowModeSSH exposes them the other way round: the shadow dials in to an SSH
// login node and forwards each port to the compute node the job landed on. The
// workload runs nothing, and no compute node needs outbound internet access.
ShadowModeSSH = "ssh"
)

// Traffic-forwarding strategies selectable through SSHTunnel.ForwardMode.
const (
// SSHForwardModePortForward uses `ssh -L`, and needs AllowTcpForwarding on the
// login node. This is the default.
SSHForwardModePortForward = "portforward"
// SSHForwardModeExec pipes each connection through a command run on the login
// node, for sites that do not grant TCP forwarding.
SSHForwardModeExec = "exec"
)

// DefaultSSHExecConnectCommand relays a connection on stdin/stdout in "exec" mode.
const DefaultSSHExecConnectCommand = "nc"

// SSH authentication methods selectable through SSHTunnel.Auth.
const (
// SSHAuthPublicKey authenticates with a private key from KeySecret.
SSHAuthPublicKey = "publickey"
// SSHAuthKerberos authenticates with GSSAPI, using a keytab from KeytabSecret.
SSHAuthKerberos = "kerberos"
)

// SSHTunnel configures the SSH port-forward shadow.
//
// The shadow runs `ssh -N -L <port>:<compute node>:<port>` against the site's login
// node, one -L per exposed port, so cluster traffic reaches services inside an
// offloaded pod without the compute node needing any outbound connectivity. It
// covers the same ground as the wstunnel shadow, in the opposite direction; it does
// not give the offloaded pod access back into the cluster (see Network.FullMesh).
type SSHTunnel struct {
// LoginHost is the SSH login node to forward through (required)
LoginHost string `yaml:"LoginHost,omitempty"`
// Port is the login node's SSH port (default 22)
Port int `yaml:"Port,omitempty"`
// User is the login name on the login node (required)
User string `yaml:"User,omitempty"`
// Image is the container image running in the shadow. It must provide an ssh
// client, and kinit/klist when Auth is "kerberos".
Image string `yaml:"Image,omitempty"`
// Auth selects the authentication method: "publickey" (default) or "kerberos"
Auth string `yaml:"Auth,omitempty"`
// ForwardMode selects how traffic reaches the compute node:
//
// "portforward" (default) — one `ssh -L` per exposed port. Cheapest and most
// direct, but the login node must set AllowTcpForwarding yes for this
// account. Sites that disable it refuse every channel with
// "administratively prohibited".
// "exec" — a local listener per exposed port, each connection piped through a
// command run on the login node (see ExecConnectCommand). Needs no
// forwarding privilege at all, at the cost of one ssh process per connection.
ForwardMode string `yaml:"ForwardMode,omitempty"`
// ExecConnectCommand is the command run on the login node in "exec" mode. It is
// invoked as `<command> <compute node> <port>` and must relay the connection on
// its stdin and stdout. Defaults to "nc".
ExecConnectCommand string `yaml:"ExecConnectCommand,omitempty"`
// KeySecret is the Secret holding the SSH private key ("publickey" auth)
KeySecret string `yaml:"KeySecret,omitempty"`
// KeySecretKey is the key inside KeySecret holding the private key (default "id_ed25519")
KeySecretKey string `yaml:"KeySecretKey,omitempty"`
// KeytabSecret is the Secret holding the Kerberos keytab ("kerberos" auth)
KeytabSecret string `yaml:"KeytabSecret,omitempty"`
// KeytabSecretKey is the key inside KeytabSecret holding the keytab (default "user.keytab")
KeytabSecretKey string `yaml:"KeytabSecretKey,omitempty"`
// Principal is the Kerberos principal to obtain a ticket for ("kerberos" auth)
Principal string `yaml:"Principal,omitempty"`
// Krb5ConfigMap is an optional ConfigMap holding a krb5.conf to mount at /etc/krb5.conf
Krb5ConfigMap string `yaml:"Krb5ConfigMap,omitempty"`
// KnownHostsConfigMap is an optional ConfigMap holding a known_hosts file. When
// unset the shadow falls back to StrictHostKeyChecking=accept-new, which trusts
// whatever key the login node presents on first contact.
KnownHostsConfigMap string `yaml:"KnownHostsConfigMap,omitempty"`
// ReplicateCredentials copies the referenced Secret and ConfigMaps from the
// virtual kubelet's own namespace into the shadow's namespace, so offloaded pods
// in arbitrary (e.g. per-user) namespaces work without pre-seeding credentials
// everywhere. Defaults to true. Note this makes the credential readable by anyone
// who can read Secrets in those namespaces.
ReplicateCredentials *bool `yaml:"ReplicateCredentials,omitempty"`
// NodeWaitTimeout bounds how long the shadow waits for the plugin to report the
// compute node before failing (default "2h"). Queue waits are normal, so this is
// generous by design.
NodeWaitTimeout string `yaml:"NodeWaitTimeout,omitempty"`
// ExtraOptions are additional ssh client options, each passed verbatim as -o <opt>
ExtraOptions []string `yaml:"ExtraOptions,omitempty"`
}
4 changes: 4 additions & 0 deletions pkg/virtualkubelet/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,10 @@ func checkPodsStatus(ctx context.Context, p *Provider, pod *v1.Pod, token string

// if the PodUID match with the one in etcd we are talking of the same thing. GOOD
if podRemoteStatus.PodUID == string(podRefInCluster.UID) {
// The plugin reports the remote compute node only once the job is actually
// running, so this stays a no-op for as long as the job sits in the queue.
p.publishShadowNodeName(ctx, podRefInCluster, podRemoteStatus.NodeName)

// check if the pod is already in a terminal state (Failed or Succeeded)
if currentPhase, terminal := p.podTerminalPhase(podRemoteStatus.PodUID); terminal {
if podRefInCluster.Status.Phase == currentPhase {
Expand Down
Loading
Loading