package build import ( "fmt" "io" "os" "path/filepath" "syscall" "werkdock/internal/engine" ) // CopyBackend builds in a full copy of the base rootfs, bound // read-write into every RUN step (RFC 0006). It needs nothing from // bwrap that 0.8 does not have: a writable root bind works in an // unprivileged user namespace (measured), and no mount is made inside // the sandbox, which could not make one anyway (CapEff is 0). type CopyBackend struct{} func (CopyBackend) Name() string { return "copy" } // CachesSteps is false: without a union mount a cached step's result is // a whole tree, so caching four steps of a 2 GiB image would keep four // 2 GiB copies. The per-step cache waits for the overlay backend. func (CopyBackend) CachesSteps() bool { return false } // Open copies base to dir/rootfs. func (CopyBackend) Open(base, dir string) (Tree, error) { rootfs := filepath.Join(dir, "rootfs") if err := copyTree(base, rootfs); err != nil { return nil, fmt.Errorf("copying the base image: %w", err) } return ©TreeHandle{rootfs: rootfs}, nil } type copyTreeHandle struct { rootfs string } // Step hands out the same writable tree for every step: the copy // backend has one tree, and a step's writes are simply in it. func (t *copyTreeHandle) Step(int) (Step, error) { if t.rootfs == "" { return nil, fmt.Errorf("the work tree has been committed") } return copyStep{rootfs: t.rootfs}, nil } // Commit hands the tree over: it is already the flat rootfs the store // wants, so the commit the caller then performs is a rename. func (t *copyTreeHandle) Commit() (string, error) { if t.rootfs == "" { return "", fmt.Errorf("the work tree has already been committed") } rootfs := t.rootfs t.rootfs = "" return rootfs, nil } // Close has nothing to release: the tree lives in the caller's staging // directory, which the caller removes when the build failed. func (t *copyTreeHandle) Close() error { return nil } type copyStep struct { rootfs string } func (s copyStep) Root(spec *engine.RunSpec) { spec.RootFS = s.rootfs spec.WritableRoot = true } func (s copyStep) Writable() string { return s.rootfs } // copyTree duplicates a directory tree with real copies of the files. // // It is deliberately NOT store.copyTree, which hardlinks so that a // second tag of one image costs no data — there both copies are // read-only. Here a RUN step writes into the tree in place, and // through a hardlink that write would land in the base image as well. // A build needs a real copy; that copy is the cost of the copy backend // (RFC 0006, Costs). // // Hardlinks *inside* the base are reproduced inside the copy (busybox // links hundreds of names to one binary), which costs nothing and // keeps the tree's own structure: those links point at the copy, never // at the base. func copyTree(src, dst string) error { linked := map[uint64]string{} return filepath.Walk(src, func(p string, fi os.FileInfo, err error) error { if err != nil { return err } rel, relErr := filepath.Rel(src, p) if relErr != nil { return relErr } target := filepath.Join(dst, rel) switch { case fi.IsDir(): if err := os.MkdirAll(target, 0o700); err != nil { return err } // Owner-writable, whatever the image says: the steps have // to be able to add to every directory, and the finished // tree is bound read-only into every run anyway — as // store.applyLayer does for the same reason. return os.Chmod(target, fi.Mode().Perm()|0o700) case fi.Mode()&os.ModeSymlink != 0: link, linkErr := os.Readlink(p) if linkErr != nil { return linkErr } return os.Symlink(link, target) case fi.Mode().IsRegular(): if ino, ok := hardlinkKey(fi); ok { if first, seen := linked[ino]; seen { return os.Link(first, target) } linked[ino] = target } return copyFile(p, target, fi.Mode().Perm()) default: // Sockets, fifos, device nodes: not image content. /dev // comes from the host (RFC 0002) and store.applyLayer // skips them when unpacking, so a base image holds none. return nil } }) } // hardlinkKey is the inode of a file that has more than one name, so // that the second name becomes a link inside the copy. func hardlinkKey(fi os.FileInfo) (uint64, bool) { st, ok := fi.Sys().(*syscall.Stat_t) if !ok || st.Nlink < 2 { return 0, false } return st.Ino, true } func copyFile(src, dst string, mode os.FileMode) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) if err != nil { return err } if _, err := io.Copy(out, in); err != nil { out.Close() return err } if err := out.Close(); err != nil { return err } // O_CREATE applies the umask to mode, so set it explicitly, as the // layer unpacking does. return os.Chmod(dst, mode) }