podman

Форк
0
643 строки · 20.9 Кб
1
package volumes
2

3
import (
4
	"context"
5
	"fmt"
6
	"os"
7
	"path"
8
	"path/filepath"
9
	"strconv"
10
	"strings"
11

12
	"errors"
13

14
	"github.com/containers/buildah/copier"
15
	"github.com/containers/buildah/define"
16
	"github.com/containers/buildah/internal"
17
	internalParse "github.com/containers/buildah/internal/parse"
18
	"github.com/containers/buildah/internal/tmpdir"
19
	internalUtil "github.com/containers/buildah/internal/util"
20
	"github.com/containers/common/pkg/parse"
21
	"github.com/containers/image/v5/types"
22
	"github.com/containers/storage"
23
	"github.com/containers/storage/pkg/idtools"
24
	"github.com/containers/storage/pkg/lockfile"
25
	"github.com/containers/storage/pkg/unshare"
26
	specs "github.com/opencontainers/runtime-spec/specs-go"
27
	selinux "github.com/opencontainers/selinux/go-selinux"
28
)
29

30
const (
31
	// TypeTmpfs is the type for mounting tmpfs
32
	TypeTmpfs = "tmpfs"
33
	// TypeCache is the type for mounting a common persistent cache from host
34
	TypeCache = "cache"
35
	// mount=type=cache must create a persistent directory on host so its available for all consecutive builds.
36
	// Lifecycle of following directory will be inherited from how host machine treats temporary directory
37
	buildahCacheDir = "buildah-cache"
38
	// mount=type=cache allows users to lock a cache store while its being used by another build
39
	BuildahCacheLockfile = "buildah-cache-lockfile"
40
	// All the lockfiles are stored in a separate directory inside `BuildahCacheDir`
41
	// Example `/var/tmp/buildah-cache/<target>/buildah-cache-lockfile`
42
	BuildahCacheLockfileDir = "buildah-cache-lockfiles"
43
)
44

45
var (
46
	errBadMntOption  = errors.New("invalid mount option")
47
	errBadOptionArg  = errors.New("must provide an argument for option")
48
	errBadVolDest    = errors.New("must set volume destination")
49
	errBadVolSrc     = errors.New("must set volume source")
50
	errDuplicateDest = errors.New("duplicate mount destination")
51
)
52

53
// CacheParent returns a cache parent for --mount=type=cache
54
func CacheParent() string {
55
	return filepath.Join(tmpdir.GetTempDir(), buildahCacheDir+"-"+strconv.Itoa(unshare.GetRootlessUID()))
56
}
57

