podman

Форк
0
627 строк · 18.9 Кб
1
//go:build freebsd
2
// +build freebsd
3

4
package buildah
5

6
import (
7
	"errors"
8
	"fmt"
9
	"os"
10
	"path/filepath"
11
	"strings"
12
	"unsafe"
13

14
	"github.com/containers/buildah/bind"
15
	"github.com/containers/buildah/chroot"
16
	"github.com/containers/buildah/copier"
17
	"github.com/containers/buildah/define"
18
	"github.com/containers/buildah/internal"
19
	"github.com/containers/buildah/internal/tmpdir"
20
	"github.com/containers/buildah/pkg/jail"
21
	"github.com/containers/buildah/pkg/overlay"
22
	"github.com/containers/buildah/pkg/parse"
23
	butil "github.com/containers/buildah/pkg/util"
24
	"github.com/containers/buildah/util"
25
	"github.com/containers/common/libnetwork/etchosts"
26
	"github.com/containers/common/libnetwork/resolvconf"
27
	nettypes "github.com/containers/common/libnetwork/types"
28
	netUtil "github.com/containers/common/libnetwork/util"
29
	"github.com/containers/common/pkg/config"
30
	"github.com/containers/storage/pkg/idtools"
31
	"github.com/containers/storage/pkg/lockfile"
32
	"github.com/containers/storage/pkg/stringid"
33
	"github.com/docker/go-units"
34
	"github.com/opencontainers/runtime-spec/specs-go"
35
	spec "github.com/opencontainers/runtime-spec/specs-go"
36
	"github.com/opencontainers/runtime-tools/generate"
37
	"github.com/sirupsen/logrus"
38
	"golang.org/x/exp/slices"
39
	"golang.org/x/sys/unix"
40
)
41

42
const (
43
	P_PID             = 0
44
	P_PGID            = 2
45
	PROC_REAP_ACQUIRE = 2
46
	PROC_REAP_RELEASE = 3
47
)
48

49
var (
50
	// We dont want to remove destinations with /etc, /dev as
51
	// rootfs already contains these files and unionfs will create
52
	// a `whiteout` i.e `.wh` files on removal of overlapping
53
	// files from these directories.  everything other than these
54
	// will be cleaned up
55
	nonCleanablePrefixes = []string{
56
		"/etc", "/dev",
57
	}
58
)
59

60
func procctl(idtype int, id int, cmd int, arg *byte) error {
61
	_, _, e1 := unix.Syscall6(
62
		unix.SYS_PROCCTL, uintptr(idtype), uintptr(id),
63
		uintptr(cmd), uintptr(unsafe.Pointer(arg)), 0, 0)
64
	if e1 != 0 {
65
		return unix.Errno(e1)
66
	}
67
	return nil
68
}
69

70
func setChildProcess() error {
71
	if err := procctl(P_PID, unix.Getpid(), PROC_REAP_ACQUIRE, nil); err != nil {
72
		fmt.Fprintf(os.Stderr, "procctl(PROC_REAP_ACQUIRE): %v\n", err)
73
		return err
74
	}
75
	return nil
76
}
77

