feat: runtime manager

This commit is contained in:
Ilia Denisov
2026-04-28 20:39:18 +02:00
committed by GitHub
parent e0a99b346b
commit a7cee15115
289 changed files with 45660 additions and 2207 deletions
@@ -0,0 +1,66 @@
// Package engineimage resolves the Docker reference Lobby publishes on
// `runtime:start_jobs`. The reference is built from a configurable
// template that must contain the literal `{engine_version}` placeholder
// and a per-game `target_engine_version`.
//
// The resolver intentionally performs only template substitution and a
// non-empty-version guard. Semver validation of the engine version
// itself lives in `lobby/internal/domain/game` and runs at game-record
// construction time; by the time `startgame.Service.Handle` reads the
// record the version is already validated.
package engineimage
import (
"errors"
"fmt"
"strings"
)
// VersionPlaceholder is the literal token a template must contain. The
// resolver substitutes it with the per-game engine version verbatim.
const VersionPlaceholder = "{engine_version}"
// Resolver substitutes a per-game engine version into a pre-validated
// template. The template is validated once at construction so per-game
// `Resolve` calls remain pure string substitution.
type Resolver struct {
template string
}
// NewResolver returns a Resolver that uses template for every Resolve
// call. It returns an error if template is empty or does not contain
// VersionPlaceholder.
func NewResolver(template string) (*Resolver, error) {
trimmed := strings.TrimSpace(template)
if trimmed == "" {
return nil, errors.New("engine image resolver: template must not be empty")
}
if !strings.Contains(trimmed, VersionPlaceholder) {
return nil, fmt.Errorf(
"engine image resolver: template %q must contain placeholder %q",
template, VersionPlaceholder,
)
}
return &Resolver{template: trimmed}, nil
}
// Template returns the validated template string the resolver was
// constructed with. The accessor is intended for diagnostics and tests.
func (resolver *Resolver) Template() string {
if resolver == nil {
return ""
}
return resolver.template
}
// Resolve substitutes VersionPlaceholder in the validated template with
// version. It returns an error when version is empty or whitespace.
func (resolver *Resolver) Resolve(version string) (string, error) {
if resolver == nil {
return "", errors.New("engine image resolver: nil resolver")
}
if strings.TrimSpace(version) == "" {
return "", errors.New("engine image resolver: engine version must not be empty")
}
return strings.ReplaceAll(resolver.template, VersionPlaceholder, version), nil
}
@@ -0,0 +1,96 @@
package engineimage_test
import (
"testing"
"galaxy/lobby/internal/domain/engineimage"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewResolverAcceptsValidTemplate(t *testing.T) {
t.Parallel()
resolver, err := engineimage.NewResolver("galaxy/game:{engine_version}")
require.NoError(t, err)
require.NotNil(t, resolver)
assert.Equal(t, "galaxy/game:{engine_version}", resolver.Template())
}
func TestNewResolverRejectsEmptyTemplate(t *testing.T) {
t.Parallel()
cases := []string{"", " "}
for _, candidate := range cases {
_, err := engineimage.NewResolver(candidate)
require.Error(t, err)
}
}
func TestNewResolverRejectsTemplateWithoutPlaceholder(t *testing.T) {
t.Parallel()
_, err := engineimage.NewResolver("galaxy/game:1.0.0")
require.Error(t, err)
}
func TestResolveSubstitutesVersion(t *testing.T) {
t.Parallel()
resolver, err := engineimage.NewResolver("registry.example.com/galaxy/game:{engine_version}")
require.NoError(t, err)
got, err := resolver.Resolve("v1.4.7")
require.NoError(t, err)
assert.Equal(t, "registry.example.com/galaxy/game:v1.4.7", got)
}
func TestResolveSubstitutesEveryPlaceholderOccurrence(t *testing.T) {
t.Parallel()
resolver, err := engineimage.NewResolver(
"registry.example.com/{engine_version}/game:{engine_version}",
)
require.NoError(t, err)
got, err := resolver.Resolve("v2.0.1")
require.NoError(t, err)
assert.Equal(t, "registry.example.com/v2.0.1/game:v2.0.1", got)
}
func TestResolveRejectsEmptyVersion(t *testing.T) {
t.Parallel()
resolver, err := engineimage.NewResolver("galaxy/game:{engine_version}")
require.NoError(t, err)
cases := []string{"", " "}
for _, candidate := range cases {
_, err := resolver.Resolve(candidate)
require.Error(t, err)
}
}
func TestResolveReusesValidatedTemplate(t *testing.T) {
t.Parallel()
resolver, err := engineimage.NewResolver("galaxy/game:{engine_version}")
require.NoError(t, err)
first, err := resolver.Resolve("v1.0.0")
require.NoError(t, err)
second, err := resolver.Resolve("v2.0.0")
require.NoError(t, err)
assert.Equal(t, "galaxy/game:v1.0.0", first)
assert.Equal(t, "galaxy/game:v2.0.0", second)
}
func TestNilResolverResolveReturnsError(t *testing.T) {
t.Parallel()
var resolver *engineimage.Resolver
_, err := resolver.Resolve("v1.0.0")
require.Error(t, err)
}