fix(provisioner): pin alpine digest + verify migration copy non-empty (#2490 RC follow-up) #2545

Merged
agent-reviewer merged 2 commits from fix/KI-013-migrate-legacy-names into main 2026-06-11 04:25:00 +00:00
3 changed files with 25 additions and 15 deletions
@@ -98,6 +98,12 @@ const (
// dependency into the provisioner. Kept in sync with models.KindPlatform
// (a provisioner test asserts the two agree).
WorkspaceKindPlatform = "platform"
// alpineImage is the digest-pinned alpine image used for throwaway
// containers that read/write volumes or migrate data. Pinning prevents
// supply-chain drift / compromised-tag attacks (core#2545).
// Matches workspace-server/Dockerfile FROM alpine:3.20@sha256:...
alpineImageDefault = "alpine:3.20@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e"
)
// WorkspaceConfig holds the parameters needed to provision a workspace container.
@@ -252,7 +258,8 @@ type dockerClient interface {
// Provisioner manages Docker containers for workspace agents.
type Provisioner struct {
cli dockerClient
cli dockerClient
alpineImage string // overridable in tests; production uses the digest-pinned default
}
// New creates a new Provisioner connected to the local Docker daemon.
@@ -261,7 +268,7 @@ func New() (*Provisioner, error) {
if err != nil {
return nil, fmt.Errorf("failed to connect to Docker: %w", err)
}
return &Provisioner{cli: cli}, nil
return &Provisioner{cli: cli, alpineImage: alpineImageDefault}, nil
}
// ContainerName returns the Docker container name for a workspace.
@@ -1262,7 +1269,7 @@ func (p *Provisioner) ExecRead(ctx context.Context, containerName, filePath stri
// Used as a fallback when ExecRead fails (container already stopped).
func (p *Provisioner) ReadFromVolume(ctx context.Context, volumeName, filePath string) ([]byte, error) {
resp, err := p.cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Image: p.alpineImage,
Cmd: []string{"cat", "/vol/" + filePath},
}, &container.HostConfig{
Binds: []string{volumeName + ":/vol:ro"},
@@ -1333,7 +1340,7 @@ func (p *Provisioner) WriteAuthTokenToVolume(ctx context.Context, workspaceID, t
}
volName := p.resolveConfigVolumeName(ctx, workspaceID)
resp, err := p.cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Image: p.alpineImage,
Cmd: []string{"sh", "-c", writeAuthTokenVolumeCmd()},
Env: []string{"TOKEN=" + token},
}, &container.HostConfig{
@@ -1417,9 +1424,12 @@ func (p *Provisioner) migrateVolumeIfNeeded(ctx context.Context, newName, legacy
}
// Copy data from legacy to new via a short-lived alpine container.
// The trailing test guards against silent empty copies (e.g. legacy
// volume unexpectedly bare) which would leave the workspace without
// its config on restart. Core#2545.
resp, err := p.cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Cmd: []string{"sh", "-c", "cp -a /legacy/. /new/"},
Image: p.alpineImage,
Cmd: []string{"sh", "-c", "cp -a /legacy/. /new/ && test -n \"$(ls -A /new/)\""},
}, &container.HostConfig{
Binds: []string{
legacyName + ":/legacy",
@@ -1765,7 +1775,7 @@ func (p *Provisioner) VolumeHasFile(ctx context.Context, workspaceID, relPath st
return false, nil
}
resp, err := p.cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Image: p.alpineImage,
Cmd: []string{"test", "-f", "/vol/" + relPath},
}, &container.HostConfig{
Binds: []string{volName + ":/vol:ro"},
@@ -181,7 +181,7 @@ const migrateTestWorkspaceID = "abcdef1234567890"
func TestResolveConfigVolumeName_LegacyExists_MigratesInPlace(t *testing.T) {
ctx := context.Background()
cli := newFakeDockerClient()
p := &Provisioner{cli: cli}
p := &Provisioner{cli: cli, alpineImage: "alpine"}
newName := ConfigVolumeName(migrateTestWorkspaceID)
legacyName := legacyConfigVolumeName(migrateTestWorkspaceID)
@@ -234,7 +234,7 @@ func TestResolveConfigVolumeName_LegacyExists_MigratesInPlace(t *testing.T) {
func TestResolveConfigVolumeName_LegacyAbsent_NoMigration(t *testing.T) {
ctx := context.Background()
cli := newFakeDockerClient()
p := &Provisioner{cli: cli}
p := &Provisioner{cli: cli, alpineImage: "alpine"}
newName := ConfigVolumeName(migrateTestWorkspaceID)
legacyName := legacyConfigVolumeName(migrateTestWorkspaceID)
@@ -259,7 +259,7 @@ func TestResolveConfigVolumeName_LegacyAbsent_NoMigration(t *testing.T) {
func TestResolveClaudeSessionVolumeName_LegacyExists_MigratesInPlace(t *testing.T) {
ctx := context.Background()
cli := newFakeDockerClient()
p := &Provisioner{cli: cli}
p := &Provisioner{cli: cli, alpineImage: "alpine"}
newName := ClaudeSessionVolumeName(migrateTestWorkspaceID)
legacyName := legacyClaudeSessionVolumeName(migrateTestWorkspaceID)
@@ -288,7 +288,7 @@ func TestResolveClaudeSessionVolumeName_LegacyExists_MigratesInPlace(t *testing.
func TestResolveClaudeSessionVolumeName_LegacyAbsent_NoMigration(t *testing.T) {
ctx := context.Background()
cli := newFakeDockerClient()
p := &Provisioner{cli: cli}
p := &Provisioner{cli: cli, alpineImage: "alpine"}
newName := ClaudeSessionVolumeName(migrateTestWorkspaceID)
legacyName := legacyClaudeSessionVolumeName(migrateTestWorkspaceID)
@@ -308,7 +308,7 @@ func TestResolveClaudeSessionVolumeName_LegacyAbsent_NoMigration(t *testing.T) {
func TestMigrateVolumeIfNeeded_CopyFails_PreservesLegacy(t *testing.T) {
ctx := context.Background()
cli := newFakeDockerClient()
p := &Provisioner{cli: cli}
p := &Provisioner{cli: cli, alpineImage: "alpine"}
newName := ConfigVolumeName(migrateTestWorkspaceID)
legacyName := legacyConfigVolumeName(migrateTestWorkspaceID)
@@ -328,7 +328,7 @@ func TestMigrateVolumeIfNeeded_CopyFails_PreservesLegacy(t *testing.T) {
func TestStop_FullIDAbsent_LegacyRemoved(t *testing.T) {
ctx := context.Background()
cli := newFakeDockerClient()
p := &Provisioner{cli: cli}
p := &Provisioner{cli: cli, alpineImage: "alpine"}
newName := ContainerName(migrateTestWorkspaceID)
legacyName := legacyContainerName(migrateTestWorkspaceID)
@@ -355,7 +355,7 @@ func TestStop_FullIDAbsent_LegacyRemoved(t *testing.T) {
func TestStop_BothAbsent_IsNoOp(t *testing.T) {
ctx := context.Background()
cli := newFakeDockerClient()
p := &Provisioner{cli: cli}
p := &Provisioner{cli: cli, alpineImage: "alpine"}
newName := ContainerName(migrateTestWorkspaceID)
legacyName := legacyContainerName(migrateTestWorkspaceID)
@@ -1443,7 +1443,7 @@ func TestMigrateVolumeIfNeeded_ExistingTruncatedVolume(t *testing.T) {
t.Skip("docker daemon unreachable:", pingErr)
}
p := &Provisioner{cli: cli}
p := &Provisioner{cli: cli, alpineImage: "alpine"}
workspaceID := "test-migrate-" + strconv.FormatInt(time.Now().UnixNano(), 10)
legacyName := legacyConfigVolumeName(workspaceID)
newName := ConfigVolumeName(workspaceID)