58
// FIXME: this code needs to be merged with pkg/parse/parse.go ValidateVolumeOpts
59
// GetBindMount parses a single bind mount entry from the --mount flag.
60
// Returns specifiedMount and a string which contains name of image that we mounted otherwise its empty.
61
// Caller is expected to perform unmount of any mounted images
62
func GetBindMount(ctx *types.SystemContext, args []string, contextDir string, store storage.Store, imageMountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir string) (specs.Mount, string, error) {
63
	newMount := specs.Mount{
64
		Type: define.TypeBind,
65
	}
66

67
	setRelabel := false
68
	mountReadability := false
69
	setDest := false
70
	bindNonRecursive := false
71
	fromImage := ""
72

73
	for _, val := range args {
74
		argName, argValue, hasArgValue := strings.Cut(val, "=")
75
		switch argName {
76
		case "type":
77
			// This is already processed
78
			continue
79
		case "bind-nonrecursive":
80
			newMount.Options = append(newMount.Options, "bind")
81
			bindNonRecursive = true
82
		case "ro", "nosuid", "nodev", "noexec":
83
			// TODO: detect duplication of these options.
84
			// (Is this necessary?)
85
			newMount.Options = append(newMount.Options, argName)
86
			mountReadability = true
87
		case "rw", "readwrite":
88
			newMount.Options = append(newMount.Options, "rw")
89
			mountReadability = true
90
		case "readonly":
91
			// Alias for "ro"
92
			newMount.Options = append(newMount.Options, "ro")
93
			mountReadability = true
94
		case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z", "U", "no-dereference":
95
			if hasArgValue {
96
				return newMount, "", fmt.Errorf("%v: %w", val, errBadOptionArg)
97
			}
98
			newMount.Options = append(newMount.Options, argName)
99
		case "from":
100
			if !hasArgValue {
101
				return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
102
			}
103
			fromImage = argValue
104
		case "bind-propagation":
105
			if !hasArgValue {
106
				return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
107
			}
108
			newMount.Options = append(newMount.Options, argValue)
109
		case "src", "source":
110
			if !hasArgValue {
111
				return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
112
			}
113
			newMount.Source = argValue
114
		case "target", "dst", "destination":
115
			if !hasArgValue {
116
				return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
117
			}
118
			targetPath := argValue
119
			if !path.IsAbs(targetPath) {
120
				targetPath = filepath.Join(workDir, targetPath)
121
			}
122
			if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
123
				return newMount, "", err
124
			}
125
			newMount.Destination = targetPath
126
			setDest = true
127
		case "relabel":
128
			if setRelabel {
129
				return newMount, "", fmt.Errorf("cannot pass 'relabel' option more than once: %w", errBadOptionArg)
130
			}
131
			setRelabel = true
132
			switch argValue {
133
			case "private":
134
				newMount.Options = append(newMount.Options, "Z")
135
			case "shared":
136
				newMount.Options = append(newMount.Options, "z")
137
			default:
138
				return newMount, "", fmt.Errorf("%s mount option must be 'private' or 'shared': %w", argName, errBadMntOption)
139
			}
140
		case "consistency":
141
			// Option for OS X only, has no meaning on other platforms
142
			// and can thus be safely ignored.
143
			// See also the handling of the equivalent "delegated" and "cached" in ValidateVolumeOpts
144
		default:
145
			return newMount, "", fmt.Errorf("%v: %w", argName, errBadMntOption)
146
		}
147
	}
148

149
	// default mount readability is always readonly
150
	if !mountReadability {
151
		newMount.Options = append(newMount.Options, "ro")
152
	}
153

154
	// Following variable ensures that we return imagename only if we did additional mount
155
	isImageMounted := false
156
	if fromImage != "" {
157
		mountPoint := ""
158
		if additionalMountPoints != nil {
159
			if val, ok := additionalMountPoints[fromImage]; ok {
160
				mountPoint = val.MountPoint
161
			}
162
		}
163
		// if mountPoint of image was not found in additionalMap
164
		// or additionalMap was nil, try mounting image
165
		if mountPoint == "" {
166
			image, err := internalUtil.LookupImage(ctx, store, fromImage)
167
			if err != nil {
168
				return newMount, "", err
169
			}
170

171
			mountPoint, err = image.Mount(context.Background(), nil, imageMountLabel)
172
			if err != nil {
173
				return newMount, "", err
174
			}
175
			isImageMounted = true
176
		}
177
		contextDir = mountPoint
178
	}
179

180
	// buildkit parity: default bind option must be `rbind`
181
	// unless specified
182
	if !bindNonRecursive {
183
		newMount.Options = append(newMount.Options, "rbind")
184
	}
185

186
	if !setDest {
187
		return newMount, fromImage, errBadVolDest
188
	}
189

190
	// buildkit parity: support absolute path for sources from current build context
191
	if contextDir != "" {
192
		// path should be /contextDir/specified path
193
		evaluated, err := copier.Eval(contextDir, newMount.Source, copier.EvalOptions{})
194
		if err != nil {
195
			return newMount, "", err
196
		}
197
		newMount.Source = evaluated
198
	} else {
199
		// looks like its coming from `build run --mount=type=bind` allow using absolute path
200
		// error out if no source is set
201
		if newMount.Source == "" {
202
			return newMount, "", errBadVolSrc
203
		}
204
		if err := parse.ValidateVolumeHostDir(newMount.Source); err != nil {
205
			return newMount, "", err
206
		}
207
	}
208

209
	opts, err := parse.ValidateVolumeOpts(newMount.Options)
210
	if err != nil {
211
		return newMount, fromImage, err
212
	}