78
func (b *Builder) Run(command []string, options RunOptions) error {
79
	p, err := os.MkdirTemp(tmpdir.GetTempDir(), define.Package)
80
	if err != nil {
81
		return err
82
	}
83
	// On some hosts like AH, /tmp is a symlink and we need an
84
	// absolute path.
85
	path, err := filepath.EvalSymlinks(p)
86
	if err != nil {
87
		return err
88
	}
89
	logrus.Debugf("using %q to hold bundle data", path)
90
	defer func() {
91
		if err2 := os.RemoveAll(path); err2 != nil {
92
			logrus.Errorf("error removing %q: %v", path, err2)
93
		}
94
	}()
95

96
	gp, err := generate.New("freebsd")
97
	if err != nil {
98
		return fmt.Errorf("generating new 'freebsd' runtime spec: %w", err)
99
	}
100
	g := &gp
101

102
	isolation := options.Isolation
103
	if isolation == IsolationDefault {
104
		isolation = b.Isolation
105
		if isolation == IsolationDefault {
106
			isolation, err = parse.IsolationOption("")
107
			if err != nil {
108
				logrus.Debugf("got %v while trying to determine default isolation, guessing OCI", err)
109
				isolation = IsolationOCI
110
			} else if isolation == IsolationDefault {
111
				isolation = IsolationOCI
112
			}
113
		}
114
	}
115
	if err := checkAndOverrideIsolationOptions(isolation, &options); err != nil {
116
		return err
117
	}
118

119
	// hardwire the environment to match docker build to avoid subtle and hard-to-debug differences due to containers.conf
120
	b.configureEnvironment(g, options, []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
121

122
	if b.CommonBuildOpts == nil {
123
		return fmt.Errorf("invalid format on container you must recreate the container")
124
	}
125

126
	if err := addCommonOptsToSpec(b.CommonBuildOpts, g); err != nil {
127
		return err
128
	}
129

130
	if options.WorkingDir != "" {
131
		g.SetProcessCwd(options.WorkingDir)
132
	} else if b.WorkDir() != "" {
133
		g.SetProcessCwd(b.WorkDir())
134
	}
135
	mountPoint, err := b.Mount(b.MountLabel)
136
	if err != nil {
137
		return fmt.Errorf("mounting container %q: %w", b.ContainerID, err)
138
	}
139
	defer func() {
140
		if err := b.Unmount(); err != nil {
141
			logrus.Errorf("error unmounting container: %v", err)
142
		}
143
	}()
144
	g.SetRootPath(mountPoint)
145
	if len(command) > 0 {
146
		command = runLookupPath(g, command)
147
		g.SetProcessArgs(command)
148
	} else {
149
		g.SetProcessArgs(nil)
150
	}
151

152
	setupTerminal(g, options.Terminal, options.TerminalSize)
153

154
	configureNetwork, networkString, err := b.configureNamespaces(g, &options)
155
	if err != nil {
156
		return err
157
	}
158

159
	containerName := Package + "-" + filepath.Base(path)
160
	if configureNetwork {
161
		if jail.NeedVnetJail() {
162
			g.AddAnnotation("org.freebsd.parentJail", containerName+"-vnet")
163
		} else {
164
			g.AddAnnotation("org.freebsd.jail.vnet", "new")
165
		}
166
	}
167

168
	homeDir, err := b.configureUIDGID(g, mountPoint, options)
169
	if err != nil {
170
		return err
171
	}
172

173
	// Now grab the spec from the generator.  Set the generator to nil so that future contributors
174
	// will quickly be able to tell that they're supposed to be modifying the spec directly from here.
175
	spec := g.Config
176
	g = nil
177

178
	// Set the seccomp configuration using the specified profile name.  Some syscalls are
179
	// allowed if certain capabilities are to be granted (example: CAP_SYS_CHROOT and chroot),
180
	// so we sorted out the capabilities lists first.
181
	if err = setupSeccomp(spec, b.CommonBuildOpts.SeccompProfilePath); err != nil {
182
		return err
183
	}
184

185
	uid, gid := spec.Process.User.UID, spec.Process.User.GID
186
	idPair := &idtools.IDPair{UID: int(uid), GID: int(gid)}
187

188
	mode := os.FileMode(0755)
189
	coptions := copier.MkdirOptions{
190
		ChownNew: idPair,
191
		ChmodNew: &mode,
192
	}
193
	if err := copier.Mkdir(mountPoint, filepath.Join(mountPoint, spec.Process.Cwd), coptions); err != nil {
194
		return err
195
	}
196

197
	bindFiles := make(map[string]string)
198
	volumes := b.Volumes()
199

200
	// Figure out who owns files that will appear to be owned by UID/GID 0 in the container.
201
	rootUID, rootGID, err := util.GetHostRootIDs(spec)
202
	if err != nil {
203
		return err
204
	}
205
	rootIDPair := &idtools.IDPair{UID: int(rootUID), GID: int(rootGID)}
206

207
	hostsFile := ""
208
	if !options.NoHosts && !slices.Contains(volumes, config.DefaultHostsFile) && options.ConfigureNetwork != define.NetworkDisabled {
209
		hostsFile, err = b.createHostsFile(path, rootIDPair)
210
		if err != nil {
211
			return err
212
		}
213
		bindFiles[config.DefaultHostsFile] = hostsFile
214

215
		// Only add entries here if we do not have to setup network,
216
		// if we do we have to do it much later after the network setup.
217
		if !configureNetwork {
218
			var entries etchosts.HostEntries
219
			// add host entry for local ip when running in host network
220
			if spec.Hostname != "" {
221
				ip := netUtil.GetLocalIP()
222
				if ip != "" {
223
					entries = append(entries, etchosts.HostEntry{
224
						Names: []string{spec.Hostname},
225
						IP:    ip,
226
					})
227
				}
228
			}
229
			err = b.addHostsEntries(hostsFile, mountPoint, entries, nil)
230
			if err != nil {
231
				return err
232
			}
233
		}
234
	}
235

236
	resolvFile := ""
237
	if !slices.Contains(volumes, resolvconf.DefaultResolvConf) && options.ConfigureNetwork != define.NetworkDisabled && !(len(b.CommonBuildOpts.DNSServers) == 1 && strings.ToLower(b.CommonBuildOpts.DNSServers[0]) == "none") {
238
		resolvFile, err = b.createResolvConf(path, rootIDPair)
239
		if err != nil {
240
			return err
241
		}
242
		bindFiles[resolvconf.DefaultResolvConf] = resolvFile
243

244
		// Only add entries here if we do not have to do setup network,
245
		// if we do we have to do it much later after the network setup.
246
		if !configureNetwork {
247
			err = b.addResolvConfEntries(resolvFile, nil, nil, false, true)
248
			if err != nil {
249
				return err
250
			}
251
		}
252
	}
253

254
	runMountInfo := runMountInfo{
255
		ContextDir:       options.ContextDir,
256
		Secrets:          options.Secrets,
257
		SSHSources:       options.SSHSources,
258
		StageMountPoints: options.StageMountPoints,
259
		SystemContext:    options.SystemContext,
260
	}
261

262
	runArtifacts, err := b.setupMounts(mountPoint, spec, path, options.Mounts, bindFiles, volumes, b.CommonBuildOpts.Volumes, options.RunMounts, runMountInfo)
263
	if err != nil {
264
		return fmt.Errorf("resolving mountpoints for container %q: %w", b.ContainerID, err)
265
	}
266
	if runArtifacts.SSHAuthSock != "" {
267
		sshenv := "SSH_AUTH_SOCK=" + runArtifacts.SSHAuthSock
268
		spec.Process.Env = append(spec.Process.Env, sshenv)
269
	}
270

271
	// following run was called from `buildah run`
272
	// and some images were mounted for this run
273
	// add them to cleanup artifacts
274
	if len(options.ExternalImageMounts) > 0 {
275
		runArtifacts.MountedImages = append(runArtifacts.MountedImages, options.ExternalImageMounts...)
276
	}
277

278
	defer func() {
279
		if err := b.cleanupRunMounts(options.SystemContext, mountPoint, runArtifacts); err != nil {
280
			options.Logger.Errorf("unable to cleanup run mounts %v", err)
281
		}
282
	}()
283

284
	defer b.cleanupTempVolumes()
285

286
	// If we are creating a network, make the vnet here so that we can
287
	// execute the OCI runtime inside it. For FreeBSD-13.3 and later, we can
288
	// configure the container network settings from outside the jail, which
289
	// removes the need for a separate jail to manage the vnet.
290
	if configureNetwork && jail.NeedVnetJail() {
291
		mynetns := containerName + "-vnet"
292

293
		jconf := jail.NewConfig()
294
		jconf.Set("name", mynetns)
295
		jconf.Set("vnet", jail.NEW)
296
		jconf.Set("children.max", 1)
297
		jconf.Set("persist", true)
298
		jconf.Set("enforce_statfs", 0)
299
		jconf.Set("devfs_ruleset", 4)
300
		jconf.Set("allow.raw_sockets", true)
301
		jconf.Set("allow.chflags", true)
302
		jconf.Set("securelevel", -1)
303
		netjail, err := jail.Create(jconf)
304
		if err != nil {
305
			return err
306
		}
307
		defer func() {
308
			jconf := jail.NewConfig()
309
			jconf.Set("persist", false)
310
			err2 := netjail.Set(jconf)
311
			if err2 != nil {
312
				logrus.Errorf("error releasing vnet jail %q: %v", mynetns, err2)
313
			}
314
		}()
315
	}
316

317
	switch isolation {
318
	case IsolationOCI:
319
		var moreCreateArgs []string
320
		if options.NoPivot {
321
			moreCreateArgs = []string{"--no-pivot"}
322
		} else {
323
			moreCreateArgs = nil
324
		}
325
		err = b.runUsingRuntimeSubproc(isolation, options, configureNetwork, networkString, moreCreateArgs, spec, mountPoint, path, containerName, b.Container, hostsFile, resolvFile)
326
	case IsolationChroot:
327
		err = chroot.RunUsingChroot(spec, path, homeDir, options.Stdin, options.Stdout, options.Stderr)
328
	default:
329
		err = errors.New("don't know how to run this command")
330
	}
331
	return err
332
}
333

334
func addCommonOptsToSpec(commonOpts *define.CommonBuildOptions, g *generate.Generator) error {
335
	defaultContainerConfig, err := config.Default()
336
	if err != nil {
337
		return fmt.Errorf("failed to get container config: %w", err)
338
	}
339
	// Other process resource limits
340
	if err := addRlimits(commonOpts.Ulimit, g, defaultContainerConfig.Containers.DefaultUlimits.Get()); err != nil {
341
		return err
342
	}
343

344
	logrus.Debugf("Resources: %#v", commonOpts)
345
	return nil
346
}
347

348
// setupSpecialMountSpecChanges creates special mounts for depending
349
// on the namespaces - nothing yet for freebsd
350
func setupSpecialMountSpecChanges(spec *spec.Spec, shmSize string) ([]specs.Mount, error) {
351
	return spec.Mounts, nil
352
}
353

354
// If this function succeeds and returns a non-nil *lockfile.LockFile, the caller must unlock it (when??).
355
func (b *Builder) getCacheMount(tokens []string, stageMountPoints map[string]internal.StageMountDetails, idMaps IDMaps, workDir string) (*spec.Mount, *lockfile.LockFile, error) {
356
	return nil, nil, errors.New("cache mounts not supported on freebsd")
357
}
358

359
func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount, idMaps IDMaps) (mounts []specs.Mount, Err error) {
360
	// Make sure the overlay directory is clean before running
361
	_, err := b.store.ContainerDirectory(b.ContainerID)
362
	if err != nil {
363
		return nil, fmt.Errorf("looking up container directory for %s: %w", b.ContainerID, err)
364
	}
365

366
	parseMount := func(mountType, host, container string, options []string) (specs.Mount, error) {
367
		var foundrw, foundro, foundO bool
368
		var upperDir string
369
		for _, opt := range options {
370
			switch opt {
371
			case "rw":
372
				foundrw = true
373
			case "ro":
374
				foundro = true
375
			case "O":
376
				foundO = true
377
			}
378
			if strings.HasPrefix(opt, "upperdir") {
379
				splitOpt := strings.SplitN(opt, "=", 2)
380
				if len(splitOpt) > 1 {
381
					upperDir = splitOpt[1]
382
				}
383
			}
384
		}
385
		if !foundrw && !foundro {
386
			options = append(options, "rw")
387
		}
388
		if mountType == "bind" || mountType == "rbind" {
389
			mountType = "nullfs"
390
		}
391
		if foundO {
392
			containerDir, err := b.store.ContainerDirectory(b.ContainerID)
393
			if err != nil {
394
				return specs.Mount{}, err
395
			}
396

397
			contentDir, err := overlay.TempDir(containerDir, idMaps.rootUID, idMaps.rootGID)
398
			if err != nil {
399
				return specs.Mount{}, fmt.Errorf("failed to create TempDir in the %s directory: %w", containerDir, err)
400
			}
401

402
			overlayOpts := overlay.Options{
403
				RootUID:                idMaps.rootUID,
404
				RootGID:                idMaps.rootGID,
405
				UpperDirOptionFragment: upperDir,
406
				GraphOpts:              b.store.GraphOptions(),
407
			}
408

409
			overlayMount, err := overlay.MountWithOptions(contentDir, host, container, &overlayOpts)
410
			if err == nil {
411
				b.TempVolumes[contentDir] = true
412
			}
413
			return overlayMount, err
414
		}
415
		return specs.Mount{
416
			Destination: container,
417
			Type:        mountType,
418
			Source:      host,
419
			Options:     options,
420
		}, nil
421
	}
422

423
	// Bind mount volumes specified for this particular Run() invocation
424
	for _, i := range optionMounts {
425
		logrus.Debugf("setting up mounted volume at %q", i.Destination)
426
		mount, err := parseMount(i.Type, i.Source, i.Destination, i.Options)
427
		if err != nil {
428
			return nil, err
429
		}
430
		mounts = append(mounts, mount)
431
	}
432
	// Bind mount volumes given by the user when the container was created
433
	for _, i := range volumeMounts {
434
		var options []string
435
		spliti := strings.Split(i, ":")
436
		if len(spliti) > 2 {
437
			options = strings.Split(spliti[2], ",")
438
		}
439
		mount, err := parseMount("nullfs", spliti[0], spliti[1], options)
440
		if err != nil {
441
			return nil, err
442
		}
443
		mounts = append(mounts, mount)
444
	}
445
	return mounts, nil
446
}
447

