Files
worker/internal/installer/sourceinstall_ssh_test.go
Gleb Tv bd6070ee1f
Все проверки выполнены успешно
CI / test (push) Successful in 3m13s
Docker / Build and publish worker image (push) Successful in 10m35s
feat(installer): build worker source over SSH
2026-08-13 00:07:43 +03:00

352 строки
10 KiB
Go

package installer
import (
"crypto/ed25519"
"crypto/rand"
"fmt"
"net"
"strings"
"sync"
"testing"
"time"
"golang.org/x/crypto/ssh"
"rocketgit.ru/rsmon/worker/internal/sshinstall"
)
const (
fakeSSHPassword = "fake-ssh-password"
fakeCommitHex = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
fakeOsRelease = "ID=ubuntu\nNAME=\"Ubuntu\"\nVERSION_ID=24.04\n"
)
// fakeSSHServer is a minimal in-process SSH server that simulates a
// remote Linux host for the SourceInstall orchestration tests. It uses
// real golang.org/x/crypto/ssh transport (no mocked SSH library), so the
// executor's dial, session, exec, and stdout/stderr plumbing is
// exercised end to end, and it records every command it ran.
type fakeSSHServer struct {
addr string
onExec func(command string) (stdout, stderr string, code int)
mu sync.Mutex
commands []string
}
func startFakeSSHServer(t *testing.T, onExec func(command string) (string, string, int)) *fakeSSHServer {
t.Helper()
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
signer, err := ssh.NewSignerFromKey(priv)
if err != nil {
t.Fatal(err)
}
config := &ssh.ServerConfig{
PasswordCallback: func(_ ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
if string(pass) == fakeSSHPassword {
return nil, nil
}
return nil, fmt.Errorf("password rejected")
},
}
config.AddHostKey(signer)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = ln.Close() })
srv := &fakeSSHServer{addr: ln.Addr().String(), onExec: onExec}
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go srv.handleConn(conn, config)
}
}()
return srv
}
func (s *fakeSSHServer) Port() int {
_, port, err := net.SplitHostPort(s.addr)
if err != nil {
return 0
}
p := 0
fmt.Sscanf(port, "%d", &p)
return p
}
func (s *fakeSSHServer) Commands() []string {
s.mu.Lock()
defer s.mu.Unlock()
return append([]string(nil), s.commands...)
}
func (s *fakeSSHServer) handleConn(conn net.Conn, config *ssh.ServerConfig) {
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
return
}
defer sconn.Close() //nolint:errcheck
go ssh.DiscardRequests(reqs)
for newChannel := range chans {
if newChannel.ChannelType() != "session" {
_ = newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
continue
}
go func() {
defer channel.Close()
s.handleSession(channel, requests)
}()
}
}
func (s *fakeSSHServer) handleSession(channel ssh.Channel, requests <-chan *ssh.Request) {
for req := range requests {
if req.Type != "exec" {
if req.WantReply {
_ = req.Reply(false, nil)
}
continue
}
var payload struct{ Command string }
if err := ssh.Unmarshal(req.Payload, &payload); err != nil {
if req.WantReply {
_ = req.Reply(false, nil)
}
continue
}
if req.WantReply {
_ = req.Reply(true, nil)
}
s.mu.Lock()
s.commands = append(s.commands, payload.Command)
s.mu.Unlock()
s.execCommand(channel, payload.Command)
return
}
}
func (s *fakeSSHServer) execCommand(channel ssh.Channel, command string) {
handler := s.onExec
if handler == nil {
handler = defaultFakeExec
}
stdout, stderr, code := handler(command)
_, _ = channel.Write([]byte(stdout))
_, _ = channel.Stderr().Write([]byte(stderr))
_, _ = channel.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{uint32(code)}))
_ = channel.CloseWrite()
}
// defaultFakeExec simulates a minimal Linux host: it answers os-release,
// uname, the init-marker file probe, origin URL, branch resolution, and
// rev-parse, and accepts every install step. A pinned-branch existence
// check (show-ref) fails by default so the missing-branch path is
// exercised without extra setup.
func defaultFakeExec(command string) (string, string, int) {
switch {
case strings.Contains(command, "cat /etc/os-release"):
return fakeOsRelease, "", 0
case strings.Contains(command, "uname -m"):
return "x86_64\n", "", 0
case strings.Contains(command, "[ -e"):
return "/usr/lib/systemd/system\n", "", 0
case strings.Contains(command, "remote.origin.url"):
return sshinstall.DefaultRepo + "\n", "", 0
case strings.Contains(command, "symbolic-ref"):
return "origin/master\n", "", 0
case strings.Contains(command, "rev-parse HEAD"):
return fakeCommitHex + "\n", "", 0
case strings.Contains(command, "show-ref"):
return "", "branch not found", 1
default:
return "", "", 0
}
}
func testSSHOptions(port int) SourceInstallOptions {
return SourceInstallOptions{
SSHOptions: SSHOptions{
Host: "127.0.0.1",
Port: port,
User: "root",
Password: fakeSSHPassword,
InsecureHostKey: true,
},
}
}
func TestSourceInstallSSHFlow(t *testing.T) {
srv := startFakeSSHServer(t, nil)
res, err := SourceInstall(testSSHOptions(srv.Port()))
if err != nil {
t.Fatal(err)
}
if res.Detection.Distro != sshinstall.DistroUbuntu || res.Detection.PackageManager != sshinstall.PkgApt {
t.Fatalf("detection = %+v", res.Detection)
}
if res.Detection.InitSystem != sshinstall.InitSystemd {
t.Fatalf("init detection = %q, want systemd", res.Detection.InitSystem)
}
if res.Plan.Toolchain.Arch != "linux-amd64" || res.Plan.Toolchain.Version != "1.26.0" {
t.Fatalf("toolchain = %+v", res.Plan.Toolchain)
}
if len(res.Plan.Packages) == 0 || res.Plan.Repo == "" {
t.Fatalf("plan = %+v", res.Plan)
}
if res.ResolvedBranch != "master" || res.ResolvedCommit != fakeCommitHex {
t.Fatalf("resolved = %s @ %s", res.ResolvedBranch, res.ResolvedCommit)
}
if res.ToolchainDir != "/usr/local/go" || res.StageBinary != "/opt/rsmon-worker-src/rsmon-worker" ||
res.RecordFile != "/opt/rsmon-worker-src/rsmon-worker.commit" {
t.Fatalf("paths = %+v", res)
}
commands := srv.Commands()
var joined strings.Builder
for _, c := range commands {
joined.WriteString(c)
joined.WriteString("\n")
}
// Marker substrings that survive the nested `sh -c '<script>'`
// quoting; exact quoting of each script is asserted by the unit
// tests (TestPackageScript, TestToolchainScript, ...).
for _, want := range []string{
"cat /etc/os-release",
"uname -m",
"apt-get update",
"apt-get install -y --no-install-recommends",
"sha256sum -c -",
"git clone",
"git -C",
"fetch --prune origin",
"symbolic-ref --short refs/remotes/origin/HEAD",
"checkout -q -B",
"rev-parse HEAD",
"branch=%s\\ncommit=%s\\n",
fakeCommitHex,
"CGO_ENABLED=0",
"build -trimpath",
"GOMODCACHE",
"mv -f",
} {
if !strings.Contains(joined.String(), want) {
t.Fatalf("recorded commands missing %q:\n%s", want, joined.String())
}
}
// Ordering: prerequisites before toolchain before source before build.
idx := func(sub string) int {
for i, c := range commands {
if strings.Contains(c, sub) {
return i
}
}
t.Fatalf("command %q not found in %v", sub, commands)
return -1
}
if !(idx("apt-get install") < idx("sha256sum") && idx("sha256sum") < idx("git clone") &&
idx("git clone") < idx("rev-parse") && idx("rev-parse") < idx("-trimpath") &&
idx("-trimpath") < idx("branch=%s")) {
t.Fatalf("step order wrong: %v", commands)
}
// Adaptive resolution means no pinned-branch existence check ran.
if strings.Contains(joined.String(), "show-ref") {
t.Fatalf("show-ref ran despite branch resolution:\n%s", joined.String())
}
if strings.Contains(joined.String(), fakeSSHPassword) {
t.Fatal("SSH password leaked into a remote command")
}
}
func TestSourceInstallSSHRejectsMissingPinnedBranch(t *testing.T) {
srv := startFakeSSHServer(t, nil)
opts := testSSHOptions(srv.Port())
opts.Branch = "main"
_, err := SourceInstall(opts)
if err == nil || !strings.Contains(err.Error(), `branch "main" does not exist`) {
t.Fatalf("err = %v, want missing-branch error", err)
}
if commands := srv.Commands(); !strings.Contains(strings.Join(commands, "\n"), "show-ref") {
t.Fatalf("pinned branch existence was not verified: %v", commands)
}
}
func TestSourceInstallSSHBuildFailure(t *testing.T) {
srv := startFakeSSHServer(t, func(command string) (string, string, int) {
if strings.Contains(command, "-trimpath") {
return "", "build exploded", 1
}
return defaultFakeExec(command)
})
_, err := SourceInstall(testSSHOptions(srv.Port()))
if err == nil || !strings.Contains(err.Error(), "build worker binary") || !strings.Contains(err.Error(), "build exploded") {
t.Fatalf("err = %v, want bounded build failure", err)
}
}
func TestSourceInstallSSHDetectionFailure(t *testing.T) {
srv := startFakeSSHServer(t, func(command string) (string, string, int) {
if strings.Contains(command, "os-release") {
return "", "os-release unreadable", 1
}
return defaultFakeExec(command)
})
_, err := SourceInstall(testSSHOptions(srv.Port()))
if err == nil || !strings.Contains(err.Error(), "/etc/os-release") {
t.Fatalf("err = %v, want detection failure", err)
}
}
// TestSourceInstallSSHCheckoutFailureNotMasked proves the fail-closed
// contract: a failed checkout (e.g. a dirty working tree) surfaces as an
// error and never reaches the build or commit-record steps, so the
// previous staging binary and record are preserved.
func TestSourceInstallSSHCheckoutFailureNotMasked(t *testing.T) {
srv := startFakeSSHServer(t, func(command string) (string, string, int) {
if strings.Contains(command, "checkout -q -B") {
return "", "your local changes to the following files would be overwritten by checkout", 1
}
return defaultFakeExec(command)
})
_, err := SourceInstall(testSSHOptions(srv.Port()))
if err == nil || !strings.Contains(err.Error(), "check out branch") {
t.Fatalf("err = %v, want checkout failure", err)
}
joined := strings.Join(srv.Commands(), "\n")
if strings.Contains(joined, "-trimpath") || strings.Contains(joined, "branch=%s") {
t.Fatalf("build or record ran after checkout failed:\n%s", joined)
}
}
// TestSourceInstallSSHCommandTimeout verifies each remote command is
// bounded by SessionTimeout and the run reports it.
func TestSourceInstallSSHCommandTimeout(t *testing.T) {
srv := startFakeSSHServer(t, func(command string) (string, string, int) {
if strings.Contains(command, "uname -m") {
time.Sleep(5 * time.Second)
return "x86_64\n", "", 0
}
return defaultFakeExec(command)
})
opts := testSSHOptions(srv.Port())
opts.SessionTimeout = 300 * time.Millisecond
_, err := SourceInstall(opts)
if err == nil || !strings.Contains(err.Error(), "timed out after") {
t.Fatalf("err = %v, want command timeout", err)
}
}