213
	newMount.Options = opts
214

215
	if !isImageMounted {
216
		// we don't want any cleanups if image was not mounted explicitly
217
		// so dont return anything
218
		fromImage = ""
219
	}
220

221
	return newMount, fromImage, nil
222
}
223

224
// GetCacheMount parses a single cache mount entry from the --mount flag.
225
//
226
// If this function succeeds and returns a non-nil *lockfile.LockFile, the caller must unlock it (when??).
227
func GetCacheMount(args []string, store storage.Store, imageMountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir string) (specs.Mount, *lockfile.LockFile, error) {
228
	var err error
229
	var mode uint64
230
	var buildahLockFilesDir string
231
	var (
232
		setDest           bool
233
		setShared         bool
234
		setReadOnly       bool
235
		foundSElinuxLabel bool
236
	)
237
	fromStage := ""
238
	newMount := specs.Mount{
239
		Type: define.TypeBind,
240
	}
241
	// if id is set a new subdirectory with `id` will be created under /host-temp/buildah-build-cache/id
242
	id := ""
243
	// buildkit parity: cache directory defaults to 755
244
	mode = 0o755
245
	// buildkit parity: cache directory defaults to uid 0 if not specified
246
	uid := 0
247
	// buildkit parity: cache directory defaults to gid 0 if not specified
248
	gid := 0
249
	// sharing mode
250
	sharing := "shared"
251

252
	for _, val := range args {
253
		argName, argValue, hasArgValue := strings.Cut(val, "=")
254
		switch argName {
255
		case "type":
256
			// This is already processed
257
			continue
258
		case "nosuid", "nodev", "noexec":
259
			// TODO: detect duplication of these options.
260
			// (Is this necessary?)
261
			newMount.Options = append(newMount.Options, argName)
262
		case "rw", "readwrite":
263
			newMount.Options = append(newMount.Options, "rw")
264
		case "readonly", "ro":
265
			// Alias for "ro"
266
			newMount.Options = append(newMount.Options, "ro")
267
			setReadOnly = true
268
		case "Z", "z":
269
			newMount.Options = append(newMount.Options, argName)
270
			foundSElinuxLabel = true
271
		case "shared", "rshared", "private", "rprivate", "slave", "rslave", "U":
272
			newMount.Options = append(newMount.Options, argName)
273
			setShared = true
274
		case "sharing":
275
			sharing = argValue
276
		case "bind-propagation":
277
			if !hasArgValue {
278
				return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
279
			}
280
			newMount.Options = append(newMount.Options, argValue)
281
		case "id":
282
			if !hasArgValue {
283
				return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
284
			}
285
			id = argValue
286
		case "from":
287
			if !hasArgValue {
288
				return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
289
			}
290
			fromStage = argValue
291
		case "target", "dst", "destination":
292
			if !hasArgValue {
293
				return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
294
			}
295
			targetPath := argValue
296
			if !path.IsAbs(targetPath) {
297
				targetPath = filepath.Join(workDir, targetPath)
298
			}
299
			if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
300
				return newMount, nil, err
301
			}
302
			newMount.Destination = targetPath
303
			setDest = true
304
		case "src", "source":
305
			if !hasArgValue {
306
				return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
307
			}
308
			newMount.Source = argValue
309
		case "mode":
310
			if !hasArgValue {
311
				return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
312
			}
313
			mode, err = strconv.ParseUint(argValue, 8, 32)
314
			if err != nil {
315
				return newMount, nil, fmt.Errorf("unable to parse cache mode: %w", err)
316
			}
317
		case "uid":
318
			if !hasArgValue {
319
				return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
320
			}
321
			uid, err = strconv.Atoi(argValue)
322
			if err != nil {
323
				return newMount, nil, fmt.Errorf("unable to parse cache uid: %w", err)
324
			}
325
		case "gid":
326
			if !hasArgValue {
327
				return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
328
			}
329
			gid, err = strconv.Atoi(argValue)
330
			if err != nil {
331
				return newMount, nil, fmt.Errorf("unable to parse cache gid: %w", err)
332
			}
333
		default:
334
			return newMount, nil, fmt.Errorf("%v: %w", argName, errBadMntOption)
335
		}
336
	}