448
func setupCapabilities(g *generate.Generator, defaultCapabilities, adds, drops []string) error {
449
	return nil
450
}
451

452
func (b *Builder) runConfigureNetwork(pid int, isolation define.Isolation, options RunOptions, networkString string, containerName string, hostnames []string) (func(), *netResult, error) {
453
	//if isolation == IsolationOCIRootless {
454
	//return setupRootlessNetwork(pid)
455
	//}
456

457
	var configureNetworks []string
458
	if len(networkString) > 0 {
459
		configureNetworks = strings.Split(networkString, ",")
460
	}
461

462
	if len(configureNetworks) == 0 {
463
		configureNetworks = []string{b.NetworkInterface.DefaultNetworkName()}
464
	}
465
	logrus.Debugf("configureNetworks: %v", configureNetworks)
466

467
	var mynetns string
468
	if jail.NeedVnetJail() {
469
		mynetns = containerName + "-vnet"
470
	} else {
471
		mynetns = containerName
472
	}
473

474
	networks := make(map[string]nettypes.PerNetworkOptions, len(configureNetworks))
475
	for i, network := range configureNetworks {
476
		networks[network] = nettypes.PerNetworkOptions{
477
			InterfaceName: fmt.Sprintf("eth%d", i),
478
		}
479
	}
480

481
	opts := nettypes.NetworkOptions{
482
		ContainerID:   containerName,
483
		ContainerName: containerName,
484
		Networks:      networks,
485
	}
486
	netStatus, err := b.NetworkInterface.Setup(mynetns, nettypes.SetupOptions{NetworkOptions: opts})
487
	if err != nil {
488
		return nil, nil, err
489
	}
490

491
	teardown := func() {
492
		err := b.NetworkInterface.Teardown(mynetns, nettypes.TeardownOptions{NetworkOptions: opts})
493
		if err != nil {
494
			logrus.Errorf("failed to cleanup network: %v", err)
495
		}
496
	}
497

498
	return teardown, netStatusToNetResult(netStatus, hostnames), nil
499
}
500

