182 lines
5.8 KiB
Go
182 lines
5.8 KiB
Go
package testenv
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/moby/moby/api/types/container"
|
|
"github.com/moby/moby/api/types/mount"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
tcnetwork "github.com/testcontainers/testcontainers-go/network"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
)
|
|
|
|
// BackendContainer wraps a running galaxy/backend:integration
|
|
// container reachable from the host (HTTPHost, GRPCPushHost) and
|
|
// from the shared Docker network at the alias "backend".
|
|
type BackendContainer struct {
|
|
Container testcontainers.Container
|
|
HTTPHost string
|
|
HTTPPort int
|
|
HTTPURL string
|
|
GRPCHost string
|
|
GRPCPort int
|
|
GRPCURL string
|
|
|
|
// AdminUser/AdminPassword are the bootstrap admin credentials this
|
|
// container started with. Tests that exercise the admin surface
|
|
// reuse them directly.
|
|
AdminUser string
|
|
AdminPassword string
|
|
}
|
|
|
|
// BackendOptions tunes a backend container before it boots.
|
|
type BackendOptions struct {
|
|
NetworkAlias string
|
|
NetworkName string
|
|
PostgresDSN string
|
|
MailpitHost string
|
|
MailpitPort int
|
|
GeoIPHostPath string
|
|
AdminEmail string
|
|
Extra map[string]string
|
|
}
|
|
|
|
// StartBackend boots galaxy/backend:integration with the supplied
|
|
// options.
|
|
func StartBackend(t *testing.T, opts BackendOptions) *BackendContainer {
|
|
t.Helper()
|
|
EnsureBackendImage(t)
|
|
|
|
if opts.NetworkAlias == "" {
|
|
opts.NetworkAlias = "backend"
|
|
}
|
|
if opts.AdminEmail == "" {
|
|
opts.AdminEmail = "admin@galaxy.test"
|
|
}
|
|
|
|
geoIPInContainer := "/var/lib/galaxy/geoip.mmdb"
|
|
// Use a unique daemon-side path for each test so concurrent
|
|
// runs cannot collide. Docker creates the source directory at
|
|
// container start because BindOptions.CreateMountpoint=true.
|
|
stateRoot := "/tmp/galaxy-state-" + uuid.NewString()
|
|
|
|
env := map[string]string{
|
|
"BACKEND_HTTP_LISTEN_ADDR": ":8080",
|
|
"BACKEND_GRPC_PUSH_LISTEN_ADDR": ":8081",
|
|
"BACKEND_LOGGING_LEVEL": "info",
|
|
"BACKEND_POSTGRES_DSN": opts.PostgresDSN,
|
|
"BACKEND_SMTP_HOST": opts.MailpitHost,
|
|
"BACKEND_SMTP_PORT": fmt.Sprintf("%d", opts.MailpitPort),
|
|
"BACKEND_SMTP_FROM": "galaxy-backend@galaxy.test",
|
|
"BACKEND_SMTP_TLS_MODE": "none",
|
|
"BACKEND_DOCKER_NETWORK": opts.NetworkName,
|
|
"BACKEND_GAME_STATE_ROOT": stateRoot,
|
|
"BACKEND_ADMIN_BOOTSTRAP_USER": "bootstrap",
|
|
"BACKEND_ADMIN_BOOTSTRAP_PASSWORD": "bootstrap-secret",
|
|
"BACKEND_GEOIP_DB_PATH": geoIPInContainer,
|
|
"BACKEND_OTEL_TRACES_EXPORTER": "none",
|
|
"BACKEND_OTEL_METRICS_EXPORTER": "none",
|
|
"BACKEND_NOTIFICATION_ADMIN_EMAIL": opts.AdminEmail,
|
|
"BACKEND_AUTH_CHALLENGE_THROTTLE_MAX": "100",
|
|
"BACKEND_MAIL_WORKER_INTERVAL": "500ms",
|
|
"BACKEND_NOTIFICATION_WORKER_INTERVAL": "500ms",
|
|
}
|
|
for k, v := range opts.Extra {
|
|
env[k] = v
|
|
}
|
|
|
|
dockerSocket := DockerSocketPath()
|
|
req := testcontainers.ContainerRequest{
|
|
Image: BackendImage,
|
|
ExposedPorts: []string{"8080/tcp", "8081/tcp"},
|
|
Env: env,
|
|
WaitingFor: wait.ForHTTP("/healthz").
|
|
WithPort("8080/tcp").
|
|
WithStartupTimeout(60 * time.Second),
|
|
Files: []testcontainers.ContainerFile{
|
|
{
|
|
HostFilePath: opts.GeoIPHostPath,
|
|
ContainerFilePath: geoIPInContainer,
|
|
FileMode: 0o644,
|
|
},
|
|
},
|
|
HostConfigModifier: func(hc *container.HostConfig) {
|
|
hc.Binds = append(hc.Binds, dockerSocket+":/var/run/docker.sock")
|
|
// Bind a unique daemon-side directory at the same path
|
|
// inside the backend container. CreateMountpoint=true
|
|
// asks the daemon to create the source directory if it
|
|
// is missing, so we do not need a second container just
|
|
// to mkdir on the daemon host. Per-game subdirectories
|
|
// are created by backend's runtime via os.MkdirAll
|
|
// before each engine container start.
|
|
hc.Mounts = append(hc.Mounts, mount.Mount{
|
|
Type: mount.TypeBind,
|
|
Source: stateRoot,
|
|
Target: stateRoot,
|
|
BindOptions: &mount.BindOptions{
|
|
CreateMountpoint: true,
|
|
},
|
|
})
|
|
},
|
|
// The distroless `nonroot` user (uid 65532) cannot reach the
|
|
// Docker daemon socket that backend mounts to manage engine
|
|
// containers. In integration tests we run as root so the
|
|
// dockerclient.EnsureNetwork startup probe succeeds; the
|
|
// production deployment will rely on a docker-socket-proxy
|
|
// sidecar (see ARCHITECTURE.md §13).
|
|
User: "0:0",
|
|
}
|
|
|
|
gcr := &testcontainers.GenericContainerRequest{ContainerRequest: req}
|
|
if opts.NetworkName != "" {
|
|
_ = tcnetwork.WithNetwork([]string{opts.NetworkAlias}, &testcontainers.DockerNetwork{Name: opts.NetworkName}).Customize(gcr)
|
|
}
|
|
gcr.Started = true
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
|
defer cancel()
|
|
container, err := testcontainers.GenericContainer(ctx, *gcr)
|
|
if err != nil {
|
|
t.Fatalf("start backend container: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := testcontainers.TerminateContainer(container); err != nil {
|
|
t.Logf("terminate backend: %v", err)
|
|
}
|
|
})
|
|
|
|
host, err := container.Host(ctx)
|
|
if err != nil {
|
|
t.Fatalf("backend host: %v", err)
|
|
}
|
|
httpPort, err := container.MappedPort(ctx, "8080/tcp")
|
|
if err != nil {
|
|
t.Fatalf("backend http port: %v", err)
|
|
}
|
|
grpcPort, err := container.MappedPort(ctx, "8081/tcp")
|
|
if err != nil {
|
|
t.Fatalf("backend grpc port: %v", err)
|
|
}
|
|
|
|
return &BackendContainer{
|
|
Container: container,
|
|
HTTPHost: host,
|
|
HTTPPort: int(httpPort.Num()),
|
|
HTTPURL: fmt.Sprintf("http://%s:%d", host, httpPort.Num()),
|
|
GRPCHost: host,
|
|
GRPCPort: int(grpcPort.Num()),
|
|
GRPCURL: fmt.Sprintf("%s:%d", host, grpcPort.Num()),
|
|
AdminUser: env["BACKEND_ADMIN_BOOTSTRAP_USER"],
|
|
AdminPassword: env["BACKEND_ADMIN_BOOTSTRAP_PASSWORD"],
|
|
}
|
|
}
|
|
|
|
// _ keeps filepath imported even when only the network helper grows
|
|
// here later.
|
|
var _ = filepath.Separator
|