package runtime import ( "context" "crypto/ed25519" "crypto/rand" "crypto/x509" "encoding/pem" "fmt" "net" "os" "path/filepath" "strings" "sync" "sync/atomic" "testing" "time" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/knownhosts" "git.cloudinit.dev/coreci/orca/internal/jobspec" "git.cloudinit.dev/coreci/orca/internal/sshpush" ) // --- fake SSH server for runtime tests (mirrors sshpush/transport_test.go) --- type fakeServer struct { listener net.Listener config *ssh.ServerConfig done chan struct{} hostKey ssh.Signer mu sync.Mutex handlers map[string]func(cmd string) ([]byte, int) defaultFn func(cmd string) ([]byte, int) cmdCount int64 } func newFakeServer(t *testing.T) *fakeServer { t.Helper() _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatalf("ed25519 gen: %v", err) } signer, err := ssh.NewSignerFromKey(priv) if err != nil { t.Fatalf("ssh signer: %v", err) } config := &ssh.ServerConfig{NoClientAuth: true} config.AddHostKey(signer) ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } srv := &fakeServer{ listener: ln, config: config, done: make(chan struct{}), hostKey: signer, handlers: make(map[string]func(cmd string) ([]byte, int)), defaultFn: func(cmd string) ([]byte, int) { return []byte("sh: command not found\n"), 127 }, } go srv.serve() return srv } func (s *fakeServer) addr() string { return s.listener.Addr().String() } func (s *fakeServer) hostPublicKey() ssh.PublicKey { return s.hostKey.PublicKey() } func (s *fakeServer) close() { _ = s.listener.Close() <-s.done } func (s *fakeServer) setHandler(prefix string, fn func(cmd string) ([]byte, int)) { s.mu.Lock() defer s.mu.Unlock() s.handlers[prefix] = fn } func (s *fakeServer) setDefault(fn func(cmd string) ([]byte, int)) { s.mu.Lock() defer s.mu.Unlock() s.defaultFn = fn } func (s *fakeServer) count() int64 { return atomic.LoadInt64(&s.cmdCount) } func (s *fakeServer) serve() { for { conn, err := s.listener.Accept() if err != nil { close(s.done) return } go s.handle(conn) } } func (s *fakeServer) handle(netConn net.Conn) { defer netConn.Close() _, chans, reqs, err := ssh.NewServerConn(netConn, s.config) if err != nil { return } go ssh.DiscardRequests(reqs) for newChan := range chans { if newChan.ChannelType() != "session" { newChan.Reject(ssh.UnknownChannelType, "only session") continue } go s.handleSession(newChan) } } func (s *fakeServer) handleSession(newChan ssh.NewChannel) { ch, reqs, err := newChan.Accept() if err != nil { return } defer ch.Close() for req := range reqs { if req.Type != "exec" { req.Reply(false, nil) continue } var execReq struct{ Command string } if err := ssh.Unmarshal(req.Payload, &execReq); err != nil { req.Reply(false, nil) continue } req.Reply(true, nil) atomic.AddInt64(&s.cmdCount, 1) out, code := s.runCommand(execReq.Command) _, _ = ch.Write(out) _, _ = ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Code uint32 }{uint32(code)})) _ = ch.Close() return } } func (s *fakeServer) runCommand(cmd string) ([]byte, int) { s.mu.Lock() defer s.mu.Unlock() trimmed := strings.TrimSpace(cmd) for prefix, fn := range s.handlers { if strings.HasPrefix(trimmed, prefix) { return fn(trimmed) } } return s.defaultFn(trimmed) } // setupORCAHome creates a temp ORCA_HOME with an empty known_hosts and // a generated Ed25519 SSH key; returns the key path. func setupORCAHome(t *testing.T) string { t.Helper() dir := t.TempDir() t.Setenv("ORCA_HOME", dir) knownHosts := filepath.Join(dir, "known_hosts") if err := os.WriteFile(knownHosts, []byte{}, 0o600); err != nil { t.Fatalf("create known_hosts: %v", err) } _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatalf("ed25519 gen: %v", err) } der, err := x509.MarshalPKCS8PrivateKey(priv) if err != nil { t.Fatalf("marshal key: %v", err) } pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}) keyPath := filepath.Join(dir, "orca_ssh_key") if err := os.WriteFile(keyPath, pemBytes, 0o600); err != nil { t.Fatalf("write key: %v", err) } return keyPath } // realTransport wires a *sshpush.Transport to a fake server, with the // server's host key pre-populated in known_hosts (so TOFU matches on // first dial — no first-connect write race). func realTransport(t *testing.T, srv *fakeServer) *sshpush.Transport { t.Helper() keyPath := setupORCAHome(t) tr := sshpush.NewTransport(keyPath, "") tr.SetUser("root") addr := srv.addr() line := knownhosts.Line([]string{knownhosts.Normalize(addr)}, srv.hostPublicKey()) home := os.Getenv("ORCA_HOME") kh := filepath.Join(home, "known_hosts") if err := os.WriteFile(kh, []byte(line+"\n"), 0o600); err != nil { t.Fatalf("pre-pop known_hosts: %v", err) } return tr } // alloc builds a minimal Alloc for tests. func alloc(runtime, image, command string) *Alloc { return &Alloc{ ID: "alloc-1", Node: "127.0.0.1:0", Runtime: runtime, Spec: &jobspec.WorkloadSpec{ Name: "test", Runtime: &jobspec.RuntimeBlock{ OneOf: runtime, Image: image, Command: command, }, }, } } // allocWithNode returns an alloc bound to the given peer address. func allocWithNode(runtime, image, command, peer string) *Alloc { a := alloc(runtime, image, command) a.Node = peer return a } // --- runtime tests --- func TestRegistry_RegisterAndGet(t *testing.T) { r := NewRegistry() r.Register("process", NewProcessRuntime()) rt, err := r.Get("process") if err != nil { t.Fatalf("Get: %v", err) } if rt == nil { t.Fatal("nil runtime") } if _, err := r.Get("nope"); err == nil { t.Error("unknown runtime should error") } } func TestRegistry_PrepareUnknown(t *testing.T) { r := NewRegistry() a := alloc("nonexistent", "", "/bin/true") if err := r.Prepare(context.Background(), a); err == nil { t.Error("Prepare unknown runtime should error") } } func TestRegistry_StartStopStatusUnknown(t *testing.T) { r := NewRegistry() a := alloc("nonexistent", "", "/bin/true") if _, err := r.Start(context.Background(), a); err == nil { t.Error("Start unknown should error") } if err := r.Stop(context.Background(), a); err == nil { t.Error("Stop unknown should error") } if _, err := r.Status(context.Background(), a); err == nil { t.Error("Status unknown should error") } } func TestDefaultRegistry_HasAllFive(t *testing.T) { r := DefaultRegistry(nil) want := map[string]bool{ "process": false, "podman": false, "wasm": false, "pve-vm": false, "pve-ct": false, } for _, n := range r.Names() { if _, ok := want[n]; ok { want[n] = true } } for k, v := range want { if !v { t.Errorf("DefaultRegistry missing %q", k) } } } func TestNewRegistry_EmptyGetError(t *testing.T) { r := NewRegistry() if _, err := r.Get("anything"); err == nil { t.Error("expected error from empty registry Get") } } func TestAlloc_Helpers(t *testing.T) { if _, err := commandFor(nil); err == nil { t.Error("commandFor(nil) should error") } if _, err := imageFor(nil); err == nil { t.Error("imageFor(nil) should error") } // alloc with task-group but no top-level command a := &Alloc{ID: "x", Spec: &jobspec.WorkloadSpec{ Tasks: []jobspec.TaskGroupTask{{Command: "/bin/true"}}, }} cmd, err := commandFor(a) if err != nil { t.Fatalf("commandFor task group: %v", err) } if cmd != "/bin/true" { t.Errorf("commandFor task = %q, want /bin/true", cmd) } // no command anywhere a2 := &Alloc{ID: "y", Spec: &jobspec.WorkloadSpec{}} if _, err := commandFor(a2); err == nil { t.Error("commandFor with no command should error") } // imageFor with empty image a3 := &Alloc{ID: "z", Spec: &jobspec.WorkloadSpec{Runtime: &jobspec.RuntimeBlock{}}} if _, err := imageFor(a3); err == nil { t.Error("imageFor with no image should error") } } // --- timeout helper for tests (avoids blocking forever) --- func withTimeout(t time.Duration) (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), t) } // compile-time interface conformance checks. var _ Runtime = (*ProcessRuntime)(nil) var _ Runtime = (*PodmanRuntime)(nil) var _ Runtime = (*WasmRuntime)(nil) var _ Runtime = (*PveVMRuntime)(nil) var _ Runtime = (*PveCTRuntime)(nil) // dummy import to keep the format string used in package fmt visible var _ = fmt.Sprintf