501
func setupNamespaces(logger *logrus.Logger, g *generate.Generator, namespaceOptions define.NamespaceOptions, idmapOptions define.IDMappingOptions, policy define.NetworkConfigurationPolicy) (configureNetwork bool, networkString string, configureUTS bool, err error) {
502
	// Set namespace options in the container configuration.
503
	for _, namespaceOption := range namespaceOptions {
504
		switch namespaceOption.Name {
505
		case string(specs.NetworkNamespace):
506
			configureNetwork = false
507
			if !namespaceOption.Host && (namespaceOption.Path == "" || !filepath.IsAbs(namespaceOption.Path)) {
508
				if namespaceOption.Path != "" && !filepath.IsAbs(namespaceOption.Path) {
509
					networkString = namespaceOption.Path
510
					namespaceOption.Path = ""
511
				}
512
				configureNetwork = (policy != define.NetworkDisabled)
513
			}
514
		case string(specs.UTSNamespace):
515
			configureUTS = false
516
			if !namespaceOption.Host && namespaceOption.Path == "" {
517
				configureUTS = true
518
			}
519
		}
520
		// TODO: re-visit this when there is consensus on a
521
		// FreeBSD runtime-spec. FreeBSD jails have rough
522
		// equivalents for UTS and and network namespaces.
523
	}
524

525
	return configureNetwork, networkString, configureUTS, nil
526
}
527

