Merge pull request #16035 from alexlarsson/quadlet

Initial quadlet version integrated in golang
This commit is contained in:
OpenShift Merge Robot
2022-10-17 15:13:39 -04:00
committed by GitHub
44 changed files with 4053 additions and 2 deletions

View File

@@ -47,6 +47,7 @@ var (
type PodmanTestIntegration struct {
PodmanTest
ConmonBinary string
QuadletBinary string
Root string
NetworkConfigDir string
OCIRuntime string
@@ -212,6 +213,11 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration {
podmanRemoteBinary = os.Getenv("PODMAN_REMOTE_BINARY")
}
quadletBinary := filepath.Join(cwd, "../../bin/quadlet")
if os.Getenv("QUADLET_BINARY") != "" {
quadletBinary = os.Getenv("QUADLET_BINARY")
}
conmonBinary := "/usr/libexec/podman/conmon"
altConmonBinary := "/usr/bin/conmon"
if _, err := os.Stat(conmonBinary); os.IsNotExist(err) {
@@ -280,6 +286,7 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration {
NetworkBackend: networkBackend,
},
ConmonBinary: conmonBinary,
QuadletBinary: quadletBinary,
Root: root,
TmpDir: tempDir,
NetworkConfigDir: networkConfigDir,
@@ -513,6 +520,19 @@ func (p *PodmanTestIntegration) PodmanPID(args []string) (*PodmanSessionIntegrat
return &PodmanSessionIntegration{podmanSession}, command.Process.Pid
}
func (p *PodmanTestIntegration) Quadlet(args []string, sourceDir string) *PodmanSessionIntegration {
fmt.Printf("Running: %s %s with QUADLET_UNIT_DIRS=%s\n", p.QuadletBinary, strings.Join(args, " "), sourceDir)
command := exec.Command(p.QuadletBinary, args...)
command.Env = []string{fmt.Sprintf("QUADLET_UNIT_DIRS=%s", sourceDir)}
session, err := Start(command, GinkgoWriter, GinkgoWriter)
if err != nil {
Fail("unable to run quadlet command: " + strings.Join(args, " "))
}
quadletSession := &PodmanSession{Session: session}
return &PodmanSessionIntegration{quadletSession}
}
// Cleanup cleans up the temporary store
func (p *PodmanTestIntegration) Cleanup() {
// Remove all pods...

View File

@@ -0,0 +1,12 @@
## assert-podman-final-args imagename
## assert-podman-args "--annotation" "org.foo.Arg0=arg0"
## assert-podman-args "--annotation" "org.foo.Arg1=arg1"
## assert-podman-args "--annotation" "org.foo.Arg2=arg 2"
## assert-podman-args "--annotation" "org.foo.Arg3=arg3"
[Container]
Image=imagename
Annotation=org.foo.Arg1=arg1 "org.foo.Arg2=arg 2" \
org.foo.Arg3=arg3
Annotation=org.foo.Arg0=arg0

View File

@@ -0,0 +1,12 @@
## assert-podman-final-args run --name=systemd-%N --cidfile=%t/%N.cid --replace --rm -d --log-driver journald --pull=never --runtime /usr/bin/crun --cgroups=split --sdnotify=conmon imagename
[Container]
Image=imagename
# Disable all default features to get as empty podman run command as we can
RemapUsers=no
NoNewPrivileges=no
DropCapability=
RunInit=no
VolatileTmp=no
Timezone=

View File

@@ -0,0 +1,27 @@
## assert-podman-final-args imagename
## assert-podman-args "--name=systemd-%N"
## assert-podman-args "--cidfile=%t/%N.cid"
## assert-podman-args "--rm"
## assert-podman-args "--replace"
## assert-podman-args "-d"
## assert-podman-args "--log-driver" "journald"
## assert-podman-args "--pull=never"
## assert-podman-args "--init"
## assert-podman-args "--runtime" "/usr/bin/crun"
## assert-podman-args "--cgroups=split"
## assert-podman-args "--sdnotify=conmon"
## assert-podman-args "--security-opt=no-new-privileges"
## assert-podman-args "--cap-drop=all"
## assert-podman-args "--tmpfs" "/tmp:rw,size=512M,mode=1777"
## assert-key-is "Unit" "RequiresMountsFor" "%t/containers"
## assert-key-is "Service" "KillMode" "mixed"
## assert-key-is "Service" "Delegate" "yes"
## assert-key-is "Service" "Type" "notify"
## assert-key-is "Service" "NotifyAccess" "all"
## assert-key-is "Service" "SyslogIdentifier" "%N"
## assert-key-is "Service" "ExecStartPre" "-rm -f %t/%N.cid"
## assert-key-is "Service" "ExecStopPost" "-/usr/bin/podman rm -f -i --cidfile=%t/%N.cid" "-rm -f %t/%N.cid"
## assert-key-is "Service" "Environment" "PODMAN_SYSTEMD_UNIT=%n"
[Container]
Image=imagename

View File

@@ -0,0 +1,8 @@
## assert-key-is Unit RequiresMountsFor "%t/containers"
## assert-key-is Service Type oneshot
## assert-key-is Service RemainAfterExit yes
## assert-key-is Service ExecCondition '/usr/bin/bash -c "! /usr/bin/podman volume exists systemd-basic"'
## assert-key-is Service ExecStart "/usr/bin/podman volume create systemd-basic"
## assert-key-is Service SyslogIdentifier "%N"
[Volume]

View File

@@ -0,0 +1,8 @@
## assert-podman-args "--cap-drop=all"
## assert-podman-args "--cap-add=cap_dac_override"
## assert-podman-args "--cap-add=cap_ipc_owner"
[Container]
Image=imagename
AddCapability=CAP_DAC_OVERRIDE
AddCapability=CAP_IPC_OWNER

View File

@@ -0,0 +1,12 @@
## assert-podman-final-args imagename
## assert-podman-args --env "FOO1=foo1"
## assert-podman-args --env "FOO2=foo2 "
## assert-podman-args --env "FOO3=foo3"
## assert-podman-args --env "REPLACE=replaced"
## assert-podman-args --env "FOO4=foo\\nfoo"
[Container]
Image=imagename
Environment=FOO1=foo1 "FOO2=foo2 " \
FOO3=foo3 REPLACE=replace
Environment=REPLACE=replaced 'FOO4=foo\nfoo'

View File

@@ -0,0 +1,5 @@
## assert-podman-final-args "/some/path" "an arg" "a;b\\nc\\td'e" "a;b\\nc\\td" "a\"b"
[Container]
Image=imagename
Exec=/some/path "an arg" "a;b\nc\td'e" a;b\nc\td 'a"b'

View File

@@ -0,0 +1,6 @@
## assert-podman-final-args imagename "/some/binary file" "--arg1" "arg 2"
[Container]
Image=imagename
Exec="/some/binary file" --arg1 \
"arg 2"

View File

@@ -0,0 +1,4 @@
## assert-podman-final-args imagename
[Container]
Image=imagename

View File

@@ -0,0 +1,21 @@
## assert-symlink alias.service install.service
## assert-symlink another-alias.service install.service
## assert-symlink in/a/dir/alias3.service ../../../install.service
## assert-symlink want1.service.wants/install.service ../install.service
## assert-symlink want2.service.wants/install.service ../install.service
## assert-symlink want3.service.wants/install.service ../install.service
## assert-symlink req1.service.requires/install.service ../install.service
## assert-symlink req2.service.requires/install.service ../install.service
## assert-symlink req3.service.requires/install.service ../install.service
[Container]
Image=imagename
[Install]
Alias=alias.service \
"another-alias.service"
Alias=in/a/dir/alias3.service
WantedBy=want1.service want2.service
WantedBy=want3.service
RequiredBy=req1.service req2.service
RequiredBy=req3.service

View File

@@ -0,0 +1,12 @@
## assert-podman-final-args imagename
## assert-podman-args "--label" "org.foo.Arg0=arg0"
## assert-podman-args "--label" "org.foo.Arg1=arg1"
## assert-podman-args "--label" "org.foo.Arg2=arg 2"
## assert-podman-args "--label" "org.foo.Arg3=arg3"
[Container]
Image=imagename
Label=org.foo.Arg1=arg1 "org.foo.Arg2=arg 2" \
org.foo.Arg3=arg3
Label=org.foo.Arg0=arg0

View File

@@ -0,0 +1,8 @@
## assert-key-contains Service ExecStart " --label org.foo.Arg1=arg1 "
## assert-key-contains Service ExecStart " --label org.foo.Arg2=arg2 "
## assert-key-contains Service ExecStart " --label org.foo.Arg3=arg3 "
[Volume]
Label=org.foo.Arg1=arg1
Label=org.foo.Arg2=arg2 \
org.foo.Arg3=arg3

View File

@@ -0,0 +1,5 @@
## assert-podman-args "--name=foobar"
[Container]
Image=imagename
ContainerName=foobar

View File

@@ -0,0 +1,4 @@
## assert-failed
## assert-stderr-contains "No Image key specified"
[Container]

View File

@@ -0,0 +1,6 @@
## !assert-podman-args --uidmap
## !assert-podman-args --gidmap
[Container]
Image=imagename
RemapUsers=no

View File

@@ -0,0 +1,28 @@
# This is an non-user-remapped container, but the user is mapped (uid
# 1000 in container is uid 90 on host). This means the result should
# map those particular ids to each other, but map all other container
# ids to the same as the host.
# There is some additional complexity, as the host uid (90) that the
# container uid is mapped to can't also be mapped to itself, as ids
# can only be mapped once, so it has to be unmapped.
## assert-podman-args --user 1000:1001
## assert-podman-args --uidmap 0:0:90
## assert-podman-args --uidmap 91:91:909
## assert-podman-args --uidmap 1000:90:1
## assert-podman-args --uidmap 1001:1001:4294966294
## assert-podman-args --gidmap 0:0:91
## assert-podman-args --gidmap 92:92:909
## assert-podman-args --gidmap 1001:91:1
## assert-podman-args --gidmap 1002:1002:4294966293
[Container]
Image=imagename
RemapUsers=no
User=1000
Group=1001
HostUser=90
HostGroup=91

View File

@@ -0,0 +1,5 @@
## assert-podman-args "--sdnotify=container"
[Container]
Image=imagename
Notify=yes

View File

@@ -0,0 +1,10 @@
## assert-podman-final-args imagename
## assert-key-is "Unit" "Foo" "bar1" "bar2"
## assert-key-is "X-Container" "Image" "imagename"
[Unit]
Foo=bar1
Foo=bar2
[Container]
Image=imagename

View File

@@ -0,0 +1,9 @@
## assert-podman-args "--foo"
## assert-podman-args "--bar"
## assert-podman-args "--also"
[Container]
Image=imagename
PodmanArgs="--foo" \
--bar
PodmanArgs=--also

View File

@@ -0,0 +1,54 @@
[Container]
Image=imagename
## assert-podman-args --expose=1000
ExposeHostPort=1000
## assert-podman-args --expose=2000-3000
ExposeHostPort=2000-3000
## assert-podman-args -p=127.0.0.1:80:90
PublishPort=127.0.0.1:80:90
## assert-podman-args -p=80:91
PublishPort=0.0.0.0:80:91
## assert-podman-args -p=80:92
PublishPort=:80:92
## assert-podman-args -p=127.0.0.1::93
PublishPort=127.0.0.1::93
## assert-podman-args -p=94
PublishPort=0.0.0.0::94
## assert-podman-args -p=95
PublishPort=::95
## assert-podman-args -p=80:96
PublishPort=80:96
## assert-podman-args -p=97
PublishPort=97
## assert-podman-args -p=1234/udp
PublishPort=1234/udp
## assert-podman-args -p=1234:1234/udp
PublishPort=1234:1234/udp
## assert-podman-args -p=127.0.0.1:1234:1234/udp
PublishPort=127.0.0.1:1234:1234/udp
## assert-podman-args -p=1234/tcp
PublishPort=1234/tcp
## assert-podman-args -p=1234:1234/tcp
PublishPort=1234:1234/tcp
## assert-podman-args -p=127.0.0.1:1234:1234/tcp
PublishPort=127.0.0.1:1234:1234/tcp
## assert-podman-args --expose=2000-3000/udp
ExposeHostPort=2000-3000/udp
## assert-podman-args --expose=2000-3000/tcp
ExposeHostPort=2000-3000/tcp

View File

@@ -0,0 +1,28 @@
[Container]
Image=imagename
## assert-podman-args -p=[::1]:80:90
PublishPort=[::1]:80:90
## assert-podman-args -p=[::]:80:91
PublishPort=[::]:80:91
## assert-podman-args -p=[2001:DB8::23]:80:91
PublishPort=[2001:DB8::23]:80:91
## assert-podman-args -p=[::1]::93
PublishPort=[::1]::93
## assert-podman-args -p=[::]::94
PublishPort=[::]::94
## assert-podman-args -p=[2001:db8::42]::94
PublishPort=[2001:db8::42]::94
## assert-podman-args -p=[::1]:1234:1234/udp
PublishPort=[::1]:1234:1234/udp
## assert-podman-args -p=[::1]:1234:1234/tcp
PublishPort=[::1]:1234:1234/tcp
## assert-podman-args -p=[2001:db8:c0:ff:ee::1]:1234:1234/udp
PublishPort=[2001:db8:c0:ff:ee::1]:1234:1234/udp

View File

@@ -0,0 +1,9 @@
## assert-podman-args --preserve-fds=1
## assert-podman-args --env LISTEN_FDS=1
## assert-podman-args --env LISTEN_PID=2
## assert-key-is "Service" "Type" "notify"
## assert-key-is "Service" "NotifyAccess" "all"
[Container]
Image=imagename
SocketActivated=yes

View File

@@ -0,0 +1,5 @@
## assert-podman-args --tz=foo
[Container]
Image=imagename
Timezone=foo

View File

@@ -0,0 +1,6 @@
## assert-key-contains Service ExecStart " --opt o=uid=0,gid=11 "
[Volume]
# Test usernames too
User=root
Group=11

View File

@@ -0,0 +1,24 @@
## assert-podman-args --user 1000:1001
## assert-podman-args --uidmap 0:0:1
## assert-podman-args --uidmap 1:100000:999
## assert-podman-args --uidmap 1000:900:1
## assert-podman-args --uidmap 1001:100999:99001
## assert-podman-args --gidmap 0:0:1
## assert-podman-args --gidmap 1:100000:1000
## assert-podman-args --gidmap 1001:901:1
## assert-podman-args --gidmap 1002:101000:99000
[Container]
Image=imagename
User=1000
HostUser=900
Group=1001
HostGroup=901
RemapUsers=yes
# Set this to get well-known valuse for the checks
RemapUidRanges=100000-199999
RemapGidRanges=100000-199999

View File

@@ -0,0 +1,26 @@
## assert-podman-args --user 1000:1001
## assert-podman-args --uidmap 0:100000:1000
## assert-podman-args --uidmap 1000:0:1
## assert-podman-args --uidmap 1001:101000:99000
## !assert-podman-args --uidmap 0:0:1
## assert-podman-args --gidmap 0:100000:1001
## assert-podman-args --gidmap 1001:0:1
## assert-podman-args --gidmap 1002:101001:98999
## !assert-podman-args --gidmap 0:0:1
# Map container uid 1000 to host root
# This means container root must map to something else
[Container]
Image=imagename
User=1000
# Also test name parsing
HostUser=root
Group=1001
HostGroup=0
RemapUsers=yes
# Set this to get well-known valuse for the checks
RemapUidRanges=100000-199999
RemapGidRanges=100000-199999

View File

@@ -0,0 +1,22 @@
# No need for --user 0:0, it is the default
## !assert-podman-args --user
## assert-podman-args --uidmap 0:0:1
## assert-podman-args --gidmap 0:0:1
## assert-podman-args --uidmap 1:100000:100000
## assert-podman-args --gidmap 1:100000:100000
# Map container uid root to host root
[Container]
Image=imagename
User=0
# Also test name parsing
HostUser=root
Group=0
HostGroup=0
RemapUsers=yes
# Set this to get well-known valuse for the checks
RemapUidRanges=100000-199999
RemapGidRanges=100000-199999

View File

@@ -0,0 +1,7 @@
## assert-podman-final-args imagename
## assert-podman-args "--user" "998:999"
[Container]
Image=imagename
User=998
Group=999

View File

@@ -0,0 +1,12 @@
## assert-podman-args -v /host/dir:/container/volume
## assert-podman-args -v /host/dir2:/container/volume2:Z
## assert-podman-args -v named:/container/named
## assert-podman-args -v systemd-quadlet:/container/quadlet imagename
[Container]
Image=imagename
Volume=/host/dir:/container/volume
Volume=/host/dir2:/container/volume2:Z
Volume=/container/empty
Volume=named:/container/named
Volume=quadlet.volume:/container/quadlet

291
test/e2e/quadlet_test.go Normal file
View File

@@ -0,0 +1,291 @@
package integration
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/podman/v4/pkg/systemdparser"
"github.com/mattn/go-shellwords"
. "github.com/containers/podman/v4/test/utils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
)
type quadletTestcase struct {
data []byte
serviceName string
checks [][]string
}
func loadQuadletTestcase(path string) *quadletTestcase {
data, err := os.ReadFile(path)
Expect(err).To(BeNil())
base := filepath.Base(path)
ext := filepath.Ext(base)
service := base[:len(base)-len(ext)]
if ext == ".volume" {
service += "-volume"
}
service += ".service"
checks := make([][]string, 0)
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "##") {
words, err := shellwords.Parse(line[2:])
Expect(err).To(BeNil())
checks = append(checks, words)
}
}
return &quadletTestcase{
data,
service,
checks,
}
}
func matchSublistAt(full []string, pos int, sublist []string) bool {
if len(sublist) > len(full)-pos {
return false
}
for i := range sublist {
if sublist[i] != full[pos+i] {
return false
}
}
return true
}
func findSublist(full []string, sublist []string) int {
if len(sublist) > len(full) {
return -1
}
if len(sublist) == 0 {
return -1
}
for i := 0; i < len(full)-len(sublist)+1; i++ {
if matchSublistAt(full, i, sublist) {
return i
}
}
return -1
}
func (t *quadletTestcase) assertStdErrContains(args []string, session *PodmanSessionIntegration) bool {
return strings.Contains(session.OutputToString(), args[0])
}
func (t *quadletTestcase) assertKeyIs(args []string, unit *systemdparser.UnitFile) bool {
group := args[0]
key := args[1]
values := args[2:]
realValues := unit.LookupAll(group, key)
if len(realValues) != len(values) {
return false
}
for i := range realValues {
if realValues[i] != values[i] {
return false
}
}
return true
}
func (t *quadletTestcase) assertKeyContains(args []string, unit *systemdparser.UnitFile) bool {
group := args[0]
key := args[1]
value := args[2]
realValue, ok := unit.LookupLast(group, key)
return ok && strings.Contains(realValue, value)
}
func (t *quadletTestcase) assertPodmanArgs(args []string, unit *systemdparser.UnitFile) bool {
podmanArgs, _ := unit.LookupLastArgs("Service", "ExecStart")
return findSublist(podmanArgs, args) != -1
}
func (t *quadletTestcase) assertFinalArgs(args []string, unit *systemdparser.UnitFile) bool {
podmanArgs, _ := unit.LookupLastArgs("Service", "ExecStart")
if len(podmanArgs) < len(args) {
return false
}
return matchSublistAt(podmanArgs, len(podmanArgs)-len(args), args)
}
func (t *quadletTestcase) assertSymlink(args []string, unit *systemdparser.UnitFile) bool {
symlink := args[0]
expectedTarget := args[1]
dir := filepath.Dir(unit.Path)
target, err := os.Readlink(filepath.Join(dir, symlink))
Expect(err).ToNot(HaveOccurred())
return expectedTarget == target
}
func (t *quadletTestcase) doAssert(check []string, unit *systemdparser.UnitFile, session *PodmanSessionIntegration) error {
op := check[0]
args := make([]string, 0)
for _, a := range check[1:] {
// Apply \n and \t as they are used in the testcases
a = strings.ReplaceAll(a, "\\n", "\n")
a = strings.ReplaceAll(a, "\\t", "\t")
args = append(args, a)
}
invert := false
if op[0] == '!' {
invert = true
op = op[1:]
}
var ok bool
switch op {
case "assert-failed":
ok = true /* Handled separately */
case "assert-stderr-contains":
ok = t.assertStdErrContains(args, session)
case "assert-key-is":
ok = t.assertKeyIs(args, unit)
case "assert-key-contains":
ok = t.assertKeyContains(args, unit)
case "assert-podman-args":
ok = t.assertPodmanArgs(args, unit)
case "assert-podman-final-args":
ok = t.assertFinalArgs(args, unit)
case "assert-symlink":
ok = t.assertSymlink(args, unit)
default:
return fmt.Errorf("Unsupported assertion %s", op)
}
if invert {
ok = !ok
}
if !ok {
s, _ := unit.ToString()
return fmt.Errorf("Failed assertion for %s: %s\n\n%s", t.serviceName, strings.Join(check, " "), s)
}
return nil
}
func (t *quadletTestcase) check(generateDir string, session *PodmanSessionIntegration) {
expectFail := false
for _, c := range t.checks {
if c[0] == "assert-failed" {
expectFail = true
}
}
file := filepath.Join(generateDir, t.serviceName)
if _, err := os.Stat(file); os.IsNotExist(err) && expectFail {
return // Successful fail
}
unit, err := systemdparser.ParseUnitFile(file)
Expect(err).To(BeNil())
for _, check := range t.checks {
err := t.doAssert(check, unit, session)
Expect(err).ToNot(HaveOccurred())
}
}
var _ = Describe("quadlet system generator", func() {
var (
tempdir string
err error
generatedDir string
quadletDir string
podmanTest *PodmanTestIntegration
)
BeforeEach(func() {
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)
}
podmanTest = PodmanTestCreate(tempdir)
podmanTest.Setup()
generatedDir = filepath.Join(podmanTest.TempDir, "generated")
err = os.Mkdir(generatedDir, os.ModePerm)
Expect(err).To(BeNil())
quadletDir = filepath.Join(podmanTest.TempDir, "quadlet")
err = os.Mkdir(quadletDir, os.ModePerm)
Expect(err).To(BeNil())
})
AfterEach(func() {
podmanTest.Cleanup()
f := CurrentGinkgoTestDescription()
processTestResult(f)
})
DescribeTable("Running quadlet test case",
func(fileName string) {
testcase := loadQuadletTestcase(filepath.Join("quadlet", fileName))
// Write the tested file to the quadlet dir
err = os.WriteFile(filepath.Join(quadletDir, fileName), testcase.data, 0644)
Expect(err).To(BeNil())
// Run quadlet to convert the file
session := podmanTest.Quadlet([]string{generatedDir}, quadletDir)
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
// Print any stderr output
errs := session.ErrorToString()
if errs != "" {
fmt.Println("error:", session.ErrorToString())
}
testcase.check(generatedDir, session)
},
Entry("Basic container", "basic.container"),
Entry("annotation.container", "annotation.container"),
Entry("basepodman.container", "basepodman.container"),
Entry("capabilities.container", "capabilities.container"),
Entry("env.container", "env.container"),
Entry("escapes.container", "escapes.container"),
Entry("exec.container", "exec.container"),
Entry("image.container", "image.container"),
Entry("install.container", "install.container"),
Entry("label.container", "label.container"),
Entry("name.container", "name.container"),
Entry("noimage.container", "noimage.container"),
Entry("noremapuser2.container", "noremapuser2.container"),
Entry("noremapuser.container", "noremapuser.container"),
Entry("notify.container", "notify.container"),
Entry("other-sections.container", "other-sections.container"),
Entry("podmanargs.container", "podmanargs.container"),
Entry("ports.container", "ports.container"),
Entry("ports_ipv6.container", "ports_ipv6.container"),
Entry("socketactivated.container", "socketactivated.container"),
Entry("timezone.container", "timezone.container"),
Entry("user.container", "user.container"),
Entry("user-host.container", "user-host.container"),
Entry("user-root1.container", "user-root1.container"),
Entry("user-root2.container", "user-root2.container"),
Entry("volume.container", "volume.container"),
Entry("basic.volume", "basic.volume"),
Entry("label.volume", "label.volume"),
Entry("uid.volume", "uid.volume"),
)
})