diff --git a/bundle/bundle.go b/bundle/bundle.go index 4838ac9..488aa24 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -71,7 +71,7 @@ func (b *Bundle) loadSpec() (r rspecs.Spec, err error) { func (b *Bundle) Image() *digest.Digest { if imgIdb, err := ioutil.ReadFile(b.imageFile()); err == nil { d, err := digest.Parse(strings.Trim(string(imgIdb), " \n")) - if err == nil { + if err == nil && d.Validate() == nil { return &d } } @@ -137,25 +137,23 @@ func OpenLockedBundle(bundle Bundle) (*LockedBundle, error) { return nil, err } if err := bundle.resetExpiryTime(); err != nil { + lck.Unlock() return nil, errors.Wrap(err, "lock bundle") } return &LockedBundle{bundle, nil, nil, lck}, nil } -func CreateLockedBundle(dir string, spec SpecGenerator, image BundleImage, update bool) (r *LockedBundle, err error) { +func CreateLockedBundle(dir string, update bool) (r *LockedBundle, err error) { defer exterrors.Wrapd(&err, "create bundle") // Create bundle - id := "" - if id == "" { - id = filepath.Base(dir) - } + id := filepath.Base(dir) bundle := Bundle{id, dir, time.Now()} // Lock bundle lck, err := lockBundle(&bundle) if err != nil { - return nil, err + return } r = &LockedBundle{bundle, nil, nil, lck} defer func() { @@ -167,16 +165,13 @@ func CreateLockedBundle(dir string, spec SpecGenerator, image BundleImage, updat // Create or update bundle err = os.Mkdir(dir, 0770) exists := err != nil && os.IsExist(err) - updateRootfs := true if exists { if !update { - return r, errors.Errorf("bundle %q directory already exists", id) + return r, errors.Errorf("bundle %q already exists", id) } - lastImageId := bundle.Image() - rootfs := filepath.Join(dir, "rootfs") - if _, e := os.Stat(rootfs); e == nil && (lastImageId == nil && image == nil || lastImageId != nil && *lastImageId == image.ID()) { - updateRootfs = false + if err = bundle.resetExpiryTime(); err != nil { + return } } else { if err != nil { @@ -191,17 +186,9 @@ func CreateLockedBundle(dir string, spec SpecGenerator, image BundleImage, updat } } - // Update rootfs if not exists or image changed - if updateRootfs { - if err = r.UpdateRootfs(image); err != nil { - return - } - } - - // TODO: resolve user/group names here - // Write spec - if err = r.SetSpec(spec); err != nil { - return + r.spec = &rspecs.Spec{ + Root: &rspecs.Root{Path: "rootfs"}, + Annotations: map[string]string{ANNOTATION_BUNDLE_ID: id}, } return @@ -235,11 +222,30 @@ func (b *LockedBundle) Spec() (*rspecs.Spec, error) { return b.spec, nil } +// Returns the bundle's image ID +func (b *LockedBundle) Image() *digest.Digest { + if b.image == nil { + b.image = b.bundle.Image() + } + return b.image +} + +func (b *LockedBundle) Delete() (err error) { + err = DeleteDirSafely(b.Dir()) + err = exterrors.Append(err, b.Close()) + return +} + +// Updates the rootfs if the image changed func (b *LockedBundle) UpdateRootfs(image BundleImage) (err error) { var ( - rootfs = filepath.Join(b.Dir(), "rootfs") - imgId *digest.Digest + rootfs = filepath.Join(b.Dir(), "rootfs") + imgId *digest.Digest + lastImgId = b.Image() ) + if _, e := os.Stat(rootfs); e == nil && (lastImgId == nil && image == nil || lastImgId != nil && *lastImgId == image.ID()) { + return // don't update since the bundle is already based on the provided image + } if image != nil { id := image.ID() imgId = &id @@ -253,24 +259,32 @@ func (b *LockedBundle) UpdateRootfs(image BundleImage) (err error) { return b.SetParentImageId(imgId) } -func (b *LockedBundle) SetSpec(specgen SpecGenerator) (err error) { - spec, err := specgen.Spec(filepath.Join(b.Dir(), "rootfs")) +func (b *LockedBundle) SetParentImageId(imageID *digest.Digest) (err error) { + if imageID == nil { + if e := os.Remove(b.bundle.imageFile()); e != nil && !os.IsNotExist(e) { + err = errors.New(e.Error()) + } + } else { + _, err = atomic.WriteFile(b.bundle.imageFile(), bytes.NewBufferString((*imageID).String())) + } + if err == nil { + b.image = imageID + } else { + err = errors.Wrapf(err, "set bundle's (%s) parent image id", b.ID()) + } + return +} + +func (b *LockedBundle) SetSpec(spec *rspecs.Spec) (err error) { if err == nil { err = createVolumeDirectories(spec, b.Dir()) } if err != nil { return errors.Wrap(err, "set bundle spec") } - if spec.Root != nil { - spec.Root.Path = "rootfs" - } - if spec.Annotations == nil { - spec.Annotations = map[string]string{} - } - spec.Annotations[ANNOTATION_BUNDLE_ID] = b.ID() confFile := filepath.Join(b.Dir(), "config.json") if _, err = atomic.WriteJson(confFile, spec); err != nil { - err = errors.Wrapf(err, "write bundle %q spec", b.ID()) + return errors.Wrapf(err, "write bundle %q spec", b.ID()) } b.spec = spec return @@ -306,36 +320,6 @@ func createVolumeDirectories(spec *rspecs.Spec, dir string) (err error) { return } -// Reads image ID from cached spec -func (b *LockedBundle) Image() *digest.Digest { - if b.image == nil { - b.image = b.bundle.Image() - } - return b.image -} - -func (b *LockedBundle) SetParentImageId(imageID *digest.Digest) (err error) { - if imageID == nil { - if e := os.Remove(b.bundle.imageFile()); e != nil && !os.IsNotExist(e) { - err = errors.New(e.Error()) - } - } else { - _, err = atomic.WriteFile(b.bundle.imageFile(), bytes.NewBufferString((*imageID).String())) - } - if err == nil { - b.image = imageID - } else { - err = errors.Wrap(err, "set bundle's parent image id") - } - return -} - -func (b *LockedBundle) Delete() (err error) { - err = DeleteDirSafely(b.Dir()) - err = exterrors.Append(err, b.Close()) - return -} - func lockBundle(bundle *Bundle) (l *lock.Lockfile, err error) { // TODO: use tmpfs for lock file if l, err = lock.LockFile(filepath.Clean(bundle.dir) + ".lock"); err == nil { diff --git a/bundle/bundlebuilder.go b/bundle/bundlebuilder.go index 75e6eaa..c20ef4a 100644 --- a/bundle/bundlebuilder.go +++ b/bundle/bundlebuilder.go @@ -1,12 +1,9 @@ package bundle import ( - "encoding/base32" - "strings" + "path/filepath" "github.com/mgoltzsche/ctnr/pkg/generate" - "github.com/pkg/errors" - "github.com/satori/go.uuid" ) type BundleBuilder struct { @@ -16,32 +13,9 @@ type BundleBuilder struct { } func Builder(id string) *BundleBuilder { - spec := generate.NewSpecBuilder() - spec.AddAnnotation(ANNOTATION_BUNDLE_ID, id) - spec.SetRootPath("rootfs") - return FromSpec(&spec) -} - -func BuilderFromImage(id string, image BundleImage) (b *BundleBuilder, err error) { - spec := generate.NewSpecBuilder() - spec.SetRootPath("rootfs") - conf := image.Config() - spec.ApplyImage(conf) - spec.AddAnnotation(ANNOTATION_BUNDLE_ID, id) - b = FromSpec(&spec) - b.image = image - return b, errors.Wrap(err, "bundle build from image") -} - -func FromSpec(spec *generate.SpecBuilder) *BundleBuilder { - id := "" - if s := spec.Generator.Spec(); s != nil && s.Annotations != nil { - id = s.Annotations[ANNOTATION_BUNDLE_ID] - } - if id == "" { - id = generateId() - } - b := &BundleBuilder{"", spec, nil} + specgen := generate.NewSpecBuilder() + specgen.SetRootPath("rootfs") + b := &BundleBuilder{"", &specgen, nil} b.SetID(id) return b } @@ -59,12 +33,24 @@ func (b *BundleBuilder) GetID() string { return b.id } -func (b *BundleBuilder) Build(dir string, update bool) (*LockedBundle, error) { - // Create bundle directory - bundle, err := CreateLockedBundle(dir, b, b.image, update) - return bundle, errors.Wrap(err, "build bundle") +func (b *BundleBuilder) SetImage(image BundleImage) { + b.ApplyImage(image.Config()) + b.image = image } -func generateId() string { - return strings.ToLower(strings.TrimRight(base32.StdEncoding.EncodeToString(uuid.NewV4().Bytes()), "=")) +func (b *BundleBuilder) Build(bundle *LockedBundle) (err error) { + // Prepare rootfs + if err = bundle.UpdateRootfs(b.image); err != nil { + return + } + + // Resolve user/group names + rootfs := filepath.Join(bundle.Dir(), b.Generator.Spec().Root.Path) + spec, err := b.Spec(rootfs) + if err != nil { + return + } + + // Apply spec + return bundle.SetSpec(spec) } diff --git a/bundle/store.go b/bundle/store.go index 2def353..0ad77e1 100644 --- a/bundle/store.go +++ b/bundle/store.go @@ -5,7 +5,7 @@ import ( ) type BundleStore interface { - CreateBundle(builder *BundleBuilder, update bool) (*LockedBundle, error) + CreateBundle(id string, update bool) (*LockedBundle, error) Bundle(id string) (Bundle, error) Bundles() ([]Bundle, error) BundleGC(ttl time.Duration) ([]Bundle, error) diff --git a/bundle/store/bundlestore.go b/bundle/store/bundlestore.go index 816514b..27221fa 100644 --- a/bundle/store/bundlestore.go +++ b/bundle/store/bundlestore.go @@ -47,8 +47,18 @@ func (s *BundleStore) Bundle(id string) (r bundle.Bundle, err error) { return bundle.NewBundle(filepath.Join(s.dir, id)) } -func (s *BundleStore) CreateBundle(builder *bundle.BundleBuilder, update bool) (b *bundle.LockedBundle, err error) { - return builder.Build(filepath.Join(s.dir, builder.GetID()), update) +func (s *BundleStore) CreateBundle(id string, update bool) (b *bundle.LockedBundle, err error) { + dir := filepath.Join(s.dir, id) + if id == "" { + if err = os.MkdirAll(s.dir, 0770); err != nil { + return nil, errors.Wrap(err, "create bundle") + } + if dir, err = ioutil.TempDir(s.dir, ""); err != nil { + return nil, errors.Wrap(err, "create bundle") + } + update = true + } + return bundle.CreateLockedBundle(dir, update) } // Deletes all bundles that have not been used longer than the given TTL. diff --git a/cmd/common.go b/cmd/common.go index 08d82b3..e902ad9 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -255,32 +255,34 @@ func createRuntimeBundle(service *model.Service, res model.ResourceResolver) (b bundleId = "" } - // Load image and bundle builder - var builder *bundle.BundleBuilder - if service.Image == "" { - builder = bundle.Builder(bundleId) + // Create bundle + if bundleDir != "" { + b, err = bundle.CreateLockedBundle(bundleDir, service.BundleUpdate) } else { + b, err = store.CreateBundle(bundleId, service.BundleUpdate) + } + defer func() { + if err != nil { + b.Delete() + } + }() + + // Apply image + builder := bundle.Builder(b.ID()) + if service.Image != "" { var img image.Image if img, err = image.GetImage(istore, service.Image); err != nil { - return - } - if builder, err = bundle.BuilderFromImage(bundleId, image.NewUnpackableImage(&img, istore)); err != nil { - return + return nil, err } + builder.SetImage(image.NewUnpackableImage(&img, istore)) } - // Generate config.json - if err = oci.ToSpec(service, res, flagRootless, flagPRootPath, builder.SpecBuilder); err != nil { - return + // Apply config.json + if err = oci.ToSpec(service, res, flagRootless, flagPRootPath, builder); err != nil { + return nil, err } - // Create bundle - if bundleDir != "" { - b, err = builder.Build(bundleDir, service.BundleUpdate) - } else { - b, err = store.CreateBundle(builder, service.BundleUpdate) - } - return + return b, builder.Build(b) } func isFile(file string) bool { diff --git a/image/builder/imagebuilder.go b/image/builder/imagebuilder.go index 4096cfb..e4551b4 100644 --- a/image/builder/imagebuilder.go +++ b/image/builder/imagebuilder.go @@ -188,29 +188,30 @@ func (b *ImageBuilder) initBundle() (err error) { return } - // Derive bundle from image - var bb *bundle.BundleBuilder - if b.image == nil { - bb = bundle.Builder("") - } else if bb, err = bundle.BuilderFromImage("", image.NewUnpackableImage(b.image, b.images)); err != nil { - return errors.Wrap(err, "image builder") + // Create locked bundle + newBundle, err := b.bundles.CreateBundle("", false) + if err != nil { + return + } + b.bundle = newBundle + b.lockedBundles = append(b.lockedBundles, newBundle) + + // Derive bundle spec from image + builder := bundle.Builder(newBundle.ID()) + if b.image != nil { + builder.SetImage(image.NewUnpackableImage(b.image, b.images)) } if b.rootless { - bb.ToRootless() + builder.ToRootless() } if b.proot != "" { - bb.SetPRootPath(b.proot) + builder.SetPRootPath(b.proot) } // TODO: use separate default network when not in rootless mode - bb.UseHostNetwork() - bb.SetProcessTerminal(false) - bb.SetLinuxSeccompDefault() - bundle, err := b.bundles.CreateBundle(bb, false) - if err == nil { - b.bundle = bundle - b.lockedBundles = append(b.lockedBundles, bundle) - } - return + builder.UseHostNetwork() + builder.SetProcessTerminal(false) + builder.SetLinuxSeccompDefault() + return builder.Build(newBundle) } func (b *ImageBuilder) initContainer() (err error) { @@ -731,16 +732,17 @@ func (b *ImageBuilder) resolveUser(u *idutils.User) (usrp *idutils.UserIds, err } // TODO: better resolve user using bundle's rootfs only when available, otherwise image's rootfs - if err = b.initBundle(); err == nil { - s, _ := b.bundle.Spec() - if s.Root != nil { - rootfs := filepath.Join(b.bundle.Dir(), s.Root.Path) - user, err = u.Resolve(rootfs) - } else { - err = errors.New("no rootfs available") - } + if err = b.initBundle(); err != nil { + return &user, errors.Wrap(err, "resolve user name") + } + s, _ := b.bundle.Spec() + if s.Root != nil { + rootfs := filepath.Join(b.bundle.Dir(), s.Root.Path) + user, err = u.Resolve(rootfs) + } else { + err = errors.New("no rootfs available to resolve user/group name from") } - return &user, errors.Wrap(err, "resolve user name") + return &user, err } func (b *ImageBuilder) commitConfig(createdBy string) (err error) { diff --git a/model/oci/ocitransform.go b/model/oci/ocitransform.go index 7801231..6695a51 100644 --- a/model/oci/ocitransform.go +++ b/model/oci/ocitransform.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + "github.com/mgoltzsche/ctnr/bundle" "github.com/mgoltzsche/ctnr/model" "github.com/mgoltzsche/ctnr/pkg/generate" "github.com/mgoltzsche/ctnr/pkg/idutils" @@ -22,7 +23,7 @@ const ( ANNOTATION_BUNDLE_ID = "com.github.mgoltzsche.ctnr.bundle.id" ) -func ToSpec(service *model.Service, res model.ResourceResolver, rootless bool, prootPath string, spec *generate.SpecBuilder) (err error) { +func ToSpec(service *model.Service, res model.ResourceResolver, rootless bool, prootPath string, spec *bundle.BundleBuilder) (err error) { defer func() { err = errors.Wrap(err, "generate OCI bundle spec") }() @@ -33,7 +34,7 @@ func ToSpec(service *model.Service, res model.ResourceResolver, rootless bool, p sp := spec.Generator.Spec() - if err = ToSpecProcess(&service.Process, prootPath, spec); err != nil { + if err = ToSpecProcess(&service.Process, prootPath, spec.SpecBuilder); err != nil { return } @@ -227,70 +228,70 @@ func mountHostFile(spec *specs.Spec, file string) error { return nil } -func ToSpecProcess(p *model.Process, prootPath string, spec *generate.SpecBuilder) (err error) { +func ToSpecProcess(p *model.Process, prootPath string, builder *generate.SpecBuilder) (err error) { // Entrypoint & command if p.Entrypoint != nil { - spec.SetProcessEntrypoint(p.Entrypoint) - spec.SetProcessCmd([]string{}) + builder.SetProcessEntrypoint(p.Entrypoint) + builder.SetProcessCmd([]string{}) } if p.Command != nil { - spec.SetProcessCmd(p.Command) + builder.SetProcessCmd(p.Command) } // Add proot if p.PRoot { if prootPath == "" { return errors.New("proot enabled but no proot path configured") } - spec.SetPRootPath(prootPath) + builder.SetPRootPath(prootPath) } // Env for k, v := range p.Environment { - spec.AddProcessEnv(k, v) + builder.AddProcessEnv(k, v) } // Working dir if p.Cwd != "" { - spec.SetProcessCwd(p.Cwd) + builder.SetProcessCwd(p.Cwd) } // Terminal - spec.SetProcessTerminal(p.Tty) + builder.SetProcessTerminal(p.Tty) // User if p.User != nil { // TODO: map additional groups - spec.SetProcessUser(idutils.User{p.User.User, p.User.Group}) + builder.SetProcessUser(idutils.User{p.User.User, p.User.Group}) } // Capabilities if p.CapAdd != nil { for _, addCap := range p.CapAdd { if strings.ToUpper(addCap) == "ALL" { - spec.AddAllProcessCapabilities() + builder.AddAllProcessCapabilities() break - } else if err = spec.AddProcessCapability("CAP_" + addCap); err != nil { + } else if err = builder.AddProcessCapability("CAP_" + addCap); err != nil { return } } for _, dropCap := range p.CapDrop { - if err = spec.DropProcessCapability("CAP_" + dropCap); err != nil { + if err = builder.DropProcessCapability("CAP_" + dropCap); err != nil { return } } } - spec.SetProcessApparmorProfile(p.ApparmorProfile) - spec.SetProcessNoNewPrivileges(p.NoNewPrivileges) - spec.SetProcessSelinuxLabel(p.SelinuxLabel) + builder.SetProcessApparmorProfile(p.ApparmorProfile) + builder.SetProcessNoNewPrivileges(p.NoNewPrivileges) + builder.SetProcessSelinuxLabel(p.SelinuxLabel) if p.OOMScoreAdj != nil { - spec.SetProcessOOMScoreAdj(*p.OOMScoreAdj) + builder.SetProcessOOMScoreAdj(*p.OOMScoreAdj) } return nil } -func toMounts(mounts []model.VolumeMount, res model.ResourceResolver, spec *generate.SpecBuilder) error { +func toMounts(mounts []model.VolumeMount, res model.ResourceResolver, spec *bundle.BundleBuilder) error { for _, m := range mounts { src, err := res.ResolveMountSource(m) if err != nil { diff --git a/pkg/generate/generate.go b/pkg/generate/generate.go index 852bc52..7c278b1 100644 --- a/pkg/generate/generate.go +++ b/pkg/generate/generate.go @@ -17,6 +17,7 @@ package generate import ( "os" "sort" + "strconv" "strings" "time" @@ -44,6 +45,12 @@ func NewSpecBuilder() SpecBuilder { } func FromSpec(spec *rspecs.Spec) SpecBuilder { + user := idutils.User{"0", "0"} + if spec.Process != nil { + user.User = strconv.Itoa(int(spec.Process.User.UID)) + user.Group = strconv.Itoa(int(spec.Process.User.GID)) + // TODO: map additional gids + } return SpecBuilder{Generator: generate.NewFromSpec(spec)} } @@ -238,6 +245,7 @@ func (b *SpecBuilder) ApplyImage(img *ispecs.Image) { } } +// Returns the generated spec with resolved user/group names func (b *SpecBuilder) Spec(rootfs string) (spec *rspecs.Spec, err error) { usr, err := b.user.Resolve(rootfs) if err != nil { diff --git a/vendor.conf b/vendor.conf index 5fd9d91..7ede9e8 100644 --- a/vendor.conf +++ b/vendor.conf @@ -100,10 +100,6 @@ github.com/mattn/go-shellwords v1.0.3 #github.com/kr/pty v1.0.0 -# UUID generator -github.com/satori/go.uuid 879c5887cd475cd7864858769793b2ceb0d44feb - - # Pretty print file size and time github.com/dustin/go-humanize 79e699ccd02f240a1f1fbbdcee7e64c1c12e41aa