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,367 @@
package internalhttp
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"galaxy/rtmanager/internal/api/internalhttp/handlers"
domainruntime "galaxy/rtmanager/internal/domain/runtime"
"galaxy/rtmanager/internal/ports"
"galaxy/rtmanager/internal/service/cleanupcontainer"
"galaxy/rtmanager/internal/service/patchruntime"
"galaxy/rtmanager/internal/service/restartruntime"
"galaxy/rtmanager/internal/service/startruntime"
"galaxy/rtmanager/internal/service/stopruntime"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers"
"github.com/getkin/kin-openapi/routers/legacy"
"github.com/stretchr/testify/require"
)
// TestInternalRESTConformance loads the OpenAPI specification, drives
// every runtime operation against the live internal HTTP listener
// backed by stub services, and validates each response body against
// the spec via `openapi3filter.ValidateResponse`. The test catches
// drift between the wire shape produced by the handler layer and the
// frozen contract; failure-path response shapes are validated by the
// per-handler tests in `handlers/<op>_test.go`.
func TestInternalRESTConformance(t *testing.T) {
t.Parallel()
doc := loadConformanceSpec(t)
router, err := legacy.NewRouter(doc)
require.NoError(t, err)
deps := newConformanceDeps(t)
server, err := NewServer(newConformanceConfig(), Dependencies{
Logger: nil,
Telemetry: nil,
Readiness: nil,
RuntimeRecords: deps.records,
StartRuntime: deps.start,
StopRuntime: deps.stop,
RestartRuntime: deps.restart,
PatchRuntime: deps.patch,
CleanupContainer: deps.cleanup,
})
require.NoError(t, err)
cases := []conformanceCase{
{
name: "internalListRuntimes",
method: http.MethodGet,
path: "/api/v1/internal/runtimes",
},
{
name: "internalGetRuntime",
method: http.MethodGet,
path: "/api/v1/internal/runtimes/" + conformanceGameID,
},
{
name: "internalStartRuntime",
method: http.MethodPost,
path: "/api/v1/internal/runtimes/" + conformanceGameID + "/start",
contentType: "application/json",
body: `{"image_ref":"galaxy/game:v1.2.3"}`,
},
{
name: "internalStopRuntime",
method: http.MethodPost,
path: "/api/v1/internal/runtimes/" + conformanceGameID + "/stop",
contentType: "application/json",
body: `{"reason":"admin_request"}`,
},
{
name: "internalRestartRuntime",
method: http.MethodPost,
path: "/api/v1/internal/runtimes/" + conformanceGameID + "/restart",
},
{
name: "internalPatchRuntime",
method: http.MethodPost,
path: "/api/v1/internal/runtimes/" + conformanceGameID + "/patch",
contentType: "application/json",
body: `{"image_ref":"galaxy/game:v1.2.4"}`,
},
{
name: "internalCleanupRuntimeContainer",
method: http.MethodDelete,
path: "/api/v1/internal/runtimes/" + conformanceGameID + "/container",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
runConformanceCase(t, server.handler, router, tc)
})
}
}
// conformanceGameID is the path variable used for every per-game
// conformance request.
const conformanceGameID = "game-conformance"
// conformanceServerURL mirrors the canonical `servers[0].url` entry in
// `rtmanager/api/internal-openapi.yaml`. The legacy router matches
// requests against this prefix; updating the spec's server URL
// requires updating this constant.
const conformanceServerURL = "http://localhost:8096"
// conformanceCase describes one request the conformance test drives.
type conformanceCase struct {
name string
method string
path string
contentType string
body string
}
func runConformanceCase(t *testing.T, handler http.Handler, router routers.Router, tc conformanceCase) {
t.Helper()
// Drive the handler with the path-only form so the listener's
// http.ServeMux matches the registered routes (which use raw paths,
// without the OpenAPI server URL prefix).
var bodyReader io.Reader
if tc.body != "" {
bodyReader = strings.NewReader(tc.body)
}
request := httptest.NewRequest(tc.method, tc.path, bodyReader)
if tc.contentType != "" {
request.Header.Set("Content-Type", tc.contentType)
}
request.Header.Set("X-Galaxy-Caller", "admin")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
require.Equalf(t, http.StatusOK, recorder.Code, "operation %s returned %d: %s", tc.name, recorder.Code, recorder.Body.String())
// kin-openapi's legacy router requires the request URL to match a
// `servers[].url` entry; rebuild the validation request with the
// canonical local server URL declared in the spec.
validationURL := conformanceServerURL + tc.path
validationRequest := httptest.NewRequest(tc.method, validationURL, bodyReaderFor(tc.body))
if tc.contentType != "" {
validationRequest.Header.Set("Content-Type", tc.contentType)
}
validationRequest.Header.Set("X-Galaxy-Caller", "admin")
route, pathParams, err := router.FindRoute(validationRequest)
require.NoError(t, err)
requestInput := &openapi3filter.RequestValidationInput{
Request: validationRequest,
PathParams: pathParams,
Route: route,
Options: &openapi3filter.Options{
IncludeResponseStatus: true,
},
}
require.NoError(t, openapi3filter.ValidateRequest(context.Background(), requestInput))
responseInput := &openapi3filter.ResponseValidationInput{
RequestValidationInput: requestInput,
Status: recorder.Code,
Header: recorder.Header(),
Options: &openapi3filter.Options{
IncludeResponseStatus: true,
},
}
responseInput.SetBodyBytes(recorder.Body.Bytes())
require.NoError(t, openapi3filter.ValidateResponse(context.Background(), responseInput))
}
func loadConformanceSpec(t *testing.T) *openapi3.T {
t.Helper()
_, thisFile, _, ok := runtime.Caller(0)
require.True(t, ok)
specPath := filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "api", "internal-openapi.yaml")
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(specPath)
require.NoError(t, err)
require.NoError(t, doc.Validate(context.Background()))
return doc
}
func bodyReaderFor(raw string) io.Reader {
if raw == "" {
return http.NoBody
}
return bytes.NewBufferString(raw)
}
// conformanceDeps groups the stub collaborators handed to the listener.
type conformanceDeps struct {
records *conformanceRecords
start *conformanceStart
stop *conformanceStop
restart *conformanceRestart
patch *conformancePatch
cleanup *conformanceCleanup
}
func newConformanceDeps(t *testing.T) *conformanceDeps {
t.Helper()
return &conformanceDeps{
records: newConformanceRecords(),
start: &conformanceStart{},
stop: &conformanceStop{},
restart: &conformanceRestart{},
patch: &conformancePatch{},
cleanup: &conformanceCleanup{},
}
}
func newConformanceConfig() Config {
return Config{
Addr: ":0",
ReadHeaderTimeout: time.Second,
ReadTimeout: time.Second,
WriteTimeout: time.Second,
IdleTimeout: time.Second,
}
}
// conformanceRecord builds a canonical running record used by every
// stub service.
func conformanceRecord() domainruntime.RuntimeRecord {
started := time.Date(2026, 4, 26, 13, 0, 0, 0, time.UTC)
return domainruntime.RuntimeRecord{
GameID: conformanceGameID,
Status: domainruntime.StatusRunning,
CurrentContainerID: "container-conformance",
CurrentImageRef: "galaxy/game:v1.2.3",
EngineEndpoint: "http://galaxy-game-" + conformanceGameID + ":8080",
StatePath: "/var/lib/galaxy/" + conformanceGameID,
DockerNetwork: "galaxy-engine",
StartedAt: &started,
LastOpAt: started,
CreatedAt: started,
}
}
// conformanceRecords is an in-memory record store seeded with one
// canonical record so the get / list endpoints have something to
// return.
type conformanceRecords struct {
mu sync.Mutex
stored map[string]domainruntime.RuntimeRecord
}
func newConformanceRecords() *conformanceRecords {
return &conformanceRecords{
stored: map[string]domainruntime.RuntimeRecord{
conformanceGameID: conformanceRecord(),
},
}
}
func (s *conformanceRecords) Get(_ context.Context, gameID string) (domainruntime.RuntimeRecord, error) {
s.mu.Lock()
defer s.mu.Unlock()
record, ok := s.stored[gameID]
if !ok {
return domainruntime.RuntimeRecord{}, domainruntime.ErrNotFound
}
return record, nil
}
func (s *conformanceRecords) Upsert(_ context.Context, _ domainruntime.RuntimeRecord) error {
return errors.New("not used in conformance test")
}
func (s *conformanceRecords) UpdateStatus(_ context.Context, _ ports.UpdateStatusInput) error {
return errors.New("not used in conformance test")
}
func (s *conformanceRecords) ListByStatus(_ context.Context, _ domainruntime.Status) ([]domainruntime.RuntimeRecord, error) {
return nil, errors.New("not used in conformance test")
}
func (s *conformanceRecords) List(_ context.Context) ([]domainruntime.RuntimeRecord, error) {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]domainruntime.RuntimeRecord, 0, len(s.stored))
for _, record := range s.stored {
out = append(out, record)
}
return out, nil
}
// conformanceStart is the stub StartService used by the conformance
// test. Every Handle call returns the canonical record.
type conformanceStart struct{}
func (s *conformanceStart) Handle(_ context.Context, _ startruntime.Input) (startruntime.Result, error) {
return startruntime.Result{
Record: conformanceRecord(),
Outcome: "success",
}, nil
}
type conformanceStop struct{}
func (s *conformanceStop) Handle(_ context.Context, _ stopruntime.Input) (stopruntime.Result, error) {
rec := conformanceRecord()
rec.Status = domainruntime.StatusStopped
stopped := rec.LastOpAt.Add(time.Second)
rec.StoppedAt = &stopped
rec.LastOpAt = stopped
return stopruntime.Result{Record: rec, Outcome: "success"}, nil
}
type conformanceRestart struct{}
func (s *conformanceRestart) Handle(_ context.Context, _ restartruntime.Input) (restartruntime.Result, error) {
return restartruntime.Result{Record: conformanceRecord(), Outcome: "success"}, nil
}
type conformancePatch struct{}
func (s *conformancePatch) Handle(_ context.Context, in patchruntime.Input) (patchruntime.Result, error) {
rec := conformanceRecord()
if in.NewImageRef != "" {
rec.CurrentImageRef = in.NewImageRef
}
return patchruntime.Result{Record: rec, Outcome: "success"}, nil
}
type conformanceCleanup struct{}
func (s *conformanceCleanup) Handle(_ context.Context, _ cleanupcontainer.Input) (cleanupcontainer.Result, error) {
rec := conformanceRecord()
rec.Status = domainruntime.StatusRemoved
rec.CurrentContainerID = ""
removed := rec.LastOpAt.Add(time.Minute)
rec.RemovedAt = &removed
rec.LastOpAt = removed
return cleanupcontainer.Result{Record: rec, Outcome: "success"}, nil
}
// Compile-time guards: the stubs must satisfy the handler-level
// service ports plus ports.RuntimeRecordStore so the listener accepts
// them.
var (
_ handlers.StartService = (*conformanceStart)(nil)
_ handlers.StopService = (*conformanceStop)(nil)
_ handlers.RestartService = (*conformanceRestart)(nil)
_ handlers.PatchService = (*conformancePatch)(nil)
_ handlers.CleanupService = (*conformanceCleanup)(nil)
_ ports.RuntimeRecordStore = (*conformanceRecords)(nil)
)