feat: runtime manager
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user