337

338
	// If selinux is enabled and no selinux option was configured
339
	// default to `z` i.e shared content label.
340
	if !foundSElinuxLabel && (selinux.EnforceMode() != selinux.Disabled) && fromStage == "" {
341
		newMount.Options = append(newMount.Options, "z")
342
	}
343

344
	if !setDest {
345
		return newMount, nil, errBadVolDest
346
	}
347

348
	if fromStage != "" {
349
		// do not create cache on host
350
		// instead use read-only mounted stage as cache
351
		mountPoint := ""
352
		if additionalMountPoints != nil {
353
			if val, ok := additionalMountPoints[fromStage]; ok {
354
				if val.IsStage {
355
					mountPoint = val.MountPoint
356
				}
357
			}
358
		}
359
		// Cache does not supports using image so if not stage found
360
		// return with error
361
		if mountPoint == "" {
362
			return newMount, nil, fmt.Errorf("no stage found with name %s", fromStage)
363
		}
364
		// path should be /contextDir/specified path
365
		newMount.Source = filepath.Join(mountPoint, filepath.Clean(string(filepath.Separator)+newMount.Source))
366
	} else {
367
		// we need to create cache on host if no image is being used
368

369
		// since type is cache and cache can be reused by consecutive builds
370
		// create a common cache directory, which persists on hosts within temp lifecycle
371
		// add subdirectory if specified
372

373
		// cache parent directory: creates separate cache parent for each user.
374
		cacheParent := CacheParent()
375
		// create cache on host if not present
376
		err = os.MkdirAll(cacheParent, os.FileMode(0755))
377
		if err != nil {
378
			return newMount, nil, fmt.Errorf("unable to create build cache directory: %w", err)
379
		}
380

381
		if id != "" {
382
			newMount.Source = filepath.Join(cacheParent, filepath.Clean(id))
383
			buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, filepath.Clean(id))
384
		} else {
385
			newMount.Source = filepath.Join(cacheParent, filepath.Clean(newMount.Destination))
386
			buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, filepath.Clean(newMount.Destination))
387
		}
388
		idPair := idtools.IDPair{
389
			UID: uid,
390
			GID: gid,
391
		}
392
		// buildkit parity: change uid and gid if specified otheriwise keep `0`
393
		err = idtools.MkdirAllAndChownNew(newMount.Source, os.FileMode(mode), idPair)
394
		if err != nil {
395
			return newMount, nil, fmt.Errorf("unable to change uid,gid of cache directory: %w", err)
396
		}
397

398
		// create a subdirectory inside `cacheParent` just to store lockfiles
399
		buildahLockFilesDir = filepath.Join(cacheParent, buildahLockFilesDir)
400
		err = os.MkdirAll(buildahLockFilesDir, os.FileMode(0700))
401
		if err != nil {
402
			return newMount, nil, fmt.Errorf("unable to create build cache lockfiles directory: %w", err)
403
		}
404
	}
405

406
	var targetLock *lockfile.LockFile // = nil
407
	succeeded := false
408
	defer func() {
409
		if !succeeded && targetLock != nil {
410
			targetLock.Unlock()
411
		}
412
	}()
413
	switch sharing {
414
	case "locked":
415
		// lock parent cache
416
		lockfile, err := lockfile.GetLockFile(filepath.Join(buildahLockFilesDir, BuildahCacheLockfile))
417
		if err != nil {
418
			return newMount, nil, fmt.Errorf("unable to acquire lock when sharing mode is locked: %w", err)
419
		}
420
		// Will be unlocked after the RUN step is executed.
421
		lockfile.Lock()
422
		targetLock = lockfile
423
	case "shared":
424
		// do nothing since default is `shared`
425
		break
426
	default:
427
		// error out for unknown values
428
		return newMount, nil, fmt.Errorf("unrecognized value %q for field `sharing`: %w", sharing, err)
429
	}
430

431
	// buildkit parity: default sharing should be shared
432
	// unless specified
