186
pkg/rootfs/apply.go
Normal file
186
pkg/rootfs/apply.go
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rootfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/diff"
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/core/snapshots"
|
||||
"github.com/containerd/containerd/v2/pkg/errdefs"
|
||||
"github.com/containerd/log"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/opencontainers/image-spec/identity"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// Layer represents the descriptors for a layer diff. These descriptions
|
||||
// include the descriptor for the uncompressed tar diff as well as a blob
|
||||
// used to transport that tar. The blob descriptor may or may not describe
|
||||
// a compressed object.
|
||||
type Layer struct {
|
||||
Diff ocispec.Descriptor
|
||||
Blob ocispec.Descriptor
|
||||
}
|
||||
|
||||
// ApplyLayers applies all the layers using the given snapshotter and applier.
|
||||
// The returned result is a chain id digest representing all the applied layers.
|
||||
// Layers are applied in order they are given, making the first layer the
|
||||
// bottom-most layer in the layer chain.
|
||||
func ApplyLayers(ctx context.Context, layers []Layer, sn snapshots.Snapshotter, a diff.Applier) (digest.Digest, error) {
|
||||
return ApplyLayersWithOpts(ctx, layers, sn, a, nil)
|
||||
}
|
||||
|
||||
// ApplyLayersWithOpts applies all the layers using the given snapshotter, applier, and apply opts.
|
||||
// The returned result is a chain id digest representing all the applied layers.
|
||||
// Layers are applied in order they are given, making the first layer the
|
||||
// bottom-most layer in the layer chain.
|
||||
func ApplyLayersWithOpts(ctx context.Context, layers []Layer, sn snapshots.Snapshotter, a diff.Applier, applyOpts []diff.ApplyOpt) (digest.Digest, error) {
|
||||
chain := make([]digest.Digest, len(layers))
|
||||
for i, layer := range layers {
|
||||
chain[i] = layer.Diff.Digest
|
||||
}
|
||||
chainID := identity.ChainID(chain)
|
||||
|
||||
// Just stat top layer, remaining layers will have their existence checked
|
||||
// on prepare. Calling prepare on upper layers first guarantees that upper
|
||||
// layers are not removed while calling stat on lower layers
|
||||
_, err := sn.Stat(ctx, chainID.String())
|
||||
if err != nil {
|
||||
if !errdefs.IsNotFound(err) {
|
||||
return "", fmt.Errorf("failed to stat snapshot %s: %w", chainID, err)
|
||||
}
|
||||
|
||||
if err := applyLayers(ctx, layers, chain, sn, a, nil, applyOpts); err != nil && !errdefs.IsAlreadyExists(err) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return chainID, nil
|
||||
}
|
||||
|
||||
// ApplyLayer applies a single layer on top of the given provided layer chain,
|
||||
// using the provided snapshotter and applier. If the layer was unpacked true
|
||||
// is returned, if the layer already exists false is returned.
|
||||
func ApplyLayer(ctx context.Context, layer Layer, chain []digest.Digest, sn snapshots.Snapshotter, a diff.Applier, opts ...snapshots.Opt) (bool, error) {
|
||||
return ApplyLayerWithOpts(ctx, layer, chain, sn, a, opts, nil)
|
||||
}
|
||||
|
||||
// ApplyLayerWithOpts applies a single layer on top of the given provided layer chain,
|
||||
// using the provided snapshotter, applier, and apply opts. If the layer was unpacked true
|
||||
// is returned, if the layer already exists false is returned.
|
||||
func ApplyLayerWithOpts(ctx context.Context, layer Layer, chain []digest.Digest, sn snapshots.Snapshotter, a diff.Applier, opts []snapshots.Opt, applyOpts []diff.ApplyOpt) (bool, error) {
|
||||
var (
|
||||
chainID = identity.ChainID(append(chain, layer.Diff.Digest)).String()
|
||||
applied bool
|
||||
)
|
||||
if _, err := sn.Stat(ctx, chainID); err != nil {
|
||||
if !errdefs.IsNotFound(err) {
|
||||
return false, fmt.Errorf("failed to stat snapshot %s: %w", chainID, err)
|
||||
}
|
||||
|
||||
if err := applyLayers(ctx, []Layer{layer}, append(chain, layer.Diff.Digest), sn, a, opts, applyOpts); err != nil {
|
||||
if !errdefs.IsAlreadyExists(err) {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
applied = true
|
||||
}
|
||||
}
|
||||
return applied, nil
|
||||
|
||||
}
|
||||
|
||||
func applyLayers(ctx context.Context, layers []Layer, chain []digest.Digest, sn snapshots.Snapshotter, a diff.Applier, opts []snapshots.Opt, applyOpts []diff.ApplyOpt) error {
|
||||
var (
|
||||
parent = identity.ChainID(chain[:len(chain)-1])
|
||||
chainID = identity.ChainID(chain)
|
||||
layer = layers[len(layers)-1]
|
||||
diff ocispec.Descriptor
|
||||
key string
|
||||
mounts []mount.Mount
|
||||
err error
|
||||
)
|
||||
|
||||
for {
|
||||
key = fmt.Sprintf(snapshots.UnpackKeyFormat, uniquePart(), chainID)
|
||||
|
||||
// Prepare snapshot with from parent, label as root
|
||||
mounts, err = sn.Prepare(ctx, key, parent.String(), opts...)
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) && len(layers) > 1 {
|
||||
if err := applyLayers(ctx, layers[:len(layers)-1], chain[:len(chain)-1], sn, a, opts, applyOpts); err != nil {
|
||||
if !errdefs.IsAlreadyExists(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Do no try applying layers again
|
||||
layers = nil
|
||||
continue
|
||||
} else if errdefs.IsAlreadyExists(err) {
|
||||
// Try a different key
|
||||
continue
|
||||
}
|
||||
|
||||
// Already exists should have the caller retry
|
||||
return fmt.Errorf("failed to prepare extraction snapshot %q: %w", key, err)
|
||||
|
||||
}
|
||||
break
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if !errdefs.IsAlreadyExists(err) {
|
||||
log.G(ctx).WithError(err).WithField("key", key).Infof("apply failure, attempting cleanup")
|
||||
}
|
||||
|
||||
if rerr := sn.Remove(ctx, key); rerr != nil {
|
||||
log.G(ctx).WithError(rerr).WithField("key", key).Warnf("extraction snapshot removal failed")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
diff, err = a.Apply(ctx, layer.Blob, mounts, applyOpts...)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to extract layer %s: %w", layer.Diff.Digest, err)
|
||||
return err
|
||||
}
|
||||
if diff.Digest != layer.Diff.Digest {
|
||||
err = fmt.Errorf("wrong diff id calculated on extraction %q", diff.Digest)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = sn.Commit(ctx, chainID.String(), key, opts...); err != nil {
|
||||
err = fmt.Errorf("failed to commit snapshot %s: %w", key, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uniquePart() string {
|
||||
t := time.Now()
|
||||
var b [3]byte
|
||||
// Ignore read failures, just decreases uniqueness
|
||||
rand.Read(b[:])
|
||||
return fmt.Sprintf("%d-%s", t.Nanosecond(), base64.URLEncoding.EncodeToString(b[:]))
|
||||
}
|
||||
67
pkg/rootfs/diff.go
Normal file
67
pkg/rootfs/diff.go
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rootfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/diff"
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/core/snapshots"
|
||||
"github.com/containerd/containerd/v2/pkg/cleanup"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// CreateDiff creates a layer diff for the given snapshot identifier from the
|
||||
// parent of the snapshot. A content ref is provided to track the progress of
|
||||
// the content creation and the provided snapshotter and mount differ are used
|
||||
// for calculating the diff. The descriptor for the layer diff is returned.
|
||||
func CreateDiff(ctx context.Context, snapshotID string, sn snapshots.Snapshotter, d diff.Comparer, opts ...diff.Opt) (ocispec.Descriptor, error) {
|
||||
info, err := sn.Stat(ctx, snapshotID)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
lowerKey := fmt.Sprintf("%s-parent-view-%s", info.Parent, uniquePart())
|
||||
lower, err := sn.View(ctx, lowerKey, info.Parent)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
defer cleanup.Do(ctx, func(ctx context.Context) {
|
||||
sn.Remove(ctx, lowerKey)
|
||||
})
|
||||
|
||||
var upper []mount.Mount
|
||||
if info.Kind == snapshots.KindActive {
|
||||
upper, err = sn.Mounts(ctx, snapshotID)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
} else {
|
||||
upperKey := fmt.Sprintf("%s-view-%s", snapshotID, uniquePart())
|
||||
upper, err = sn.View(ctx, upperKey, snapshotID)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
defer cleanup.Do(ctx, func(ctx context.Context) {
|
||||
sn.Remove(ctx, upperKey)
|
||||
})
|
||||
}
|
||||
|
||||
return d.Compare(ctx, lower, upper, opts...)
|
||||
}
|
||||
116
pkg/rootfs/init.go
Normal file
116
pkg/rootfs/init.go
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rootfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/core/snapshots"
|
||||
"github.com/containerd/log"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
var (
|
||||
initializers = map[string]initializerFunc{}
|
||||
)
|
||||
|
||||
type initializerFunc func(string) error
|
||||
|
||||
// Mounter handles mount and unmount
|
||||
type Mounter interface {
|
||||
Mount(target string, mounts ...mount.Mount) error
|
||||
Unmount(target string) error
|
||||
}
|
||||
|
||||
// InitRootFS initializes the snapshot for use as a rootfs
|
||||
func InitRootFS(ctx context.Context, name string, parent digest.Digest, readonly bool, snapshotter snapshots.Snapshotter, mounter Mounter) ([]mount.Mount, error) {
|
||||
_, err := snapshotter.Stat(ctx, name)
|
||||
if err == nil {
|
||||
return nil, errors.New("rootfs already exists")
|
||||
}
|
||||
// TODO: ensure not exist error once added to snapshot package
|
||||
|
||||
parentS := parent.String()
|
||||
|
||||
initName := defaultInitializer
|
||||
initFn := initializers[initName]
|
||||
if initFn != nil {
|
||||
parentS, err = createInitLayer(ctx, parentS, initName, initFn, snapshotter, mounter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if readonly {
|
||||
return snapshotter.View(ctx, name, parentS)
|
||||
}
|
||||
|
||||
return snapshotter.Prepare(ctx, name, parentS)
|
||||
}
|
||||
|
||||
func createInitLayer(ctx context.Context, parent, initName string, initFn func(string) error, snapshotter snapshots.Snapshotter, mounter Mounter) (_ string, retErr error) {
|
||||
initS := fmt.Sprintf("%s %s", parent, initName)
|
||||
if _, err := snapshotter.Stat(ctx, initS); err == nil {
|
||||
return initS, nil
|
||||
}
|
||||
// TODO: ensure not exist error once added to snapshot package
|
||||
|
||||
// Create tempdir
|
||||
td, err := os.MkdirTemp(os.Getenv("XDG_RUNTIME_DIR"), "create-init-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.RemoveAll(td)
|
||||
|
||||
mounts, err := snapshotter.Prepare(ctx, td, parent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
if rerr := snapshotter.Remove(ctx, td); rerr != nil {
|
||||
log.G(ctx).Errorf("Failed to remove snapshot %s: %v", td, rerr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = mounter.Mount(td, mounts...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = initFn(td); err != nil {
|
||||
if merr := mounter.Unmount(td); merr != nil {
|
||||
log.G(ctx).Errorf("Failed to unmount %s: %v", td, merr)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = mounter.Unmount(td); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := snapshotter.Commit(ctx, initS, td); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return initS, nil
|
||||
}
|
||||
130
pkg/rootfs/init_linux.go
Normal file
130
pkg/rootfs/init_linux.go
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rootfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultInitializer = "linux-init"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers[defaultInitializer] = initFS
|
||||
}
|
||||
|
||||
func createDirectory(name string, uid, gid int) initializerFunc {
|
||||
return func(root string) error {
|
||||
dname := filepath.Join(root, name)
|
||||
st, err := os.Stat(dname)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
} else if err == nil {
|
||||
if st.IsDir() {
|
||||
stat := st.Sys().(*syscall.Stat_t)
|
||||
if int(stat.Gid) == gid && int(stat.Uid) == uid {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(dname); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Mkdir(dname, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := os.Mkdir(dname, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return os.Chown(dname, uid, gid)
|
||||
}
|
||||
}
|
||||
|
||||
func touchFile(name string, uid, gid int) initializerFunc {
|
||||
return func(root string) error {
|
||||
fname := filepath.Join(root, name)
|
||||
|
||||
st, err := os.Stat(fname)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
} else if err == nil {
|
||||
stat := st.Sys().(*syscall.Stat_t)
|
||||
if int(stat.Gid) == gid && int(stat.Uid) == uid {
|
||||
return nil
|
||||
}
|
||||
return os.Chown(fname, uid, gid)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(fname, os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return f.Chown(uid, gid)
|
||||
}
|
||||
}
|
||||
|
||||
func symlink(oldname, newname string) initializerFunc {
|
||||
return func(root string) error {
|
||||
linkName := filepath.Join(root, newname)
|
||||
if _, err := os.Stat(linkName); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
} else if err == nil {
|
||||
return nil
|
||||
}
|
||||
return os.Symlink(oldname, linkName)
|
||||
}
|
||||
}
|
||||
|
||||
func initFS(root string) error {
|
||||
st, err := os.Stat(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stat := st.Sys().(*syscall.Stat_t)
|
||||
uid := int(stat.Uid)
|
||||
gid := int(stat.Gid)
|
||||
|
||||
initFuncs := []initializerFunc{
|
||||
createDirectory("/dev", uid, gid),
|
||||
createDirectory("/dev/pts", uid, gid),
|
||||
createDirectory("/dev/shm", uid, gid),
|
||||
touchFile("/dev/console", uid, gid),
|
||||
createDirectory("/proc", uid, gid),
|
||||
createDirectory("/sys", uid, gid),
|
||||
createDirectory("/etc", uid, gid),
|
||||
touchFile("/etc/resolv.conf", uid, gid),
|
||||
touchFile("/etc/hosts", uid, gid),
|
||||
touchFile("/etc/hostname", uid, gid),
|
||||
symlink("/proc/mounts", "/etc/mtab"),
|
||||
}
|
||||
|
||||
for _, fn := range initFuncs {
|
||||
if err := fn(root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
23
pkg/rootfs/init_other.go
Normal file
23
pkg/rootfs/init_other.go
Normal file
@@ -0,0 +1,23 @@
|
||||
//go:build !linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rootfs
|
||||
|
||||
const (
|
||||
defaultInitializer = ""
|
||||
)
|
||||
Reference in New Issue
Block a user