1/*
2   Copyright The containerd Authors.
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17package v2
18
19import (
20	"context"
21	"fmt"
22	"io/ioutil"
23	"os"
24	"path/filepath"
25
26	"github.com/containerd/containerd/identifiers"
27	"github.com/containerd/containerd/mount"
28	"github.com/containerd/containerd/namespaces"
29	"github.com/pkg/errors"
30)
31
32const configFilename = "config.json"
33
34// LoadBundle loads an existing bundle from disk
35func LoadBundle(ctx context.Context, root, id string) (*Bundle, error) {
36	ns, err := namespaces.NamespaceRequired(ctx)
37	if err != nil {
38		return nil, err
39	}
40	return &Bundle{
41		ID:        id,
42		Path:      filepath.Join(root, ns, id),
43		Namespace: ns,
44	}, nil
45}
46
47// NewBundle returns a new bundle on disk
48func NewBundle(ctx context.Context, root, state, id string, spec []byte) (b *Bundle, err error) {
49	if err := identifiers.Validate(id); err != nil {
50		return nil, errors.Wrapf(err, "invalid task id %s", id)
51	}
52
53	ns, err := namespaces.NamespaceRequired(ctx)
54	if err != nil {
55		return nil, err
56	}
57	work := filepath.Join(root, ns, id)
58	b = &Bundle{
59		ID:        id,
60		Path:      filepath.Join(state, ns, id),
61		Namespace: ns,
62	}
63	var paths []string
64	defer func() {
65		if err != nil {
66			for _, d := range paths {
67				os.RemoveAll(d)
68			}
69		}
70	}()
71	// create state directory for the bundle
72	if err := os.MkdirAll(filepath.Dir(b.Path), 0711); err != nil {
73		return nil, err
74	}
75	if err := os.Mkdir(b.Path, 0711); err != nil {
76		return nil, err
77	}
78	paths = append(paths, b.Path)
79	// create working directory for the bundle
80	if err := os.MkdirAll(filepath.Dir(work), 0711); err != nil {
81		return nil, err
82	}
83	rootfs := filepath.Join(b.Path, "rootfs")
84	if err := os.MkdirAll(rootfs, 0711); err != nil {
85		return nil, err
86	}
87	paths = append(paths, rootfs)
88	if err := os.Mkdir(work, 0711); err != nil {
89		if !os.IsExist(err) {
90			return nil, err
91		}
92		os.RemoveAll(work)
93		if err := os.Mkdir(work, 0711); err != nil {
94			return nil, err
95		}
96	}
97	paths = append(paths, work)
98	// symlink workdir
99	if err := os.Symlink(work, filepath.Join(b.Path, "work")); err != nil {
100		return nil, err
101	}
102	// write the spec to the bundle
103	err = ioutil.WriteFile(filepath.Join(b.Path, configFilename), spec, 0666)
104	return b, err
105}
106
107// Bundle represents an OCI bundle
108type Bundle struct {
109	// ID of the bundle
110	ID string
111	// Path to the bundle
112	Path string
113	// Namespace of the bundle
114	Namespace string
115}
116
117// Delete a bundle atomically
118func (b *Bundle) Delete() error {
119	work, werr := os.Readlink(filepath.Join(b.Path, "work"))
120	rootfs := filepath.Join(b.Path, "rootfs")
121	if err := mount.UnmountAll(rootfs, 0); err != nil {
122		return errors.Wrapf(err, "unmount rootfs %s", rootfs)
123	}
124	if err := os.Remove(rootfs); err != nil && os.IsNotExist(err) {
125		return errors.Wrap(err, "failed to remove bundle rootfs")
126	}
127	err := atomicDelete(b.Path)
128	if err == nil {
129		if werr == nil {
130			return atomicDelete(work)
131		}
132		return nil
133	}
134	// error removing the bundle path; still attempt removing work dir
135	var err2 error
136	if werr == nil {
137		err2 = atomicDelete(work)
138		if err2 == nil {
139			return err
140		}
141	}
142	return errors.Wrapf(err, "failed to remove both bundle and workdir locations: %v", err2)
143}
144
145// atomicDelete renames the path to a hidden file before removal
146func atomicDelete(path string) error {
147	// create a hidden dir for an atomic removal
148	atomicPath := filepath.Join(filepath.Dir(path), fmt.Sprintf(".%s", filepath.Base(path)))
149	if err := os.Rename(path, atomicPath); err != nil {
150		if os.IsNotExist(err) {
151			return nil
152		}
153		return err
154	}
155	return os.RemoveAll(atomicPath)
156}
157