summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile1
-rw-r--r--OWNERS2
-rw-r--r--docs/source/markdown/containers-mounts.conf.5.md2
-rw-r--r--docs/tutorials/README.md2
-rw-r--r--nix/default.nix2
-rw-r--r--pkg/specgen/generate/config_linux.go5
-rw-r--r--pkg/specgen/generate/security.go10
-rw-r--r--test/apiv2/rest_api/__init__.py132
-rw-r--r--test/apiv2/rest_api/test_rest_v2_0_0.py82
-rw-r--r--test/apiv2/rest_api/v1_test_rest_v1_0_0.py (renamed from test/apiv2/rest_api/test_rest_v1_0_0.py)52
-rw-r--r--test/e2e/common_test.go8
-rw-r--r--test/e2e/play_kube_test.go2
-rw-r--r--test/e2e/run_test.go33
-rw-r--r--test/e2e/toolbox_test.go15
14 files changed, 273 insertions, 75 deletions
diff --git a/Makefile b/Makefile
index 89bf8707e..5cfed666a 100644
--- a/Makefile
+++ b/Makefile
@@ -357,6 +357,7 @@ remotesystem:
.PHONY: localapiv2
localapiv2:
env PODMAN=./bin/podman ./test/apiv2/test-apiv2
+ env PODMAN=./bin/podman ${PYTHON} -m unittest discover -v ./test/apiv2/rest_api/
.PHONY: remoteapiv2
remoteapiv2:
diff --git a/OWNERS b/OWNERS
index 6d0cb3e25..7cbef7f37 100644
--- a/OWNERS
+++ b/OWNERS
@@ -7,6 +7,7 @@ approvers:
- rhatdan
- TomSweeneyRedHat
- vrothberg
+ - umohnani8
reviewers:
- baude
- edsantiago
@@ -18,3 +19,4 @@ reviewers:
- vrothberg
- ashley-cui
- QiWang19
+ - umohnani8
diff --git a/docs/source/markdown/containers-mounts.conf.5.md b/docs/source/markdown/containers-mounts.conf.5.md
index 130c1c523..74492c831 100644
--- a/docs/source/markdown/containers-mounts.conf.5.md
+++ b/docs/source/markdown/containers-mounts.conf.5.md
@@ -10,7 +10,7 @@ The mounts.conf file specifies volume mount directories that are automatically m
The format of the mounts.conf is the volume format `/SRC:/DEST`, one mount per line. For example, a mounts.conf with the line `/usr/share/secrets:/run/secrets` would cause the contents of the `/usr/share/secrets` directory on the host to be mounted on the `/run/secrets` directory inside the container. Setting mountpoints allows containers to use the files of the host, for instance, to use the host's subscription to some enterprise Linux distribution.
## FILES
-Some distributions may provide a `/usr/share/containers/mounts.conf` file to provide default mounts, but users can create a `/etc/containers/mounts.conf`, to specify their own special volumes to mount in the container.
+Some distributions may provide a `/usr/share/containers/mounts.conf` file to provide default mounts, but users can create a `/etc/containers/mounts.conf`, to specify their own special volumes to mount in the container. When Podman runs in rootless mode, the file `$HOME/.config/containers/mounts.conf` will override the default if it exists.
## HISTORY
Aug 2018, Originally compiled by Valentin Rothberg <vrothberg@suse.com>
diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md
index 7f7b4853d..246d235ee 100644
--- a/docs/tutorials/README.md
+++ b/docs/tutorials/README.md
@@ -12,7 +12,7 @@ Learn how to setup Podman and perform some basic commands with the utility.
The steps required to setup rootless Podman are enumerated.
-**[Setup Mac/Windows](mac_win_client.md)
+**[Setup Mac/Windows](mac_win_client.md)**
Special setup for running the Podman remote client on a Mac or Windows PC and connecting to Podman running on a Linux VM are documented.
diff --git a/nix/default.nix b/nix/default.nix
index cc8786ce0..a1a8c5287 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -44,7 +44,7 @@ let
export CFLAGS='-static'
export LDFLAGS='-s -w -static-libgcc -static'
export EXTRA_LDFLAGS='-s -w -linkmode external -extldflags "-static -lm"'
- export BUILDTAGS='static netgo exclude_graphdriver_btrfs exclude_graphdriver_devicemapper seccomp apparmor selinux'
+ export BUILDTAGS='static netgo osusergo exclude_graphdriver_btrfs exclude_graphdriver_devicemapper seccomp apparmor selinux'
'';
buildPhase = ''
patchShebangs .
diff --git a/pkg/specgen/generate/config_linux.go b/pkg/specgen/generate/config_linux.go
index fcb7641d2..2d40dba8f 100644
--- a/pkg/specgen/generate/config_linux.go
+++ b/pkg/specgen/generate/config_linux.go
@@ -350,3 +350,8 @@ func deviceFromPath(path string) (*spec.LinuxDevice, error) {
Minor: int64(unix.Minor(devNumber)),
}, nil
}
+
+func supportAmbientCapabilities() bool {
+ err := unix.Prctl(unix.PR_CAP_AMBIENT, unix.PR_CAP_AMBIENT_IS_SET, 0, 0, 0)
+ return err == nil
+}
diff --git a/pkg/specgen/generate/security.go b/pkg/specgen/generate/security.go
index d17cd4a9a..dee140282 100644
--- a/pkg/specgen/generate/security.go
+++ b/pkg/specgen/generate/security.go
@@ -135,7 +135,9 @@ func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator,
configSpec.Process.Capabilities.Bounding = caplist
configSpec.Process.Capabilities.Inheritable = caplist
- if s.User == "" || s.User == "root" || s.User == "0" {
+ user := strings.Split(s.User, ":")[0]
+
+ if (user == "" && s.UserNS.NSMode != specgen.KeepID) || user == "root" || user == "0" {
configSpec.Process.Capabilities.Effective = caplist
configSpec.Process.Capabilities.Permitted = caplist
} else {
@@ -145,6 +147,12 @@ func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator,
}
configSpec.Process.Capabilities.Effective = userCaps
configSpec.Process.Capabilities.Permitted = userCaps
+
+ // Ambient capabilities were added to Linux 4.3. Set ambient
+ // capabilities only when the kernel supports them.
+ if supportAmbientCapabilities() {
+ configSpec.Process.Capabilities.Ambient = userCaps
+ }
}
g.SetProcessNoNewPrivileges(s.NoNewPrivileges)
diff --git a/test/apiv2/rest_api/__init__.py b/test/apiv2/rest_api/__init__.py
index e69de29bb..5f0777d58 100644
--- a/test/apiv2/rest_api/__init__.py
+++ b/test/apiv2/rest_api/__init__.py
@@ -0,0 +1,132 @@
+import configparser
+import json
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+
+class Podman(object):
+ """
+ Instances hold the configuration and setup for running podman commands
+ """
+
+ def __init__(self):
+ """Initialize a Podman instance with global options"""
+ binary = os.getenv("PODMAN", "bin/podman")
+ self.cmd = [binary, "--storage-driver=vfs"]
+
+ cgroupfs = os.getenv("CGROUP_MANAGER", "cgroupfs")
+ self.cmd.append(f"--cgroup-manager={cgroupfs}")
+
+ if os.getenv("DEBUG"):
+ self.cmd.append("--log-level=debug")
+
+ self.anchor_directory = tempfile.mkdtemp(prefix="podman_restapi_")
+ self.cmd.append("--root=" + os.path.join(self.anchor_directory, "crio"))
+ self.cmd.append("--runroot=" + os.path.join(self.anchor_directory, "crio-run"))
+
+ os.environ["REGISTRIES_CONFIG_PATH"] = os.path.join(
+ self.anchor_directory, "registry.conf"
+ )
+ p = configparser.ConfigParser()
+ p.read_dict(
+ {
+ "registries.search": {"registries": "['docker.io']"},
+ "registries.insecure": {"registries": "[]"},
+ "registries.block": {"registries": "[]"},
+ }
+ )
+ with open(os.environ["REGISTRIES_CONFIG_PATH"], "w") as w:
+ p.write(w)
+
+ os.environ["CNI_CONFIG_PATH"] = os.path.join(
+ self.anchor_directory, "cni", "net.d"
+ )
+ os.makedirs(os.environ["CNI_CONFIG_PATH"], exist_ok=True)
+ self.cmd.append("--cni-config-dir=" + os.environ["CNI_CONFIG_PATH"])
+ cni_cfg = os.path.join(
+ os.environ["CNI_CONFIG_PATH"], "87-podman-bridge.conflist"
+ )
+ # json decoded and encoded to ensure legal json
+ buf = json.loads(
+ """
+ {
+ "cniVersion": "0.3.0",
+ "name": "podman",
+ "plugins": [{
+ "type": "bridge",
+ "bridge": "cni0",
+ "isGateway": true,
+ "ipMasq": true,
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.88.0.0/16",
+ "routes": [{
+ "dst": "0.0.0.0/0"
+ }]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ }
+ ]
+ }
+ """
+ )
+ with open(cni_cfg, "w") as w:
+ json.dump(buf, w)
+
+ def open(self, command, *args, **kwargs):
+ """Podman initialized instance to run a given command
+
+ :param self: Podman instance
+ :param command: podman sub-command to run
+ :param args: arguments and options for command
+ :param kwargs: See subprocess.Popen() for shell keyword
+ :return: subprocess.Popen() instance configured to run podman instance
+ """
+ cmd = self.cmd.copy()
+ cmd.append(command)
+ cmd.extend(args)
+
+ shell = kwargs.get("shell", False)
+
+ return subprocess.Popen(
+ cmd,
+ shell=shell,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+
+ def run(self, command, *args, **kwargs):
+ """Podman initialized instance to run a given command
+
+ :param self: Podman instance
+ :param command: podman sub-command to run
+ :param args: arguments and options for command
+ :param kwargs: See subprocess.Popen() for shell and check keywords
+ :return: subprocess.Popen() instance configured to run podman instance
+ """
+ cmd = self.cmd.copy()
+ cmd.append(command)
+ cmd.extend(args)
+
+ check = kwargs.get("check", False)
+ shell = kwargs.get("shell", False)
+
+ return subprocess.run(
+ cmd,
+ shell=shell,
+ check=check,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+
+ def tear_down(self):
+ shutil.rmtree(self.anchor_directory, ignore_errors=True)
diff --git a/test/apiv2/rest_api/test_rest_v2_0_0.py b/test/apiv2/rest_api/test_rest_v2_0_0.py
index 3376f8402..5dfd1fc02 100644
--- a/test/apiv2/rest_api/test_rest_v2_0_0.py
+++ b/test/apiv2/rest_api/test_rest_v2_0_0.py
@@ -1,5 +1,4 @@
import json
-import os
import subprocess
import sys
import time
@@ -9,27 +8,25 @@ from multiprocessing import Process
import requests
from dateutil.parser import parse
+from test.apiv2.rest_api import Podman
+
PODMAN_URL = "http://localhost:8080"
def _url(path):
- return PODMAN_URL + "/v1.0.0/libpod" + path
-
-
-def podman():
- binary = os.getenv("PODMAN_BINARY")
- if binary is None:
- binary = "bin/podman"
- return binary
+ return PODMAN_URL + "/v2.0.0/libpod" + path
def ctnr(path):
- r = requests.get(_url("/containers/json?all=true"))
try:
+ r = requests.get(_url("/containers/json?all=true"))
ctnrs = json.loads(r.text)
except Exception as e:
- sys.stderr.write("Bad container response: {}/{}".format(r.text, e))
- raise e
+ msg = f"Bad container response: {e}"
+ if r is not None:
+ msg = msg + " " + r.text
+ sys.stderr.write(msg + "\n")
+ raise
return path.format(ctnrs[0]["Id"])
@@ -44,50 +41,50 @@ def validateObjectFields(buffer):
class TestApi(unittest.TestCase):
- podman = None
+ podman = None # initialized podman configuration for tests
+ service = None # podman service instance
def setUp(self):
super().setUp()
- if TestApi.podman.poll() is not None:
- sys.stderr.write(f"podman service returned {TestApi.podman.returncode}\n")
- sys.exit(2)
- requests.get(
- _url("/images/create?fromSrc=docker.io%2Falpine%3Alatest"))
- # calling out to podman is easier than the API for running a container
- subprocess.run([podman(), "run", "alpine", "/bin/ls"],
- check=True,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL)
+
+ try:
+ TestApi.podman.run("run", "alpine", "/bin/ls", check=True)
+ except subprocess.CalledProcessError as e:
+ if e.stdout:
+ sys.stdout.write("\nRun Stdout:\n" + e.stdout.decode("utf-8"))
+ if e.stderr:
+ sys.stderr.write("\nRun Stderr:\n" + e.stderr.decode("utf-8"))
+ raise
@classmethod
def setUpClass(cls):
super().setUpClass()
- TestApi.podman = subprocess.Popen(
- [
- podman(), "system", "service", "tcp:localhost:8080",
- "--log-level=debug", "--time=0"
- ],
- shell=False,
- stdin=subprocess.DEVNULL,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
+ TestApi.podman = Podman()
+ TestApi.service = TestApi.podman.open(
+ "system", "service", "tcp:localhost:8080", "--log-level=debug", "--time=0"
)
+ # give the service some time to be ready...
time.sleep(2)
+ returncode = TestApi.service.poll()
+ if returncode is not None:
+ raise subprocess.CalledProcessError(returncode, "podman system service")
+
+ r = requests.post(_url("/images/pull?reference=docker.io%2Falpine%3Alatest"))
+ if r.status_code != 200:
+ raise subprocess.CalledProcessError(
+ r.status_code, f"podman images pull docker.io/alpine:latest {r.text}"
+ )
+
@classmethod
def tearDownClass(cls):
- TestApi.podman.terminate()
- stdout, stderr = TestApi.podman.communicate(timeout=0.5)
+ TestApi.service.terminate()
+ stdout, stderr = TestApi.service.communicate(timeout=0.5)
if stdout:
- print("\nService Stdout:\n" + stdout.decode('utf-8'))
+ sys.stdout.write("\nService Stdout:\n" + stdout.decode("utf-8"))
if stderr:
- print("\nService Stderr:\n" + stderr.decode('utf-8'))
-
- if TestApi.podman.returncode > 0:
- sys.stderr.write(f"podman exited with error code {TestApi.podman.returncode}\n")
- sys.exit(2)
-
+ sys.stderr.write("\nService Stderr:\n" + stderr.decode("utf-8"))
return super().tearDownClass()
def test_info(self):
@@ -160,6 +157,7 @@ class TestApi(unittest.TestCase):
self.assertIsNone(r.text)
def test_attach_containers(self):
+ self.skipTest("FIXME: Test timeouts")
r = requests.post(_url(ctnr("/containers/{}/attach")), timeout=5)
self.assertIn(r.status_code, (101, 500), r.text)
@@ -242,5 +240,5 @@ class TestApi(unittest.TestCase):
self.assertEqual(r.status_code, 200, r.text)
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/test/apiv2/rest_api/test_rest_v1_0_0.py b/test/apiv2/rest_api/v1_test_rest_v1_0_0.py
index 2e574e015..acd6273ef 100644
--- a/test/apiv2/rest_api/test_rest_v1_0_0.py
+++ b/test/apiv2/rest_api/v1_test_rest_v1_0_0.py
@@ -43,16 +43,16 @@ class TestApi(unittest.TestCase):
def setUp(self):
super().setUp()
if TestApi.podman.poll() is not None:
- sys.stderr.write("podman service returned {}",
- TestApi.podman.returncode)
+ sys.stderr.write("podman service returned {}", TestApi.podman.returncode)
sys.exit(2)
- requests.get(
- _url("/images/create?fromSrc=docker.io%2Falpine%3Alatest"))
+ requests.get(_url("/images/create?fromSrc=docker.io%2Falpine%3Alatest"))
# calling out to podman is easier than the API for running a container
- subprocess.run([podman(), "run", "alpine", "/bin/ls"],
- check=True,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL)
+ subprocess.run(
+ [podman(), "run", "alpine", "/bin/ls"],
+ check=True,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
@classmethod
def setUpClass(cls):
@@ -60,8 +60,12 @@ class TestApi(unittest.TestCase):
TestApi.podman = subprocess.Popen(
[
- podman(), "system", "service", "tcp:localhost:8080",
- "--log-level=debug", "--time=0"
+ podman(),
+ "system",
+ "service",
+ "tcp:localhost:8080",
+ "--log-level=debug",
+ "--time=0",
],
shell=False,
stdin=subprocess.DEVNULL,
@@ -75,13 +79,14 @@ class TestApi(unittest.TestCase):
TestApi.podman.terminate()
stdout, stderr = TestApi.podman.communicate(timeout=0.5)
if stdout:
- print("\nService Stdout:\n" + stdout.decode('utf-8'))
+ print("\nService Stdout:\n" + stdout.decode("utf-8"))
if stderr:
- print("\nService Stderr:\n" + stderr.decode('utf-8'))
+ print("\nService Stderr:\n" + stderr.decode("utf-8"))
if TestApi.podman.returncode > 0:
- sys.stderr.write("podman exited with error code {}\n".format(
- TestApi.podman.returncode))
+ sys.stderr.write(
+ "podman exited with error code {}\n".format(TestApi.podman.returncode)
+ )
sys.exit(2)
return super().tearDownClass()
@@ -222,13 +227,14 @@ class TestApi(unittest.TestCase):
def validateObjectFields(self, buffer):
- objs = json.loads(buffer)
- if not isinstance(objs, dict):
- for o in objs:
- _ = o["Id"]
- else:
- _ = objs["Id"]
- return objs
-
-if __name__ == '__main__':
+ objs = json.loads(buffer)
+ if not isinstance(objs, dict):
+ for o in objs:
+ _ = o["Id"]
+ else:
+ _ = objs["Id"]
+ return objs
+
+
+if __name__ == "__main__":
unittest.main()
diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go
index 3814d161d..678b2c882 100644
--- a/test/e2e/common_test.go
+++ b/test/e2e/common_test.go
@@ -613,12 +613,10 @@ func SkipIfRootlessCgroupsV1(reason string) {
}
}
-func SkipIfUnprevilegedCPULimits() {
+func SkipIfUnprivilegedCPULimits() {
info := GetHostDistributionInfo()
- if isRootless() &&
- info.Distribution == "fedora" &&
- (info.Version == "31" || info.Version == "32") {
- ginkgo.Skip("Rootless Fedora doesn't have permission to set CPU limits before version 33")
+ if isRootless() && info.Distribution == "fedora" {
+ ginkgo.Skip("Rootless Fedora doesn't have permission to set CPU limits")
}
}
diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go
index 6dcfa9bd8..1d683e987 100644
--- a/test/e2e/play_kube_test.go
+++ b/test/e2e/play_kube_test.go
@@ -1406,7 +1406,7 @@ spec:
It("podman play kube allows setting resource limits", func() {
SkipIfContainerized("Resource limits require a running systemd")
SkipIfRootlessCgroupsV1("Limits require root or cgroups v2")
- SkipIfUnprevilegedCPULimits()
+ SkipIfUnprivilegedCPULimits()
podmanTest.CgroupManager = "systemd"
var (
diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go
index e6bba9f67..deb4419af 100644
--- a/test/e2e/run_test.go
+++ b/test/e2e/run_test.go
@@ -315,6 +315,39 @@ var _ = Describe("Podman run", func() {
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(session.OutputToString()).To(ContainSubstring("00000000a80425fb"))
+
+ session = podmanTest.Podman([]string{"run", "--user=1000:1000", "--cap-add=DAC_OVERRIDE", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(session.OutputToString()).To(ContainSubstring("0000000000000002"))
+
+ session = podmanTest.Podman([]string{"run", "--user=1000:1000", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(session.OutputToString()).To(ContainSubstring("0000000000000000"))
+
+ session = podmanTest.Podman([]string{"run", "--user=0", "--cap-add=DAC_OVERRIDE", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(session.OutputToString()).To(ContainSubstring("0000000000000000"))
+
+ session = podmanTest.Podman([]string{"run", "--user=0:0", "--cap-add=DAC_OVERRIDE", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(session.OutputToString()).To(ContainSubstring("0000000000000000"))
+
+ if os.Geteuid() > 0 {
+ if os.Getenv("SKIP_USERNS") != "" {
+ Skip("Skip userns tests.")
+ }
+ if _, err := os.Stat("/proc/self/uid_map"); err != nil {
+ Skip("User namespaces not supported.")
+ }
+ session = podmanTest.Podman([]string{"run", "--userns=keep-id", "--cap-add=DAC_OVERRIDE", "--rm", ALPINE, "grep", "CapAmb", "/proc/self/status"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(session.OutputToString()).To(ContainSubstring("0000000000000002"))
+ }
})
It("podman run user capabilities test with image", func() {
diff --git a/test/e2e/toolbox_test.go b/test/e2e/toolbox_test.go
index fbff8d19e..822159fc2 100644
--- a/test/e2e/toolbox_test.go
+++ b/test/e2e/toolbox_test.go
@@ -30,10 +30,12 @@ import (
"os"
"os/exec"
"os/user"
+ "path"
"strconv"
"strings"
"syscall"
+ "github.com/containers/podman/v2/pkg/rootless"
. "github.com/containers/podman/v2/test/utils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -371,10 +373,23 @@ var _ = Describe("Toolbox-specific testing", func() {
currentUser, err := user.Current()
Expect(err).To(BeNil())
+
session = podmanTest.Podman([]string{"run", "-v", fmt.Sprintf("%s:%s", currentUser.HomeDir, currentUser.HomeDir), "--userns=keep-id", fedoraToolbox, "sh", "-c", "echo $HOME"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(session.OutputToString()).To(ContainSubstring(currentUser.HomeDir))
+
+ if rootless.IsRootless() {
+ location := path.Dir(currentUser.HomeDir)
+ volumeArg := fmt.Sprintf("%s:%s", location, location)
+ session = podmanTest.Podman([]string{"run",
+ "--userns=keep-id",
+ "--volume", volumeArg,
+ fedoraToolbox, "sh", "-c", "echo $HOME"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(session.OutputToString()).To(ContainSubstring(currentUser.HomeDir))
+ }
})
})