github.com/YousefHaggyHeroku/pack@v1.5.5/internal/build/container_ops.go (about)

     1  package build
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"runtime"
    11  
    12  	"github.com/BurntSushi/toml"
    13  	"github.com/docker/docker/api/types"
    14  	dcontainer "github.com/docker/docker/api/types/container"
    15  	"github.com/docker/docker/client"
    16  	"github.com/pkg/errors"
    17  
    18  	"github.com/YousefHaggyHeroku/pack/internal/paths"
    19  
    20  	"github.com/YousefHaggyHeroku/pack/internal/archive"
    21  	"github.com/YousefHaggyHeroku/pack/internal/builder"
    22  	"github.com/YousefHaggyHeroku/pack/internal/container"
    23  )
    24  
    25  type ContainerOperation func(ctrClient client.CommonAPIClient, ctx context.Context, containerID string, stdout, stderr io.Writer) error
    26  
    27  // CopyDir copies a local directory (src) to the destination on the container while filtering files and changing it's UID/GID.
    28  func CopyDir(src, dst string, uid, gid int, os string, fileFilter func(string) bool) ContainerOperation {
    29  	return func(ctrClient client.CommonAPIClient, ctx context.Context, containerID string, stdout, stderr io.Writer) error {
    30  		tarPath := dst
    31  		if os == "windows" {
    32  			tarPath = paths.WindowsToSlash(dst)
    33  		}
    34  
    35  		reader, err := createReader(src, tarPath, uid, gid, fileFilter)
    36  		if err != nil {
    37  			return errors.Wrapf(err, "create tar archive from '%s'", src)
    38  		}
    39  		defer reader.Close()
    40  
    41  		if os == "windows" {
    42  			return copyDirWindows(ctx, ctrClient, containerID, reader, dst, stdout, stderr)
    43  		}
    44  		return copyDir(ctx, ctrClient, containerID, reader)
    45  	}
    46  }
    47  
    48  func copyDir(ctx context.Context, ctrClient client.CommonAPIClient, containerID string, appReader io.Reader) error {
    49  	var clientErr, err error
    50  
    51  	doneChan := make(chan interface{})
    52  	pr, pw := io.Pipe()
    53  	go func() {
    54  		clientErr = ctrClient.CopyToContainer(ctx, containerID, "/", pr, types.CopyToContainerOptions{})
    55  		close(doneChan)
    56  	}()
    57  	func() {
    58  		defer pw.Close()
    59  		_, err = io.Copy(pw, appReader)
    60  	}()
    61  
    62  	<-doneChan
    63  	if err == nil {
    64  		err = clientErr
    65  	}
    66  
    67  	return err
    68  }
    69  
    70  // copyDirWindows provides an alternate, Windows container-specific implementation of copyDir.
    71  // This implementation is needed because copying directly to a mounted volume is currently buggy
    72  // for Windows containers and does not work. Instead, we perform the copy from inside a container
    73  // using xcopy.
    74  // See: https://github.com/moby/moby/issues/40771
    75  func copyDirWindows(ctx context.Context, ctrClient client.CommonAPIClient, containerID string, reader io.Reader, dst string, stdout, stderr io.Writer) error {
    76  	info, err := ctrClient.ContainerInspect(ctx, containerID)
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	baseName := paths.WindowsBasename(dst)
    82  
    83  	mnt, err := findMount(info, dst)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	ctr, err := ctrClient.ContainerCreate(ctx,
    89  		&dcontainer.Config{
    90  			Image: info.Image,
    91  			Cmd: []string{
    92  				"cmd",
    93  				"/c",
    94  
    95  				//xcopy args
    96  				// e - recursively create subdirectories
    97  				// h - copy hidden and system files
    98  				// b - copy symlinks, do not dereference
    99  				// x - copy attributes
   100  				// y - suppress prompting
   101  				fmt.Sprintf(`xcopy c:\windows\%s %s /e /h /b /x /y`, baseName, dst),
   102  			},
   103  			WorkingDir: "/",
   104  			User:       windowsContainerAdmin,
   105  		},
   106  		&dcontainer.HostConfig{
   107  			Binds:     []string{fmt.Sprintf("%s:%s", mnt.Name, mnt.Destination)},
   108  			Isolation: dcontainer.IsolationProcess,
   109  		},
   110  		nil, "",
   111  	)
   112  	if err != nil {
   113  		return errors.Wrapf(err, "creating prep container")
   114  	}
   115  	defer ctrClient.ContainerRemove(context.Background(), ctr.ID, types.ContainerRemoveOptions{Force: true})
   116  
   117  	err = ctrClient.CopyToContainer(ctx, ctr.ID, "/windows", reader, types.CopyToContainerOptions{})
   118  	if err != nil {
   119  		return errors.Wrap(err, "copy app to container")
   120  	}
   121  
   122  	return container.Run(
   123  		ctx,
   124  		ctrClient,
   125  		ctr.ID,
   126  		ioutil.Discard, // Suppress xcopy output
   127  		stderr,
   128  	)
   129  }
   130  
   131  func findMount(info types.ContainerJSON, dst string) (types.MountPoint, error) {
   132  	for _, m := range info.Mounts {
   133  		if m.Destination == dst {
   134  			return m, nil
   135  		}
   136  	}
   137  	return types.MountPoint{}, fmt.Errorf("no matching mount found for %s", dst)
   138  }
   139  
   140  // WriteStackToml writes a `stack.toml` based on the StackMetadata provided to the destination path.
   141  func WriteStackToml(dstPath string, stack builder.StackMetadata, os string) ContainerOperation {
   142  	return func(ctrClient client.CommonAPIClient, ctx context.Context, containerID string, stdout, stderr io.Writer) error {
   143  		buf := &bytes.Buffer{}
   144  		err := toml.NewEncoder(buf).Encode(stack)
   145  		if err != nil {
   146  			return errors.Wrap(err, "marshaling stack metadata")
   147  		}
   148  
   149  		tarBuilder := archive.TarBuilder{}
   150  
   151  		tarPath := dstPath
   152  		if os == "windows" {
   153  			tarPath = paths.WindowsToSlash(dstPath)
   154  		}
   155  
   156  		tarBuilder.AddFile(tarPath, 0755, archive.NormalizedDateTime, buf.Bytes())
   157  		reader := tarBuilder.Reader(archive.DefaultTarWriterFactory())
   158  		defer reader.Close()
   159  
   160  		if os == "windows" {
   161  			dirName := paths.WindowsDir(dstPath)
   162  			return copyDirWindows(ctx, ctrClient, containerID, reader, dirName, stdout, stderr)
   163  		}
   164  
   165  		return ctrClient.CopyToContainer(ctx, containerID, "/", reader, types.CopyToContainerOptions{})
   166  	}
   167  }
   168  
   169  func createReader(src, dst string, uid, gid int, fileFilter func(string) bool) (io.ReadCloser, error) {
   170  	fi, err := os.Stat(src)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	if fi.IsDir() {
   176  		var mode int64 = -1
   177  		if runtime.GOOS == "windows" {
   178  			mode = 0777
   179  		}
   180  
   181  		return archive.ReadDirAsTar(src, dst, uid, gid, mode, false, fileFilter), nil
   182  	}
   183  
   184  	return archive.ReadZipAsTar(src, dst, uid, gid, -1, false, fileFilter), nil
   185  }
   186  
   187  //EnsureVolumeAccess grants full access permissions to volumes for UID/GID-based user
   188  //When UID/GID are 0 it grants explicit full access to BUILTIN\Administrators and any other UID/GID grants full access to BUILTIN\Users
   189  //Changing permissions on volumes through stopped containers does not work on Docker for Windows so we start the container and make change using icacls
   190  //See: https://github.com/moby/moby/issues/40771
   191  func EnsureVolumeAccess(uid, gid int, os string, volumeNames ...string) ContainerOperation {
   192  	return func(ctrClient client.CommonAPIClient, ctx context.Context, containerID string, stdout, stderr io.Writer) error {
   193  		if os != "windows" {
   194  			return nil
   195  		}
   196  
   197  		containerInfo, err := ctrClient.ContainerInspect(ctx, containerID)
   198  		if err != nil {
   199  			return err
   200  		}
   201  
   202  		cmd := ""
   203  		binds := []string{}
   204  		for i, volumeName := range volumeNames {
   205  			containerPath := fmt.Sprintf("c:/volume-mnt-%d", i)
   206  			binds = append(binds, fmt.Sprintf("%s:%s", volumeName, containerPath))
   207  
   208  			if cmd != "" {
   209  				cmd += "&&"
   210  			}
   211  
   212  			//icacls args
   213  			// /grant - add new permissions instead of replacing
   214  			// (OI) - object inherit
   215  			// (CI) - container inherit
   216  			// F - full access
   217  			// /t - recursively apply
   218  			// /l - perform on a symbolic link itself versus its target
   219  			// /q - suppress success messages
   220  			cmd += fmt.Sprintf(`icacls %s /grant *%s:(OI)(CI)F /t /l /q`, containerPath, paths.WindowsPathSID(uid, gid))
   221  		}
   222  
   223  		ctr, err := ctrClient.ContainerCreate(ctx,
   224  			&dcontainer.Config{
   225  				Image:      containerInfo.Image,
   226  				Cmd:        []string{"cmd", "/c", cmd},
   227  				WorkingDir: "/",
   228  				User:       windowsContainerAdmin,
   229  			},
   230  			&dcontainer.HostConfig{
   231  				Binds:     binds,
   232  				Isolation: dcontainer.IsolationProcess,
   233  			},
   234  			nil, "",
   235  		)
   236  		if err != nil {
   237  			return err
   238  		}
   239  		defer ctrClient.ContainerRemove(context.Background(), ctr.ID, types.ContainerRemoveOptions{Force: true})
   240  
   241  		return container.Run(
   242  			ctx,
   243  			ctrClient,
   244  			ctr.ID,
   245  			ioutil.Discard, // Suppress icacls output
   246  			stderr,
   247  		)
   248  	}
   249  }