daemon: integrate QoL slices 1-5 (model/rename/catalog, history deletes, stats, repo prepare+imageUsed+built) — 95.3% cover green

This commit is contained in:
Raphael Westphal
2026-08-19 10:07:52 +02:00
parent 7287b831e4
commit 698b742cb3
16 changed files with 1506 additions and 36 deletions
+30 -20
View File
@@ -118,10 +118,12 @@ type SpawnJob struct {
UpdatedAt int64 `json:"-"`
}
// SpawnResult is the immediate POST /api/spawn response.
// SpawnResult is the immediate POST /api/spawn response. ImageUsed is the
// image the job will create the container from (registry lookup; the build
// itself may still fail on a missing custom image).
type SpawnResult struct {
SessionID string `json:"sessionId"`
ContainerID string `json:"containerId"`
SessionID string `json:"sessionId"`
ImageUsed string `json:"imageUsed"`
}
// Spawner owns the docker client, per-repo clone serialization and job state.
@@ -242,7 +244,16 @@ func (s *Spawner) Start(ctx context.Context, repo, branch string) (SpawnResult,
sessionID := newUUID()
s.setJob(sessionID, repo, stateCloning, "", "")
go s.runJob(repo, branch, sessionID)
return SpawnResult{SessionID: sessionID}, nil
return SpawnResult{SessionID: sessionID, ImageUsed: s.resolveImage(repo)}, nil
}
// resolveImage returns the image a repo's spawns use: the registered custom
// image when present, else the default worker image.
func (s *Spawner) resolveImage(repo string) string {
if custom, ok, _ := s.store.GetRepoImage(repo); ok && custom != "" {
return custom
}
return imageRefWorker
}
func (s *Spawner) imageExists(ctx context.Context) (bool, error) {
@@ -287,7 +298,7 @@ func (s *Spawner) runJob(repo, branch, sessionID string) {
return
}
s.setJob(sessionID, repo, stateCreating, "", "")
containerID, err := s.createAndStart(s.ctx, repo, slug, sessionID)
containerID, image, err := s.createAndStart(s.ctx, repo, slug, sessionID)
if err != nil {
s.setJob(sessionID, repo, stateError, "", err.Error())
return
@@ -296,7 +307,7 @@ func (s *Spawner) runJob(repo, branch, sessionID string) {
s.setJob(sessionID, repo, stateError, containerID, "container started but not persisted: "+err.Error())
return
}
s.setJob(sessionID, repo, stateRunning, containerID, "")
s.setJob(sessionID, repo, stateRunning, containerID, "image "+image)
}
// cloneOrUpdate clones the repo into reposDir/<slug> or fast-forwards it.
@@ -466,22 +477,21 @@ 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)
func (s *Spawner) createAndStart(ctx context.Context, repo, slug, sessionID string) (string, string, error) {
image := s.resolveImage(repo)
if image != imageRefWorker {
exists, err := s.imageRefExists(ctx, image)
if err != nil {
return "", fmt.Errorf("docker unavailable: %w", err)
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)
return "", "", fmt.Errorf("custom image %s not built (ask the ops agent to build it)", image)
}
image = custom
}
repoVolume := volumeRepoPrefix + slug
fresh, err := s.ensureRepoVolume(ctx, repoVolume)
if err != nil {
return "", err
return "", "", err
}
if fresh {
if err := s.seedVolume(ctx, slug, repoVolume, sessionID); err != nil {
@@ -490,14 +500,14 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, sessionID stri
if rmErr := s.cli.VolumeRemove(context.Background(), repoVolume, true); rmErr != nil {
log.Printf("spawner: remove failed seed volume %s: %v", repoVolume, rmErr)
}
return "", fmt.Errorf("seed %s: %w", repoVolume, err)
return "", "", fmt.Errorf("seed %s: %w", repoVolume, err)
}
}
if _, err := s.cli.VolumeCreate(ctx, volume.CreateOptions{Name: volumeSessions}); err != nil {
return "", fmt.Errorf("volume %s: %w", volumeSessions, err)
return "", "", fmt.Errorf("volume %s: %w", volumeSessions, err)
}
if _, err := s.cli.VolumeCreate(ctx, volume.CreateOptions{Name: volumePiCache}); err != nil {
return "", fmt.Errorf("volume %s: %w", volumePiCache, err)
return "", "", fmt.Errorf("volume %s: %w", volumePiCache, err)
}
binds := []string{
@@ -526,13 +536,13 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, sessionID stri
name := "lvmh-agent-" + strings.ReplaceAll(sessionID, "-", "")[:12]
created, err := s.cli.ContainerCreate(ctx, cfg, hostCfg, nil, nil, name)
if err != nil {
return "", fmt.Errorf("docker create: %w", err)
return "", "", fmt.Errorf("docker create: %w", err)
}
if err := s.cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil {
s.removeContainer(context.Background(), created.ID)
return "", fmt.Errorf("docker start: %w", err)
return "", "", fmt.Errorf("docker start: %w", err)
}
return created.ID, nil
return created.ID, image, nil
}
// ensureRepoVolume creates the per-repo volume; reports whether it is fresh.