feat: backend service
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
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
|
||||
Reference in New Issue
Block a user