daemon: agent-driven workspace images — repo→image registry (GET/PUT/DELETE /api/repos), spawn resolves custom image, self-healing lvmh-ops control container with docker.sock; 132 tests, e2e 87/87

This commit is contained in:
Raphael Westphal
2026-08-18 23:54:09 +02:00
parent 8fcaeb7c03
commit 7287b831e4
15 changed files with 1216 additions and 38 deletions
+50 -23
View File
@@ -132,11 +132,12 @@ type Spawner struct {
cli *client.Client
baseURL string // gitlab base, for clone URLs
reposDir string
dockerfile string
buildContext string
containerURL string
network string
reposDir string
dockerfile string
controlDockerfile string
buildContext string
containerURL string
network string
mu sync.Mutex
jobs map[string]*SpawnJob // keyed by sessionId
@@ -151,18 +152,19 @@ func NewSpawner(ctx context.Context, store *Store, hub *Hub, baseURL string) (*S
}
dockerfile := envOr(envWorkerDockerfile, defaultDockerfile)
return &Spawner{
ctx: ctx,
store: store,
hub: hub,
cli: cli,
baseURL: strings.TrimRight(baseURL, "/"),
reposDir: envOr(envRepoDir, defaultRepoDir),
dockerfile: dockerfile,
buildContext: envOr(envWorkerContext, filepath.Dir(filepath.Dir(dockerfile))),
containerURL: envOr(envContainerLVMHURL, defaultContainerURL),
network: envOr(envContainerNetwork, defaultNetwork),
jobs: make(map[string]*SpawnJob),
slugLocks: make(map[string]*sync.Mutex),
ctx: ctx,
store: store,
hub: hub,
cli: cli,
baseURL: strings.TrimRight(baseURL, "/"),
reposDir: envOr(envRepoDir, defaultRepoDir),
dockerfile: dockerfile,
controlDockerfile: envOr(envControlDockerfile, defaultControlDockerfile),
buildContext: envOr(envWorkerContext, filepath.Dir(filepath.Dir(dockerfile))),
containerURL: envOr(envContainerLVMHURL, defaultContainerURL),
network: envOr(envContainerNetwork, defaultNetwork),
jobs: make(map[string]*SpawnJob),
slugLocks: make(map[string]*sync.Mutex),
}, nil
}
@@ -244,7 +246,12 @@ func (s *Spawner) Start(ctx context.Context, repo, branch string) (SpawnResult,
}
func (s *Spawner) imageExists(ctx context.Context) (bool, error) {
args := filters.NewArgs(filters.Arg("reference", imageRefWorker))
return s.imageRefExists(ctx, imageRefWorker)
}
// imageRefExists reports whether ref is present in the local docker store.
func (s *Spawner) imageRefExists(ctx context.Context, ref string) (bool, error) {
args := filters.NewArgs(filters.Arg("reference", ref))
summaries, err := s.cli.ImageList(ctx, image.ListOptions{Filters: args})
if err != nil {
return false, err
@@ -391,19 +398,26 @@ func (s *Spawner) ensureImage(ctx context.Context) error {
if _, err := os.Stat(s.dockerfile); err != nil {
return fmt.Errorf("worker Dockerfile missing at %s: %w", s.dockerfile, err)
}
relDockerfile, err := filepath.Rel(s.buildContext, s.dockerfile)
return s.buildImage(ctx, s.buildContext, s.dockerfile, imageRefWorker)
}
// buildImage streams a tar of buildContext into docker ImageBuild, building
// tag from dockerfile (relative to buildContext). Shared by the worker and
// ops image paths.
func (s *Spawner) buildImage(ctx context.Context, buildContext, dockerfile, tag string) error {
relDockerfile, err := filepath.Rel(buildContext, dockerfile)
if err != nil {
return err
}
pr, pw := io.Pipe()
tarDone := make(chan error, 1)
go func() {
err := tarDir(pw, s.buildContext)
err := tarDir(pw, buildContext)
_ = pw.CloseWithError(err)
tarDone <- err
}()
resp, buildErr := s.cli.ImageBuild(ctx, pr, build.ImageBuildOptions{
Tags: []string{imageRefWorker},
Tags: []string{tag},
Dockerfile: relDockerfile,
Remove: true,
})
@@ -414,7 +428,7 @@ func (s *Spawner) ensureImage(ctx context.Context) error {
if resp.Body != nil {
_ = resp.Body.Close()
}
return fmt.Errorf("build context %s: %w", s.buildContext, tarErr)
return fmt.Errorf("build context %s: %w", buildContext, tarErr)
}
if buildErr != nil {
return fmt.Errorf("docker build: %w", buildErr)
@@ -450,7 +464,20 @@ func extractBuildError(body []byte) string {
}
// createAndStart provisions volumes, creates and starts the worker container.
// A repo-registered custom image (see /api/repos) overrides the default
// worker image; the ops agent builds and registers those.
func (s *Spawner) createAndStart(ctx context.Context, repo, slug, sessionID string) (string, error) {
image := imageRefWorker
if custom, ok, _ := s.store.GetRepoImage(repo); ok && custom != "" {
exists, err := s.imageRefExists(ctx, custom)
if err != nil {
return "", fmt.Errorf("docker unavailable: %w", err)
}
if !exists {
return "", fmt.Errorf("custom image %s not built (ask the ops agent to build it)", custom)
}
image = custom
}
repoVolume := volumeRepoPrefix + slug
fresh, err := s.ensureRepoVolume(ctx, repoVolume)
if err != nil {
@@ -479,7 +506,7 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, sessionID stri
volumePiCache + ":" + cacheMount,
}
cfg := &container.Config{
Image: imageRefWorker,
Image: image,
Env: []string{
envProviderAPIKey + "=" + os.Getenv(envProviderAPIKey),
envToken + "=" + daemonToken,