528
func (b *Builder) configureNamespaces(g *generate.Generator, options *RunOptions) (bool, string, error) {
529
	defaultNamespaceOptions, err := DefaultNamespaceOptions()
530
	if err != nil {
531
		return false, "", err
532
	}
533

534
	namespaceOptions := defaultNamespaceOptions
535
	namespaceOptions.AddOrReplace(b.NamespaceOptions...)
536
	namespaceOptions.AddOrReplace(options.NamespaceOptions...)
537

538
	networkPolicy := options.ConfigureNetwork
539
	//Nothing was specified explicitly so network policy should be inherited from builder
540
	if networkPolicy == NetworkDefault {
541
		networkPolicy = b.ConfigureNetwork
542

543
		// If builder policy was NetworkDisabled and
544
		// we want to disable network for this run.
545
		// reset options.ConfigureNetwork to NetworkDisabled
546
		// since it will be treated as source of truth later.
547
		if networkPolicy == NetworkDisabled {
548
			options.ConfigureNetwork = networkPolicy
549
		}
550
	}
551

552
	configureNetwork, networkString, configureUTS, err := setupNamespaces(options.Logger, g, namespaceOptions, b.IDMappingOptions, networkPolicy)
553
	if err != nil {
554
		return false, "", err
555
	}
556

557
	if configureUTS {
558
		if options.Hostname != "" {
559
			g.SetHostname(options.Hostname)
560
		} else if b.Hostname() != "" {
561
			g.SetHostname(b.Hostname())
562
		} else {
563
			g.SetHostname(stringid.TruncateID(b.ContainerID))
564
		}
565
	} else {
566
		g.SetHostname("")
567
	}
568

569
	found := false
570
	spec := g.Config
571
	for i := range spec.Process.Env {
572
		if strings.HasPrefix(spec.Process.Env[i], "HOSTNAME=") {
573
			found = true
574
			break
575
		}
576
	}
577
	if !found {
578
		spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("HOSTNAME=%s", spec.Hostname))
579
	}