433
	if !setShared {
434
		newMount.Options = append(newMount.Options, "shared")
435
	}
436

437
	// buildkit parity: cache must writable unless `ro` or `readonly` is configured explicitly
438
	if !setReadOnly {
439
		newMount.Options = append(newMount.Options, "rw")
440
	}
441

442
	newMount.Options = append(newMount.Options, "bind")
443

444
	opts, err := parse.ValidateVolumeOpts(newMount.Options)
445
	if err != nil {
446
		return newMount, nil, err
447
	}
448
	newMount.Options = opts
449

450
	succeeded = true
451
	return newMount, targetLock, nil
452
}
453

454
func getVolumeMounts(volumes []string) (map[string]specs.Mount, error) {
455
	finalVolumeMounts := make(map[string]specs.Mount)
456

457
	for _, volume := range volumes {
458
		volumeMount, err := internalParse.Volume(volume)
459
		if err != nil {
460
			return nil, err
461
		}
462
		if _, ok := finalVolumeMounts[volumeMount.Destination]; ok {
463
			return nil, fmt.Errorf("%v: %w", volumeMount.Destination, errDuplicateDest)
464
		}
465
		finalVolumeMounts[volumeMount.Destination] = volumeMount
466
	}
467
	return finalVolumeMounts, nil
468
}
469

470
// UnlockLockArray is a helper for cleaning up after GetVolumes and the like.
471
func UnlockLockArray(locks []*lockfile.LockFile) {
472
	for _, lock := range locks {
473
		lock.Unlock()
474
	}
475
}
476

477
// GetVolumes gets the volumes from --volume and --mount
478
//
479
// If this function succeeds, the caller must unlock the returned *lockfile.LockFile s if any (when??).
480
func GetVolumes(ctx *types.SystemContext, store storage.Store, volumes []string, mounts []string, contextDir string, workDir string) ([]specs.Mount, []string, []*lockfile.LockFile, error) {
481
	unifiedMounts, mountedImages, targetLocks, err := getMounts(ctx, store, mounts, contextDir, workDir)
482
	if err != nil {
483
		return nil, mountedImages, nil, err
484
	}
485
	succeeded := false
486
	defer func() {
487
		if !succeeded {
488
			UnlockLockArray(targetLocks)
489
		}
490
	}()
491
	volumeMounts, err := getVolumeMounts(volumes)
492
	if err != nil {
493
		return nil, mountedImages, nil, err
494
	}
495
	for dest, mount := range volumeMounts {
496
		if _, ok := unifiedMounts[dest]; ok {
497
			return nil, mountedImages, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest)
498
		}
499
		unifiedMounts[dest] = mount
500
	}
501

502
	finalMounts := make([]specs.Mount, 0, len(unifiedMounts))
503
	for _, mount := range unifiedMounts {
504
		finalMounts = append(finalMounts, mount)
505
	}
506
	succeeded = true
507
	return finalMounts, mountedImages, targetLocks, nil
508
}
509

510
// getMounts takes user-provided input from the --mount flag and creates OCI
511
// spec mounts.
512
// buildah run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
513
// buildah run --mount type=tmpfs,target=/dev/shm ...
514
//
515
// If this function succeeds, the caller must unlock the returned *lockfile.LockFile s if any (when??).
516
func getMounts(ctx *types.SystemContext, store storage.Store, mounts []string, contextDir string, workDir string) (map[string]specs.Mount, []string, []*lockfile.LockFile, error) {
517
	// If `type` is not set default to "bind"
518
	mountType := define.TypeBind
519
	finalMounts := make(map[string]specs.Mount)
520
	mountedImages := make([]string, 0)
521
	targetLocks := make([]*lockfile.LockFile, 0)
522
	succeeded := false
523
	defer func() {
524
		if !succeeded {
525
			UnlockLockArray(targetLocks)
526
		}
527
	}()
528

529
	errInvalidSyntax := errors.New("incorrect mount format: should be --mount type=<bind|tmpfs>,[src=<host-dir>,]target=<ctr-dir>[,options]")
530

531
	// TODO(vrothberg): the manual parsing can be replaced with a regular expression
532
	//                  to allow a more robust parsing of the mount format and to give
533
	//                  precise errors regarding supported format versus supported options.
534
	for _, mount := range mounts {
535
		tokens := strings.Split(mount, ",")
536
		if len(tokens) < 2 {
537
			return nil, mountedImages, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax)
538
		}
539
		for _, field := range tokens {
540
			if strings.HasPrefix(field, "type=") {
541
				kv := strings.Split(field, "=")
542
				if len(kv) != 2 {
543
					return nil, mountedImages, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax)
544
				}
545
				mountType = kv[1]
546
			}
547
		}
