Add Docker SSH end-to-end test · Entire
Add Docker SSH end-to-end test
dec0ec0→main·
Soph·2mo ago·3 files·+332 added/-0 removed
Sessions
6f3629569e1eView transcript
Changes
3
docs
- Mtesting.md+11
internal/syncer
- Assh_docker_test.go+317
Mmise.toml+4
31 unmodified lines
## Docker SSH End-To-End Test
Optional end-to-end sync against a real `sshd` running in Docker:
```bash
env GOCACHE=/tmp/go-build GITSYNC_E2E_SSH_DOCKER=1 go test ./internal/syncer -run TestRun_SSHDockerSync -v
```
That path builds a disposable Docker image with `openssh-server` and `git`, mounts local bare source/target repositories into the container, configures a temporary SSH keypair plus an isolated SSH config passed via a wrapper `ssh` binary, and runs a real SSH-based sync through `git-upload-pack` / `git-receive-pack`.
## Live Linux Smokes
Optional live Linux bootstrap smoke:
- `mise run test` — default suite
- `mise run test:ci` — default suite with race detection
- `mise run test:git-http-backend` — `git-http-backend` end-to-end
- `mise run test:ssh-docker` — Docker-based SSH end-to-end
- `mise run test:linux-smoke` — live Linux bootstrap smoke
- `mise run test:linux-smoke:batched` — live Linux batched bootstrap smoke
package syncer
import (
"context"
"encoding/base64"
"fmt"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"entire.io/entire/git-sync/internal/gitproto"
"github.com/go-git/go-git/v6/plumbing"
)
const sshDockerEnv = "GITSYNC_E2E_SSH_DOCKER"
func TestRun_SSHDockerSync(t *testing.T) {
if os.Getenv(sshDockerEnv) == "" {
t.Skip("set GITSYNC_E2E_SSH_DOCKER=1 to run docker-based SSH integration test")
}
for _, tool := range []string{"docker", "git", "ssh", "ssh-keygen"} {
if _, err := exec.LookPath(tool); err != nil {
t.Skipf("%s not available: %%v", tool, err)
}
}
root := dockerBindMountTempDir(t)
reposDir := filepath.Join(root, "repos")
worktree := filepath.Join(root, "worktree")
sourceBare := filepath.Join(reposDir, "source.git")
targetBare := filepath.Join(reposDir, "target.git")
if err := os.MkdirAll(reposDir, 0o755); err != nil {
t.Fatalf("mkdir repos: %%v", err)
}
runGit(t, root, "init", "--bare", sourceBare)
runGit(t, root, "init", "--bare", targetBare)
runGit(t, root, "init", "-b", testBranch, worktree)
runGit(t, worktree, "config", "user.name", "git-sync test")
runGit(t, worktree, "config", "user.email", "git-sync@example.com")
writeFile(t, filepath.Join(worktree, "README.md"), "hello over docker ssh\n")
runGit(t, worktree, "add", "README.md")
runGit(t, worktree, "commit", "-m", "initial")
runGit(t, worktree, "remote", "add", "origin", sourceBare)
runGit(t, worktree, "push", "origin", "HEAD:refs/heads/"+testBranch)
keyPath := filepath.Join(root, "id_ed25519")
runCommand(t, root, nil, "ssh-keygen", "-t", "ed25519", "-N", "", "-f", keyPath)
pubKey, err := os.ReadFile(keyPath + ".pub")
if err != nil {
t.Fatalf("read public key: %%v", err)
}
pubKeyB64 := base64.StdEncoding.EncodeToString([]byte(strings.TrimSpace(string(pubKey))))
imageCtx := filepath.Join(root, "image")
writeSSHServerDockerContext(t, imageCtx)
imageTag := fmt.Sprintf("git-sync-ssh-e2e:%%d", rand.New(rand.NewSource(time.Now().UnixNano())).Int63())
runCommand(t, imageCtx, nil, "docker", "build", "-t", imageTag, ".")
t.Cleanup(func() {
_ = runCommandBestEffort(root, nil, "docker", "image", "rm", "-f", imageTag)
})
containerID := strings.TrimSpace(runCommand(t, root, nil,
"docker", "run", "-d",
"-e", "AUTHORIZED_KEY_B64="+pubKeyB64,
"-P",
imageTag,
))
t.Cleanup(func() {
_ = runCommandBestEffort(root, nil, "docker", "rm", "-f", containerID)
})
runCommand(t, root, nil, "docker", "cp", sourceBare, containerID+":/srv/git/source.git")
runCommand(t, root, nil, "docker", "cp", targetBare, containerID+":/srv/git/target.git")
runCommand(t, root, nil, "docker", "exec", containerID, "chown", "-R", "git:git", "/srv/git")
portOutput := strings.TrimSpace(runCommandWithDiagnostics(t, root, nil, containerID,
"docker", "port", containerID, "22/tcp",
))
port, err := parseDockerPort(portOutput)
if err != nil {
t.Fatalf("parse docker port %%q: %%v", portOutput, err)
}
if port == "" {
t.Fatal("docker inspect returned empty SSH host port")
}
sshConfigPath := filepath.Join(root, "ssh_config")
writeFile(t, sshConfigPath, strings.Join([]string{
"Host gitsync-ssh-docker",
" HostName 127.0.0.1",
" Port " + port,
" User git",
" IdentityFile " + keyPath,
" IdentitiesOnly yes",
" StrictHostKeyChecking no",
" UserKnownHostsFile /dev/null",
" BatchMode yes",
" ",
}, "\n"))
sshPath, err := exec.LookPath("ssh")
if err != nil {
t.Fatalf("locate ssh: %%v", err)
}
sshWrapper := filepath.Join(root, "ssh-wrapper.sh")
writeFile(t, sshWrapper, strings.Join([]string{
"#!/bin/sh",
"exec " + shSingleQuote(sshPath) + " -F " + shSingleQuote(sshConfigPath) + ` "$@"`,
"",
}, "\n"))
if err := os.Chmod(sshWrapper, 0o755); err != nil {
t.Fatalf("chmod ssh wrapper: %%v", err)
}
orig := gitproto.SSHLookPath
t.Cleanup(func() { gitproto.SSHLookPath = orig })
gitproto.SSHLookPath = func(string) (string, error) { return sshWrapper, nil }
waitForSSHReady(t, sshWrapper, containerID)
result, err := Run(context.Background(), Config{
Source: Endpoint{URL: "ssh://gitsync-ssh-docker/srv/git/source.git"},
Target: Endpoint{URL: "ssh://gitsync-ssh-docker/srv/git/target.git"},
ProtocolMode: protocolModeAuto,
})
if err != nil {
t.Fatalf("Run over Docker SSH failed: %%v", err)
}
if result.Protocol != protocolModeV2 {
t.Fatalf("result.Protocol = %%q, want %%q", result.Protocol, protocolModeV2)
}
if !result.Relay || result.RelayMode != relayModeBootstrap {
t.Fatalf("expected bootstrap relay over SSH, got %+v", result)
}
syncedTargetBare := filepath.Join(root, "synced-target.git")
runCommand(t, root, nil, "docker", "cp", containerID+":/srv/git/target.git", syncedTargetBare)
assertGitRefEqual(t, sourceBare, syncedTargetBare, plumbing.NewBranchReferenceName(testBranch))
}
func writeSSHServerDockerContext(t *testing.T, dir string) {
t.Helper()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir docker context: %%v", err)
}
writeFile(t, filepath.Join(dir, "Dockerfile"), strings.Join([]string{
"FROM alpine:3.21",
"RUN apk add --no-cache git openssh-server",
"RUN adduser -D -h /home/git -s /bin/sh git",
`RUN sed -i 's/^git:!:/git::/' /etc/shadow`,
"EXPOSE 22",
"COPY sshd_config /etc/ssh/sshd_config",
"COPY entrypoint.sh /entrypoint.sh",
"RUN chmod +x /entrypoint.sh",
`ENTRYPOINT ["/entrypoint.sh"]`,
"",
}, "\n"))
writeFile(t, filepath.Join(dir, "sshd_config"), strings.Join([]string{
"Port 22",
"ListenAddress 0.0.0.0",
"PasswordAuthentication no",
"KbdInteractiveAuthentication no",
"ChallengeResponseAuthentication no",
"UsePAM no",
"PermitRootLogin no",
"PubkeyAuthentication yes",
"AuthorizedKeysFile .ssh/authorized_keys",
"AllowUsers git",
"LogLevel VERBOSE",
"Subsystem sftp internal-sftp",
"PidFile /run/sshd.pid",
"",
}, "\n"))
writeFile(t, filepath.Join(dir, "entrypoint.sh"), strings.Join([]string{
"#!/bin/sh",
"set -eu",
"mkdir -p /run/sshd /home/git/.ssh /srv/git",
`printf '%s' "$AUTHORIZED_KEY_B64" | base64 -d > /home/git/.ssh/authorized_keys`,
"chmod 700 /home/git/.ssh",
"chmod 600 /home/git/.ssh/authorized_keys",
"chown -R git:git /home/git /srv/git",
"ssh-keygen -A >/dev/null 2>&1",
"exec /usr/sbin/sshd -D -e",
"",
}, "\n"))
}
// Additional helper functions omitted for brevity.