From 5c30b7f48fb8e08b7de9486a5c843d30a89bbd7f Mon Sep 17 00:00:00 2001 From: Pascal Wiedenbeck Date: Mon, 4 May 2026 10:38:58 +0200 Subject: [PATCH 1/2] Add windows_create_parent_dirs option with implementation --- CHANGELOG.md | 6 +++ builder/docker/config.go | 6 +++ builder/docker/config.hcl2spec.go | 2 + builder/docker/config_test.go | 35 +++++++++++++ .../docker/windows_container_communicator.go | 49 ++++++++++++++++++- .../builder/docker/Config-not-required.mdx | 3 ++ docs/builders/docker.mdx | 7 +++ 7 files changed, 106 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4310086f..a01f13ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### FEATURES: + +* builder/docker: Add `windows_create_parent_dirs` option to automatically create destination parent directories on uploads to Windows containers. [GH-208] + ## 1.1.2 (July 31, 2025) ### IMPROVEMENTS: diff --git a/builder/docker/config.go b/builder/docker/config.go index 4446dda1..37dd7b1d 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -139,6 +139,9 @@ type Config struct { // running on a windows host. This is necessary for building Windows // containers, because our normal docker bindings do not work for them. WindowsContainer bool `mapstructure:"windows_container" required:"false"` + // If true, creates parent directories for file and directory uploads to + // Windows containers. This only applies when `windows_container` is true. + WindowsCreateParentDirs bool `mapstructure:"windows_create_parent_dirs" required:"false"` // Set platform if server is multi-platform capable Platform string `mapstructure:"platform" required:"false"` @@ -205,6 +208,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { var errs *packersdk.MultiError var warnings []string + if c.WindowsCreateParentDirs && !c.WindowsContainer { + warnings = append(warnings, "`windows_create_parent_dirs` only applies when `windows_container` is true") + } if !c.BuildConfig.IsDefault() { _, err := c.BuildConfig.Prepare() diff --git a/builder/docker/config.hcl2spec.go b/builder/docker/config.hcl2spec.go index c781b996..8c2bbfbf 100644 --- a/builder/docker/config.hcl2spec.go +++ b/builder/docker/config.hcl2spec.go @@ -121,6 +121,7 @@ type FlatConfig struct { Volumes map[string]string `mapstructure:"volumes" required:"false" cty:"volumes" hcl:"volumes"` FixUploadOwner *bool `mapstructure:"fix_upload_owner" required:"false" cty:"fix_upload_owner" hcl:"fix_upload_owner"` WindowsContainer *bool `mapstructure:"windows_container" required:"false" cty:"windows_container" hcl:"windows_container"` + WindowsCreateParentDirs *bool `mapstructure:"windows_create_parent_dirs" required:"false" cty:"windows_create_parent_dirs" hcl:"windows_create_parent_dirs"` Platform *string `mapstructure:"platform" required:"false" cty:"platform" hcl:"platform"` Login *bool `mapstructure:"login" required:"false" cty:"login" hcl:"login"` LoginPassword *string `mapstructure:"login_password" required:"false" cty:"login_password" hcl:"login_password"` @@ -226,6 +227,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "volumes": &hcldec.AttrSpec{Name: "volumes", Type: cty.Map(cty.String), Required: false}, "fix_upload_owner": &hcldec.AttrSpec{Name: "fix_upload_owner", Type: cty.Bool, Required: false}, "windows_container": &hcldec.AttrSpec{Name: "windows_container", Type: cty.Bool, Required: false}, + "windows_create_parent_dirs": &hcldec.AttrSpec{Name: "windows_create_parent_dirs", Type: cty.Bool, Required: false}, "platform": &hcldec.AttrSpec{Name: "platform", Type: cty.String, Required: false}, "login": &hcldec.AttrSpec{Name: "login", Type: cty.Bool, Required: false}, "login_password": &hcldec.AttrSpec{Name: "login_password", Type: cty.String, Required: false}, diff --git a/builder/docker/config_test.go b/builder/docker/config_test.go index 03e5abcb..f895b19d 100644 --- a/builder/docker/config_test.go +++ b/builder/docker/config_test.go @@ -6,6 +6,7 @@ package docker import ( "io/ioutil" "os" + "strings" "testing" ) @@ -149,6 +150,40 @@ func TestConfigPrepare_pull(t *testing.T) { } } +func TestConfigPrepare_windowsCreateParentDirs(t *testing.T) { + raw := testConfig() + var c Config + warns, errs := c.Prepare(raw) + testConfigOk(t, warns, errs) + if c.WindowsCreateParentDirs { + t.Fatal("windows_create_parent_dirs should default to false") + } + + raw = testConfig() + raw["windows_container"] = true + raw["windows_create_parent_dirs"] = true + c = Config{} + warns, errs = c.Prepare(raw) + testConfigOk(t, warns, errs) + if !c.WindowsCreateParentDirs { + t.Fatal("windows_create_parent_dirs should be true") + } + + raw = testConfig() + raw["windows_create_parent_dirs"] = true + c = Config{} + warns, errs = c.Prepare(raw) + if errs != nil { + t.Fatalf("bad: %s", errs) + } + if len(warns) != 1 { + t.Fatalf("expected one warning, got %#v", warns) + } + if !strings.Contains(warns[0], "windows_create_parent_dirs") { + t.Fatalf("expected windows_create_parent_dirs warning, got %#v", warns) + } +} + // Test variations of a build bootstrap config; including unset func TestConfigBuildBootstrapConfig(t *testing.T) { tests := []struct { diff --git a/builder/docker/windows_container_communicator.go b/builder/docker/windows_container_communicator.go index a2e827b6..d3035d2c 100644 --- a/builder/docker/windows_container_communicator.go +++ b/builder/docker/windows_container_communicator.go @@ -12,6 +12,7 @@ import ( "log" "os" "path/filepath" + "strings" packersdk "github.com/hashicorp/packer-plugin-sdk/packer" ) @@ -28,6 +29,32 @@ type WindowsContainerCommunicator struct { Communicator } +func powerShellSingleQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", "''") + "'" +} + +// Creates the parent and all other preceding directories in the container for the given destination. +// Does not recreate paths that already exist. +func (c *WindowsContainerCommunicator) ensureContainerParentDir(ctx context.Context, destination string) error { + cmd := &packersdk.RemoteCmd{ + Command: strings.Join([]string{ + fmt.Sprintf("Split-Path -Parent %s", powerShellSingleQuote(destination)), + "Where-Object { $_ -and -not (Test-Path -LiteralPath $_) }", + "ForEach-Object { New-Item -ItemType Directory -Force -Path $_ | Out-Null }", + }, " | "), + } + if err := c.Start(ctx, cmd); err != nil { + return err + } + + cmd.Wait() + if cmd.ExitStatus() != 0 { + return fmt.Errorf("Upload failed to create parent directory for %s: %d", destination, cmd.ExitStatus()) + } + + return nil +} + // Upload uses docker exec to copy the file from the host to the container func (c *WindowsContainerCommunicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error { // Create a temporary file to store the upload @@ -48,13 +75,22 @@ func (c *WindowsContainerCommunicator) Upload(dst string, src io.Reader, fi *os. } tempfile.Close() + // Before copying the file into place, we need to make sure that the parent folders + // exists if windows_create_parent_dirs was specified in the plugin config. + // See also https://github.com/hashicorp/packer-plugin-docker/issues/208 + ctx := context.TODO() + if c.Config.WindowsCreateParentDirs { + if err := c.ensureContainerParentDir(ctx, dst); err != nil { + return err + } + } + // Copy the file into place by copying the temporary file we put // into the shared folder into the proper location in the container cmd := &packersdk.RemoteCmd{ Command: fmt.Sprintf("Copy-Item -Path %s/%s -Destination %s", c.ContainerDir, filepath.Base(tempfile.Name()), dst), } - ctx := context.TODO() if err := c.Start(ctx, cmd); err != nil { return err } @@ -135,12 +171,21 @@ func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude containerDst = filepath.Join(dst, filepath.Base(src)) } + // Before copying the files into place, we need to make sure that the parent folders + // exists if windows_create_parent_dirs was specified in the plugin config. + // See also https://github.com/hashicorp/packer-plugin-docker/issues/208 + ctx := context.TODO() + if c.Config.WindowsCreateParentDirs { + if err := c.ensureContainerParentDir(ctx, containerDst); err != nil { + return err + } + } + // Make the directory, then copy into it cmd := &packersdk.RemoteCmd{ Command: fmt.Sprintf("Copy-Item %s -Destination %s -Recurse", containerSrc, containerDst), } - ctx := context.TODO() if err := c.Start(ctx, cmd); err != nil { return err } diff --git a/docs-partials/builder/docker/Config-not-required.mdx b/docs-partials/builder/docker/Config-not-required.mdx index eb132edc..093639b9 100644 --- a/docs-partials/builder/docker/Config-not-required.mdx +++ b/docs-partials/builder/docker/Config-not-required.mdx @@ -98,6 +98,9 @@ running on a windows host. This is necessary for building Windows containers, because our normal docker bindings do not work for them. +- `windows_create_parent_dirs` (bool) - If true, creates parent directories for file and directory uploads to + Windows containers. This only applies when `windows_container` is true. + - `platform` (string) - Set platform if server is multi-platform capable - `login` (bool) - This is used to login to a private docker repository (e.g., dockerhub) diff --git a/docs/builders/docker.mdx b/docs/builders/docker.mdx index a76ebc96..b156aa1d 100644 --- a/docs/builders/docker.mdx +++ b/docs/builders/docker.mdx @@ -418,6 +418,11 @@ If you are building a Windows container, you must set the template option `"windows_container": true`. Please note that docker cannot export Windows containers, so you must either commit or discard them. +Set `"windows_create_parent_dirs": true` to have Packer create missing parent +directories before uploading files or directories to Windows containers. This +helps Windows container uploads behave more like Linux container uploads that +use `docker cp`. + The following is a fully functional template for building a Windows container. @@ -428,6 +433,7 @@ source "docker" "windows" { image = "microsoft/windowsservercore:1709" container_dir = "c:/app" windows_container = true + windows_create_parent_dirs = true commit = true } @@ -446,6 +452,7 @@ build { "image": "microsoft/windowsservercore:1709", "container_dir": "c:/app", "windows_container": true, + "windows_create_parent_dirs": true, "commit": true } ] From 8bf99b7c45a3d496d370b723c6865e2633980c0a Mon Sep 17 00:00:00 2001 From: Pascal Wiedenbeck Date: Tue, 9 Jun 2026 09:31:50 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- builder/docker/windows_container_communicator.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/builder/docker/windows_container_communicator.go b/builder/docker/windows_container_communicator.go index d3035d2c..d8e09e1c 100644 --- a/builder/docker/windows_container_communicator.go +++ b/builder/docker/windows_container_communicator.go @@ -37,11 +37,7 @@ func powerShellSingleQuote(s string) string { // Does not recreate paths that already exist. func (c *WindowsContainerCommunicator) ensureContainerParentDir(ctx context.Context, destination string) error { cmd := &packersdk.RemoteCmd{ - Command: strings.Join([]string{ - fmt.Sprintf("Split-Path -Parent %s", powerShellSingleQuote(destination)), - "Where-Object { $_ -and -not (Test-Path -LiteralPath $_) }", - "ForEach-Object { New-Item -ItemType Directory -Force -Path $_ | Out-Null }", - }, " | "), + Command: fmt.Sprintf("$parent = Split-Path -Parent %s; if ($parent -and -not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Force -LiteralPath $parent -ErrorAction Stop | Out-Null }", powerShellSingleQuote(destination)), } if err := c.Start(ctx, cmd); err != nil { return err @@ -88,8 +84,7 @@ func (c *WindowsContainerCommunicator) Upload(dst string, src io.Reader, fi *os. // Copy the file into place by copying the temporary file we put // into the shared folder into the proper location in the container cmd := &packersdk.RemoteCmd{ - Command: fmt.Sprintf("Copy-Item -Path %s/%s -Destination %s", c.ContainerDir, - filepath.Base(tempfile.Name()), dst), + Command: fmt.Sprintf("Copy-Item -LiteralPath %s -Destination %s -Force", powerShellSingleQuote(filepath.Join(c.ContainerDir, filepath.Base(tempfile.Name()))), powerShellSingleQuote(dst)), } if err := c.Start(ctx, cmd); err != nil { return err @@ -183,8 +178,7 @@ func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude // Make the directory, then copy into it cmd := &packersdk.RemoteCmd{ - Command: fmt.Sprintf("Copy-Item %s -Destination %s -Recurse", - containerSrc, containerDst), + Command: fmt.Sprintf("Copy-Item -LiteralPath %s -Destination %s -Recurse", powerShellSingleQuote(containerSrc), powerShellSingleQuote(containerDst)), } if err := c.Start(ctx, cmd); err != nil { return err