Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions atomfs/molecule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package atomfs

import (
"os"
"path"
"path/filepath"
"strings"

ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/project-stacker/stacker/mount"
"github.com/project-stacker/stacker/squashfs"
"golang.org/x/sys/unix"
)

type Molecule struct {
// Atoms is the list of atoms in this Molecule. The first element in
// this list is the top most layer in the overlayfs.
Atoms []ispec.Descriptor

config MountOCIOpts
}

// mountUnderlyingAtoms mounts all the underlying atoms at
// config.MountedAtomsPath().
func (m Molecule) mountUnderlyingAtoms() error {
for _, a := range m.Atoms {
target := m.config.MountedAtomsPath(a.Digest.Encoded())

mountpoint, err := mount.IsMountpoint(target)
if err != nil {
return err
}
if mountpoint {
// FIXME: this is unsafe. if someone mounted a
// filesystem here, we just use it, without ensuring
// the contents match the dm-verity stuff. in the
// future, we should:
//
// 1. go backwards from the mountpoint to the block dev
// 2. ask the kernel what its root hash of the block
// dev is, and compare that to the annotation we have
//
// but for now let's just re-use to get off the ground.
continue
}

if err := os.MkdirAll(target, 0755); err != nil {
return err
}

rootHash := a.Annotations[squashfs.VerityRootHashAnnotation]

err = squashfs.Mount(m.config.AtomsPath(a.Digest.Encoded()), target, rootHash)
if err != nil {
return errors.Wrapf(err, "couldn't mount")
}
}

return nil
}

// overlayArgs returns all of the mount options to pass to the kernel to
// actually mount this molecule.
func (m Molecule) overlayArgs(dest string) (string, error) {
dirs := []string{}
for _, a := range m.Atoms {
target := m.config.MountedAtomsPath(a.Digest.Encoded())
dirs = append(dirs, target)
}

// overlay doesn't work with one lowerdir. so we do a hack here: we
// just create an empty directory called "workaround" in the mounts
// directory, and add that to the dir list if it's of length one.
if len(dirs) == 1 {
workaround := m.config.MountedAtomsPath("workaround")
if err := os.MkdirAll(workaround, 0755); err != nil {
return "", errors.Wrapf(err, "couldn't make workaround dir")
}

dirs = append(dirs, workaround)
}

// Note that in overlayfs, the first thing is the top most layer in the
// overlay.
mntOpts := "index=off,lowerdir=" + strings.Join(dirs, ":")
return mntOpts, nil
}

// device mapper has no namespacing. if two different binaries invoke this code
// (for example, the stacker test suite), we want to be sure we don't both
// create or delete devices out from the other one when they have detected the
// device exists. so try to cooperate via this lock.
const advisoryLockPath = "/dev/shm/atomfs-lock"

func (m Molecule) Mount(dest string) error {
lockfile, err := os.Create(advisoryLockPath)
if err != nil {
return errors.WithStack(err)
}
defer lockfile.Close()

err = unix.Flock(int(lockfile.Fd()), unix.LOCK_EX)
if err != nil {
return errors.WithStack(err)
}

mntOpts, err := m.overlayArgs(dest)
if err != nil {
return err
}

// The kernel doesn't allow mount options longer than 4096 chars, so
// let's give a nicer error than -EINVAL here.
if len(mntOpts) > 4096 {
return errors.Errorf("too many lower dirs; must have fewer than 4096 chars")
}

err = m.mountUnderlyingAtoms()
if err != nil {
return err
}

// now, do the actual overlay mount
err = unix.Mount("overlay", dest, "overlay", 0, mntOpts)
return errors.Wrapf(err, "couldn't do overlay mount to %s, opts: %s", dest, mntOpts)
}

func Umount(dest string) error {
var err error
dest, err = filepath.Abs(dest)
if err != nil {
return errors.Wrapf(err, "couldn't create abs path for %v", dest)
}

lockfile, err := os.Create(advisoryLockPath)
if err != nil {
return errors.WithStack(err)
}
defer lockfile.Close()

err = unix.Flock(int(lockfile.Fd()), unix.LOCK_EX)
if err != nil {
return errors.WithStack(err)
}

mounts, err := mount.ParseMounts("/proc/self/mountinfo")
if err != nil {
return err
}

underlyingAtoms := []string{}
for _, m := range mounts {
if m.FSType != "overlay" {
continue
}

if m.Target != dest {
continue
}

underlyingAtoms, err = m.GetOverlayDirs()
if err != nil {
return err
}
break
}

if len(underlyingAtoms) == 0 {
return errors.Errorf("%s is not an atomfs mountpoint", dest)
}

if err := unix.Unmount(dest, 0); err != nil {
return err
}

// now, "refcount" the remaining atoms and see if any of ours are
// unused
usedAtoms := map[string]int{}

mounts, err = mount.ParseMounts("/proc/self/mountinfo")
if err != nil {
return err
}

for _, m := range mounts {
if m.FSType != "overlay" {
continue
}

dirs, err := m.GetOverlayDirs()
if err != nil {
return err
}
for _, d := range dirs {
usedAtoms[d]++
}
}

// If any of the atoms underlying the target mountpoint are now unused,
// let's unmount them too.
for _, a := range underlyingAtoms {
_, used := usedAtoms[a]
if used {
continue
}
/* TODO: some kind of logging
if !used {
log.Warnf("unused atom %s was part of this molecule?")
continue
}
*/

// the workaround dir isn't really a mountpoint, so don't unmount it
if path.Base(a) == "workaround" {
continue
}

err = squashfs.Umount(a)
if err != nil {
return err
}
}

return nil
}
58 changes: 58 additions & 0 deletions atomfs/oci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package atomfs

