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 }