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:
+50
-23
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user