import (
"path"

ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/umoci"
stackeroci "github.com/project-stacker/stacker/oci"
)

type MountOCIOpts struct {
OCIDir string
MetadataPath string
Tag string
Target string
}

func (c MountOCIOpts) AtomsPath(parts ...string) string {
atoms := path.Join(c.OCIDir, "blobs", "sha256")
return path.Join(append([]string{atoms}, parts...)...)
}

func (c MountOCIOpts) MountedAtomsPath(parts ...string) string {
mounts := path.Join(c.MetadataPath, "mounts")
return path.Join(append([]string{mounts}, parts...)...)
}

func BuildMoleculeFromOCI(opts MountOCIOpts) (Molecule, error) {
oci, err := umoci.OpenLayout(opts.OCIDir)
if err != nil {
return Molecule{}, err
}
defer oci.Close()

man, err := stackeroci.LookupManifest(oci, opts.Tag)
if err != nil {
return Molecule{}, err
}

atoms := []ispec.Descriptor{}
atoms = append(atoms, man.Layers...)

// The OCI spec says that the first layer should be the bottom most
// layer. In overlay it's the top most layer. Since the atomfs codebase
// is mostly a wrapper around overlayfs, let's keep things in our db in
// the same order that overlay expects them, i.e. the first layer is
// the top most. That means we need to reverse the order in which the
// atoms were inserted, because they were backwards.
//
// It's also terrible that golang doesn't have a reverse function, but
// that's a discussion for a different block comment.
for i := len(atoms)/2 - 1; i >= 0; i-- {
opp := len(atoms) - 1 - i
atoms[i], atoms[opp] = atoms[opp], atoms[i]
}

return Molecule{Atoms: atoms, config: opts}, nil
}
54 changes: 54 additions & 0 deletions cmd/internal_go.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"fmt"
"io/ioutil"
"os"
"path"
"runtime"
"strings"

"github.com/pkg/errors"
"github.com/project-stacker/stacker/atomfs"
"github.com/project-stacker/stacker/lib"
"github.com/project-stacker/stacker/log"
"github.com/project-stacker/stacker/overlay"
Expand Down Expand Up @@ -39,6 +41,19 @@ var internalGoCmd = cli.Command{
Name: "copy",
Action: doImageCopy,
},
cli.Command{
Name: "atomfs",
Subcommands: []cli.Command{
cli.Command{
Name: "mount",
Action: doAtomfsMount,
},
cli.Command{
Name: "umount",
Action: doAtomfsUmount,
},
},
},
},
Before: doBeforeUmociSubcommand,
}
Expand Down Expand Up @@ -124,3 +139,42 @@ func doCheckAAProfile(ctx *cli.Context) error {

return nil
}

func doAtomfsMount(ctx *cli.Context) error {
if len(ctx.Args()) != 2 {
return errors.Errorf("wrong number of args for mount")
}

tag := ctx.Args()[0]
mountpoint := ctx.Args()[1]

wd, err := os.Getwd()
if err != nil {
return errors.WithStack(err)
}

opts := atomfs.MountOCIOpts{
OCIDir: config.OCIDir,
MetadataPath: path.Join(wd, "atomfs-metadata"),
Tag: tag,
Target: mountpoint,
}

mol, err := atomfs.BuildMoleculeFromOCI(opts)
if err != nil {
return err
}

log.Debugf("about to mount %v", mol)

return mol.Mount(mountpoint)
}

func doAtomfsUmount(ctx *cli.Context) error {
if len(ctx.Args()) != 1 {
return errors.Errorf("wrong number of args for umount")
}

mountpoint := ctx.Args()[0]
return atomfs.Umount(mountpoint)
}
15 changes: 14 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ func stackerResult(err error) {
}
}

func shouldSkipInternalUserns(ctx *cli.Context) bool {
args := ctx.Args()
if len(args) >= 1 && args[0] == "unpriv-setup" {
return true
}

if len(args) >= 2 && args[0] == "internal-go" && args[1] == "atomfs" {
return true
}

return false
}

func main() {
sigquits := make(chan os.Signal, 1)
go func() {
Expand Down Expand Up @@ -265,7 +278,7 @@ func main() {
stackerlog.FilterNonStackerLogs(handler, logLevel)
stackerlog.Debugf("stacker version %s", version)

if !ctx.Bool("internal-userns") && len(ctx.Args()) >= 1 && ctx.Args()[0] != "unpriv-setup" {
if !ctx.Bool("internal-userns") && !shouldSkipInternalUserns(ctx) {
binary, err := os.Readlink("/proc/self/exe")
if err != nil {
return err
Expand Down
Loading