548
		switch mountType {
549
		case define.TypeBind:
550
			mount, image, err := GetBindMount(ctx, tokens, contextDir, store, "", nil, workDir)
551
			if err != nil {
552
				return nil, mountedImages, nil, err
553
			}
554
			if _, ok := finalMounts[mount.Destination]; ok {
555
				return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
556
			}
557
			finalMounts[mount.Destination] = mount
558
			mountedImages = append(mountedImages, image)
559
		case TypeCache:
560
			mount, tl, err := GetCacheMount(tokens, store, "", nil, workDir)
561
			if err != nil {
562
				return nil, mountedImages, nil, err
563
			}
564
			if tl != nil {
565
				targetLocks = append(targetLocks, tl)
566
			}
567
			if _, ok := finalMounts[mount.Destination]; ok {
568
				return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
569
			}
570
			finalMounts[mount.Destination] = mount
571
		case TypeTmpfs:
572
			mount, err := GetTmpfsMount(tokens)
573
			if err != nil {
574
				return nil, mountedImages, nil, err
575
			}
576
			if _, ok := finalMounts[mount.Destination]; ok {
577
				return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
578
			}
579
			finalMounts[mount.Destination] = mount
580
		default:
581
			return nil, mountedImages, nil, fmt.Errorf("invalid filesystem type %q", mountType)
582
		}
583
	}
584

585
	succeeded = true
586
	return finalMounts, mountedImages, targetLocks, nil
587
}
588

589
// GetTmpfsMount parses a single tmpfs mount entry from the --mount flag
590
func GetTmpfsMount(args []string) (specs.Mount, error) {
591
	newMount := specs.Mount{
592
		Type:   TypeTmpfs,
593
		Source: TypeTmpfs,
594
	}
595

596
	setDest := false
597

598
	for _, val := range args {
599
		argName, argValue, hasArgValue := strings.Cut(val, "=")
600
		switch argName {
601
		case "type":
602
			// This is already processed
603
			continue
604
		case "ro", "nosuid", "nodev", "noexec":
605
			newMount.Options = append(newMount.Options, argName)
606
		case "readonly":
607
			// Alias for "ro"
608
			newMount.Options = append(newMount.Options, "ro")
609
		case "tmpcopyup":
610
			// the path that is shadowed by the tmpfs mount is recursively copied up to the tmpfs itself.
611
			newMount.Options = append(newMount.Options, argName)
612
		case "tmpfs-mode":
613
			if !hasArgValue {
614
				return newMount, fmt.Errorf("%v: %w", argName, errBadOptionArg)
615
			}
616
			newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", argValue))
617
		case "tmpfs-size":
618
			if !hasArgValue {
619
				return newMount, fmt.Errorf("%v: %w", argName, errBadOptionArg)
620
			}
621
			newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", argValue))
622
		case "src", "source":
623
			return newMount, errors.New("source is not supported with tmpfs mounts")
624
		case "target", "dst", "destination":
625
			if !hasArgValue {
626
				return newMount, fmt.Errorf("%v: %w", argName, errBadOptionArg)
627
			}
628
			if err := parse.ValidateVolumeCtrDir(argValue); err != nil {
629
				return newMount, err
630
			}
631
			newMount.Destination = argValue
632
			setDest = true
633
		default:
634
			return newMount, fmt.Errorf("%v: %w", argName, errBadMntOption)
635
		}
636
	}
637

638
	if !setDest {
639
		return newMount, errBadVolDest
640
	}
641

642
	return newMount, nil
643
}
644

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.