580

581
	return configureNetwork, networkString, nil
582
}
583

584
func runSetupBoundFiles(bundlePath string, bindFiles map[string]string) (mounts []specs.Mount) {
585
	for dest, src := range bindFiles {
586
		options := []string{}
587
		if strings.HasPrefix(src, bundlePath) {
588
			options = append(options, bind.NoBindOption)
589
		}
590
		mounts = append(mounts, specs.Mount{
591
			Source:      src,
592
			Destination: dest,
593
			Type:        "nullfs",
594
			Options:     options,
595
		})
596
	}
597
	return mounts
598
}
599

600
func addRlimits(ulimit []string, g *generate.Generator, defaultUlimits []string) error {
601
	var (
602
		ul  *units.Ulimit
603
		err error
604
	)
605

606
	ulimit = append(defaultUlimits, ulimit...)
607
	for _, u := range ulimit {
608
		if ul, err = butil.ParseUlimit(u); err != nil {
609
			return fmt.Errorf("ulimit option %q requires name=SOFT:HARD, failed to be parsed: %w", u, err)
610
		}
611

612
		g.AddProcessRlimits("RLIMIT_"+strings.ToUpper(ul.Name), uint64(ul.Hard), uint64(ul.Soft))
613
	}
614
	return nil
615
}
616

617
// Create pipes to use for relaying stdio.
618
func runMakeStdioPipe(uid, gid int) ([][]int, error) {
619
	stdioPipe := make([][]int, 3)
620
	for i := range stdioPipe {
621
		stdioPipe[i] = make([]int, 2)
622
		if err := unix.Pipe(stdioPipe[i]); err != nil {
623
			return nil, fmt.Errorf("creating pipe for container FD %d: %w", i, err)
624
		}
625
	}
626
	return stdioPipe, nil
627
}
628

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

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

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

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