-
Notifications
You must be signed in to change notification settings - Fork 12
/
generate.go
349 lines (306 loc) · 9.26 KB
/
generate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
package nix2container
import (
"archive/tar"
"bytes"
"context"
_ "crypto/sha256" // required by go-digest
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/containerd/containerd/archive"
"github.com/containerd/containerd/archive/compression"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/remotes"
cfs "github.com/containerd/continuity/fs"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pdtpartners/nix-snapshotter/types"
)
const (
// NixLayerAnnotation is a remote snapshot OCI annotation to indicate that
// it will also contain annotations with NixStorePrefixAnnotation.
NixLayerAnnotation = "containerd.io/snapshot/nix-layer"
// NixStorePrefixAnnotation is a prefix for remote snapshot OCI annotations
// for each nix store path that the layer will need.
NixStorePrefixAnnotation = "containerd.io/snapshot/nix-store-path."
)
// TempDir returns the location of a temporary dir or XDG_RUNTIME_DIR if it is
// defined.
func TempDir() string {
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
return xdg
}
return os.TempDir()
}
// Generate adds a nix-snapshotter container image to store and returns its
// descriptor.
func Generate(ctx context.Context, image *types.Image, store content.Store) (desc ocispec.Descriptor, err error) {
// Initialize the manifest and manifest config from its base image.
var (
mfst ocispec.Manifest
cfg ocispec.Image
)
mfst, cfg, err = initializeManifest(ctx, image, store)
if err != nil {
return
}
// Generate and add layer to store.
buf := new(bytes.Buffer)
diffID, err := writeNixClosureLayer(ctx, buf, image.NixStorePaths, image.CopyToRoots)
if err != nil {
return
}
cfg.RootFS.DiffIDs = append(cfg.RootFS.DiffIDs, diffID)
layerDesc, err := writeBlob(ctx, store, ocispec.MediaTypeImageLayerGzip, buf.Bytes())
if err != nil {
return
}
layerDesc.Annotations = map[string]string{
NixLayerAnnotation: "true",
}
for i, nixStorePath := range image.NixStorePaths {
key := NixStorePrefixAnnotation + strconv.Itoa(i)
layerDesc.Annotations[key] = nixStorePath
}
mfst.Layers = append(mfst.Layers, layerDesc)
// Add manifest config to store.
configDesc, err := writeBlob(ctx, store, ocispec.MediaTypeImageConfig, &cfg)
if err != nil {
return
}
mfst.Config = configDesc
// Add manifest to store.
return writeBlob(ctx, store, mfst.MediaType, &mfst)
}
func writeBlob(ctx context.Context, store content.Store, mediaType string, v interface{}) (ocispec.Descriptor, error) {
blob, ok := v.([]byte)
if !ok {
var err error
blob, err = json.MarshalIndent(v, "", " ")
if err != nil {
return ocispec.Descriptor{}, err
}
}
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
ref := remotes.MakeRefKey(ctx, desc)
return desc, content.WriteBlob(ctx, store, ref, bytes.NewReader(blob), desc)
}
// initializeManifest initializes a manifest and manifest config based on the
// image's base image.
func initializeManifest(ctx context.Context, image *types.Image, store content.Store) (ocispec.Manifest, ocispec.Image, error) {
mfst := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Versioned: specs.Versioned{
SchemaVersion: 2,
},
Annotations: make(map[string]string),
}
cfg := ocispec.Image{
Config: image.Config,
Platform: platforms.Platform{
Architecture: image.Architecture,
OS: image.OS,
},
RootFS: ocispec.RootFS{
Type: "layers",
},
}
// If the base image is non-empty, add the base image's layers, annotations
// and diff IDs to the new image's manifest and manifest config.
if image.BaseImage != "" {
baseMfst, baseCfg, err := parseOCITarball(ctx, store, image.BaseImage)
if err != nil {
return mfst, cfg, err
}
// Inherit layers, annotations and diff IDs from base image.
mfst.Layers = append(mfst.Layers, baseMfst.Layers...)
for k, v := range mfst.Annotations {
mfst.Annotations[k] = v
}
cfg.RootFS.DiffIDs = append(cfg.RootFS.DiffIDs, baseCfg.RootFS.DiffIDs...)
}
return mfst, cfg, nil
}
// parseOCITarball extracts a ocispec.Manifest and ocispec.Image from an OCI
// archive tarball at the given tarballPath.
func parseOCITarball(ctx context.Context, store content.Store, tarballPath string) (mfst ocispec.Manifest, cfg ocispec.Image, err error) {
// Untar OCI tarball into temp directory.
root, err := os.MkdirTemp(TempDir(), "nix2container-oci")
if err != nil {
return mfst, cfg, fmt.Errorf("failed to create temp dir: %w", err)
}
defer os.RemoveAll(root)
f, err := os.Open(tarballPath)
if err != nil {
return
}
defer f.Close()
// Don't preserve the owner specified in the tar archive because it will try
// to lchown(2) the archive but the archive is usually from a read only nix
// store.
_, err = archive.Apply(ctx, root, f, archive.WithNoSameOwner())
if err != nil {
return
}
// Unmarshal manifest.
manifestPath := filepath.Join(root, "manifest.json")
dt, err := os.ReadFile(manifestPath)
if err != nil {
return
}
var ociMfsts []types.OCIManifest
err = json.Unmarshal(dt, &ociMfsts)
if err != nil {
return
}
if len(ociMfsts) != 1 {
return mfst, cfg, fmt.Errorf("expected %d number of manifests, got %d", 1, len(ociMfsts))
}
ociMfst := ociMfsts[0]
// Unmarshal manifest config.
configPath := filepath.Join(root, ociMfst.Config)
dt, err = os.ReadFile(configPath)
if err != nil {
return
}
err = json.Unmarshal(dt, &cfg)
if err != nil {
return
}
mfst.Config, err = writeBlob(ctx, store, ocispec.MediaTypeImageConfig, dt)
if err != nil {
return
}
// Load layers into store.
for _, layer := range ociMfst.Layers {
layerPath := filepath.Join(root, layer)
dt, err = os.ReadFile(layerPath)
if err != nil {
return
}
var desc ocispec.Descriptor
desc, err = writeBlob(ctx, store, ocispec.MediaTypeImageLayer, dt)
if err != nil {
return
}
mfst.Layers = append(mfst.Layers, desc)
}
return
}
// writeNixClosureLayer generates a tarball that creates a mountpoint for every
// store path. When the snapshotter prepares this layer, it will then mount the
// store paths into these mountpoints.
//
// Each store path in copyToRoots will also be walked to generate symlinks
// relative to root. Note that these symlinks will be broken until the
// containerd-shim finally mounts what nix-snapshotter has generated.
func writeNixClosureLayer(ctx context.Context, w io.Writer, nixStorePaths, copyToRoots []string) (digest.Digest, error) {
root, err := os.MkdirTemp(TempDir(), "nix2container-closure")
if err != nil {
return "", fmt.Errorf("failed to create temp dir: %w", err)
}
defer os.RemoveAll(root)
for _, nixStorePath := range nixStorePaths {
fi, err := os.Stat(nixStorePath)
if err != nil {
return "", err
}
relStorePath := filepath.Join(root, nixStorePath)
if !fi.IsDir() {
relStorePath = filepath.Dir(relStorePath)
}
err = os.MkdirAll(relStorePath, 0o755)
if err != nil {
return "", err
}
}
// For each copyToRoot, walk the store path locally and create a symlink for
// each file from the store path to a path relative to the rootfs' root.
//
// Example:
// /nix/store/knn6wc1a89c47yb70qwv56rmxylia6wx-hello-2.12/bin/hello
// =>
// /bin/hello
for _, copyToRoot := range copyToRoots {
err = filepath.WalkDir(copyToRoot, func(path string, dentry fs.DirEntry, err error) error {
if err != nil {
return err
}
rootPath := filepath.Join(root, strings.TrimPrefix(path, copyToRoot))
if dentry.IsDir() {
return os.MkdirAll(rootPath, 0o755)
}
return os.Symlink(path, rootPath)
})
if err != nil {
return "", err
}
}
return tarDir(ctx, w, root, true)
}
func tarDir(ctx context.Context, w io.Writer, root string, gzip bool) (digest.Digest, error) {
if gzip {
compressed, err := compression.CompressStream(w, compression.Gzip)
if err != nil {
return "", err
}
defer compressed.Close()
w = compressed
}
// Set upper bound for timestamps to be epoch 0 for reproducibility.
opts := []archive.ChangeWriterOpt{
archive.WithModTimeUpperBound(time.Time{}),
}
dgstr := digest.SHA256.Digester()
cw := archive.NewChangeWriter(io.MultiWriter(w, dgstr.Hash()), root, opts...)
err := cfs.Changes(ctx, "", root, rootUidGidChangeFunc(cw.HandleChange))
// Finish archiving data before completing compression.
cwErr := cw.Close()
if err != nil {
return "", fmt.Errorf("failed to create diff tar stream: %w", err)
}
if cwErr != nil {
return "", cwErr
}
return dgstr.Digest(), nil
}
// rootFileInfo is a wrapped fs.FileInfo that forces Uid and Gid to be 0.
type rootFileInfo struct {
fs.FileInfo
}
func (rfi *rootFileInfo) Sys() any {
sys := rfi.FileInfo.Sys()
switch s := sys.(type) {
case *tar.Header:
s.Uid = 0
s.Gid = 0
return sys
case *syscall.Stat_t:
s.Uid = 0
s.Gid = 0
return sys
default:
return sys
}
}
// rootUidGidChangeFunc is a fs.ChangeFunc that wraps underlying os.FileInfo
// with one that forces Uid and Gid to be 0.
func rootUidGidChangeFunc(fn cfs.ChangeFunc) cfs.ChangeFunc {
return func(k cfs.ChangeKind, p string, f os.FileInfo, err error) error {
rf := &rootFileInfo{f}
return fn(k, p, rf, err)
}
}