diff --git a/CHANGELOG.md b/CHANGELOG.md index 71ab42ab..d150f327 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### IMPROVEMENTS: + +* Add `windows_create_parent_dirs` option to automatically create destination parent directories on uploads to Windows containers by @wbpascal in https://github.com/hashicorp/packer-plugin-docker/pull/230 + ## 1.1.3 (June 9, 2026) ### BUG FIXES: 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..d8e09e1c 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,28 @@ 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: 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 + } + + 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 +71,21 @@ 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), + Command: fmt.Sprintf("Copy-Item -LiteralPath %s -Destination %s -Force", powerShellSingleQuote(filepath.Join(c.ContainerDir, filepath.Base(tempfile.Name()))), powerShellSingleQuote(dst)), } - ctx := context.TODO() if err := c.Start(ctx, cmd); err != nil { return err } @@ -135,12 +166,20 @@ 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), + Command: fmt.Sprintf("Copy-Item -LiteralPath %s -Destination %s -Recurse", powerShellSingleQuote(containerSrc), powerShellSingleQuote(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 } ]