fix(provisioner): pin alpine digest + verify migration copy non-empty (#2490 RC follow-up) #2545
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user