diff --git a/workspace-server/internal/provisioner/provisioner.go b/workspace-server/internal/provisioner/provisioner.go index d50ad06b..4c19c204 100644 --- a/workspace-server/internal/provisioner/provisioner.go +++ b/workspace-server/internal/provisioner/provisioner.go @@ -481,6 +481,22 @@ func (p *Provisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string, e return "", fmt.Errorf("failed to create container: %w", err) } + // Seed /configs before the entrypoint starts. molecule-runtime reads + // /configs/config.yaml immediately; post-start copy races fast runtimes + // into a FileNotFoundError crash loop. + if cfg.TemplatePath != "" { + if err := p.CopyTemplateToContainer(ctx, resp.ID, cfg.TemplatePath); err != nil { + _ = p.cli.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}) + return "", fmt.Errorf("failed to copy template to container %s before start: %w", name, err) + } + } + if len(cfg.ConfigFiles) > 0 { + if err := p.WriteFilesToContainer(ctx, resp.ID, cfg.ConfigFiles); err != nil { + _ = p.cli.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}) + return "", fmt.Errorf("failed to write config files to container %s before start: %w", name, err) + } + } + if err := p.cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { // Clean up created container on start failure _ = p.cli.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}) @@ -496,20 +512,6 @@ func (p *Provisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string, e // /configs and /workspace, then drops to agent via gosu). No per-start // chown needed here. - // Copy template files into /configs if TemplatePath is set - if cfg.TemplatePath != "" { - if err := p.CopyTemplateToContainer(ctx, resp.ID, cfg.TemplatePath); err != nil { - log.Printf("Provisioner: warning — failed to copy template to container %s: %v", name, err) - } - } - - // Write generated config files into /configs if ConfigFiles is set - if len(cfg.ConfigFiles) > 0 { - if err := p.WriteFilesToContainer(ctx, resp.ID, cfg.ConfigFiles); err != nil { - log.Printf("Provisioner: warning — failed to write config files to container %s: %v", name, err) - } - } - // Resolve the host-mapped port. Retry inspect up to 3 times if Docker hasn't // bound the ephemeral port yet (rare race under heavy load). hostURL := InternalURL(cfg.WorkspaceID) // fallback to Docker-internal diff --git a/workspace-server/internal/provisioner/provisioner_test.go b/workspace-server/internal/provisioner/provisioner_test.go index 8d4a20f0..287b13a5 100644 --- a/workspace-server/internal/provisioner/provisioner_test.go +++ b/workspace-server/internal/provisioner/provisioner_test.go @@ -62,6 +62,24 @@ func TestValidateConfigSource_TemplateIsDirName(t *testing.T) { } } +func TestStartSeedsConfigsBeforeContainerStart(t *testing.T) { + src, err := os.ReadFile("provisioner.go") + if err != nil { + t.Fatalf("read provisioner.go: %v", err) + } + text := string(src) + copyTemplate := strings.Index(text, "p.CopyTemplateToContainer(ctx, resp.ID, cfg.TemplatePath)") + writeFiles := strings.Index(text, "p.WriteFilesToContainer(ctx, resp.ID, cfg.ConfigFiles)") + start := strings.Index(text, "p.cli.ContainerStart(ctx, resp.ID, container.StartOptions{})") + + if copyTemplate < 0 || writeFiles < 0 || start < 0 { + t.Fatalf("expected Start to copy template, write config files, and start container") + } + if !(copyTemplate < start && writeFiles < start) { + t.Fatalf("config seeding must happen before ContainerStart: copyTemplate=%d writeFiles=%d start=%d", copyTemplate, writeFiles, start) + } +} + // baseHostConfig returns a fresh HostConfig with typical pre-tier binds, // mimicking what Start() builds before calling ApplyTierConfig. func baseHostConfig(pluginsPath string) *container.HostConfig {