diff --git a/cmd/quadlet/main.go b/cmd/quadlet/main.go index 261e14d00e..6b99f87f57 100644 --- a/cmd/quadlet/main.go +++ b/cmd/quadlet/main.go @@ -23,6 +23,7 @@ import ( var ( verboseFlag bool // True if -v passed + noKmsgFlag bool isUser bool // True if run as quadlet-user-generator executable ) @@ -134,7 +135,7 @@ func generateServiceFile(service *parser.UnitFile) error { Debugf("writing '%s'", service.Path) service.PrependComment("", - "Automatically generated by quadlet-generator", + fmt.Sprintf("Automatically generated by %s", os.Args[0]), "") f, err := os.Create(service.Path) @@ -219,6 +220,10 @@ func main() { enableDebug() } + if noKmsgFlag { + noKmsg = true + } + if flag.NArg() < 1 { Logf("Missing output directory argument") os.Exit(1) @@ -270,4 +275,5 @@ func main() { func init() { flag.BoolVar(&verboseFlag, "v", false, "Print debug information") + flag.BoolVar(&noKmsgFlag, "no-kmsg-log", false, "Don't log to kmsg") } diff --git a/docs/source/markdown/options/network.md b/docs/source/markdown/options/network.md index 32f650d260..36cfca0a7a 100644 --- a/docs/source/markdown/options/network.md +++ b/docs/source/markdown/options/network.md @@ -15,7 +15,8 @@ Valid _mode_ values are: - **mac=MAC**: Specify a static mac address for this container. - **interface_name**: Specify a name for the created network interface inside the container. - For example to set a static ipv4 address and a static mac address, use `--network bridge:ip=10.88.0.10,mac=44:33:22:11:00:99`. + For example to set a static ipv4 address and a static mac address, use `--network bridge:ip=10.88.0.10,mac=44:33:22:11:00:99`. + - \[:OPTIONS,...]: Connect to a user-defined network; this is the network name or ID from a network created by **[podman network create](podman-network-create.1.md)**. Using the network name implies the bridge network mode. It is possible to specify the same options described under the bridge mode above. You can use the **--network** option multiple times to specify additional networks. - **none**: Create a network namespace for the container but do not configure network interfaces for it, thus the container has no network connectivity. - **container:**_id_: Reuse another container's network stack. diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index 22cfbd4617..b8e6a408e5 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -126,22 +126,39 @@ setuid and file capabilities. #### `DropCapability=` (defaults to `all`) -Drop these capabilities from the default container capability set. The default is `all`, allowing -addition of capabilities with `AddCapability`. Set this to empty to drop no capabilities. -This can be listed multiple times. +Drop these capabilities from the default podman capability set, or `all` for all capabilities. The default if no +`DropCapability` is set is `all`. Set this to empty (i.e. `DropCapability=`) to use the default podman capability set. + +This is a space separated list of capabilities. This key can be listed multiple times. + +For example: +``` +DropCapability=CAP_DAC_OVERRIDE CAP_IPC_OWNER +``` #### `AddCapability=` By default, the container runs with no capabilities (due to DropCapabilities='all'). If any specific caps are needed, then add them with this key. For example using `AddCapability=CAP_DAC_OVERRIDE`. -This can be listed multiple times. -#### `ReadOnly=` (defaults to `no`) +This is a space separated list of capabilities. This key can be listed multiple times. + +For example: +``` +AddCapability=CAP_DAC_OVERRIDE CAP_IPC_OWNER +``` + +#### `ReadOnly=` (defaults to `yes`) If enabled, makes image read-only, with /var/tmp, /tmp and /run a tmpfs (unless disabled by `VolatileTmp=no`). **NOTE:** Podman will automatically copy any content from the image onto the tmpfs +#### `SeccompProfile=` + +Set the seccomp profile to use in the container. If unset, the default podman profile is used. +Set to either the pathname of a json file, or `unconfined` to disable the seccomp filters. + #### `RemapUsers=` (defaults to `no`) If this is enabled, then host user and group ids are remapped in the container, such that all the uids @@ -217,6 +234,14 @@ created by using a `$name.volume` quadlet file. This key can be listed multiple times. +#### `Network=` + +Specify a custom network for the container. This has the same format as the `--network` option +to `podman run`. For example, use `host` to use the host network in the container, or `none` to +not set up networking in the container. + +This key can be listed multiple times. + #### `ExposeHostPort=` Exposes a port, or a range of ports (e.g. `50-59`), from the host to the container. Equivalent @@ -241,6 +266,16 @@ allocated port can be found with the `podman port` command. This key can be listed multiple times. +#### `AddDevice=` + +Adds a device node from the host into the container. The format of this is +`HOST-DEVICE[:CONTAINER-DEVICE][:PERMISSIONS]`, where `HOST-DEVICE` is the path of +the device node on the host, `CONTAINER-DEVICE` is the path of the device node in +the container, and `PERMISSIONS` is a list of permissions combining 'r' for read, +'w' for write, and 'm' for mknod(2). + +This key can be listed multiple times. + #### `PodmanArgs=` This key contains a list of arguments passed directly to the end of the `podman run` command diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index b1869cca23..d3bde16fdc 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -56,7 +56,6 @@ const ( KeyRemapUIDRanges = "RemapUidRanges" KeyRemapGIDRanges = "RemapGidRanges" KeyNotify = "Notify" - KeySocketActivated = "SocketActivated" KeyExposeHostPort = "ExposeHostPort" KeyPublishPort = "PublishPort" KeyKeepID = "KeepId" @@ -71,6 +70,9 @@ const ( KeyRunInit = "RunInit" KeyVolatileTmp = "VolatileTmp" KeyTimezone = "Timezone" + KeySeccompProfile = "SeccompProfile" + KeyAddDevice = "AddDevice" + KeyNetwork = "Network" ) // Supported keys in "Container" group @@ -89,7 +91,6 @@ var supportedContainerKeys = map[string]bool{ KeyRemapUIDRanges: true, KeyRemapGIDRanges: true, KeyNotify: true, - KeySocketActivated: true, KeyExposeHostPort: true, KeyPublishPort: true, KeyKeepID: true, @@ -104,6 +105,9 @@ var supportedContainerKeys = map[string]bool{ KeyRunInit: true, KeyVolatileTmp: true, KeyTimezone: true, + KeySeccompProfile: true, + KeyAddDevice: true, + KeyNetwork: true, } // Supported keys in "Volume" group @@ -353,8 +357,7 @@ func ConvertContainer(container *parser.UnitFile, isUser bool) (*parser.UnitFile "-d", // But we still want output to the journal, so use the log driver. - // TODO: Once available we want to use the passthrough log-driver instead. - "--log-driver", "journald", + "--log-driver", "passthrough", // Never try to pull the image during service start "--pull=never") @@ -370,6 +373,13 @@ func ConvertContainer(container *parser.UnitFile, isUser bool) (*parser.UnitFile podman.addf("--tz=%s", timezone) } + networks := container.LookupAll(ContainerGroup, KeyNetwork) + for _, network := range networks { + if len(network) > 0 { + podman.addf("--network=%s", network) + } + } + // Run with a pid1 init to reap zombies by default (as most apps don't do that) runInit := container.LookupBoolean(ContainerGroup, KeyRunInit, true) if runInit { @@ -397,9 +407,21 @@ func ConvertContainer(container *parser.UnitFile, isUser bool) (*parser.UnitFile podman.add("--security-opt=no-new-privileges") } + // But allow overrides with AddCapability + devices := container.LookupAllStrv(ContainerGroup, KeyAddDevice) + for _, device := range devices { + podman.addf("--device=%s", device) + } + + // Default to no higher level privileges or caps + seccompProfile, hasSeccompProfile := container.Lookup(ContainerGroup, KeySeccompProfile) + if hasSeccompProfile { + podman.add("--security-opt", fmt.Sprintf("seccomp=%s", seccompProfile)) + } + dropCaps := []string{"all"} // Default if container.HasKey(ContainerGroup, KeyDropCapability) { - dropCaps = container.LookupAll(ContainerGroup, KeyDropCapability) + dropCaps = container.LookupAllStrv(ContainerGroup, KeyDropCapability) } for _, caps := range dropCaps { @@ -407,12 +429,12 @@ func ConvertContainer(container *parser.UnitFile, isUser bool) (*parser.UnitFile } // But allow overrides with AddCapability - addCaps := container.LookupAll(ContainerGroup, KeyAddCapability) + addCaps := container.LookupAllStrv(ContainerGroup, KeyAddCapability) for _, caps := range addCaps { podman.addf("--cap-add=%s", strings.ToLower(caps)) } - readOnly := container.LookupBoolean(ContainerGroup, KeyReadOnly, false) + readOnly := container.LookupBoolean(ContainerGroup, KeyReadOnly, true) if readOnly { podman.add("--read-only") } @@ -429,18 +451,6 @@ func ConvertContainer(container *parser.UnitFile, isUser bool) (*parser.UnitFile podman.add("--read-only-tmpfs=false") } - socketActivated := container.LookupBoolean(ContainerGroup, KeySocketActivated, false) - if socketActivated { - // TODO: This will not be needed with later podman versions that support activation directly: - // https://github.com/containers/podman/pull/11316 - podman.add("--preserve-fds=1") - podmanEnv["LISTEN_FDS"] = "1" - - // TODO: This will not be 2 when catatonit forwards fds: - // https://github.com/openSUSE/catatonit/pull/15 - podmanEnv["LISTEN_PID"] = "2" - } - defaultContainerUID := uint32(0) defaultContainerGID := uint32(0) diff --git a/test/e2e/quadlet/basepodman.container b/test/e2e/quadlet/basepodman.container index aa97351438..8204a293b9 100644 --- a/test/e2e/quadlet/basepodman.container +++ b/test/e2e/quadlet/basepodman.container @@ -1,10 +1,10 @@ -## 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 +## assert-podman-final-args run --name=systemd-%N --cidfile=%t/%N.cid --replace --rm -d --log-driver passthrough --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 +ReadOnly=no NoNewPrivileges=no DropCapability= RunInit=no diff --git a/test/e2e/quadlet/basic.container b/test/e2e/quadlet/basic.container index ab770b10c7..c5a97ea1b0 100644 --- a/test/e2e/quadlet/basic.container +++ b/test/e2e/quadlet/basic.container @@ -4,7 +4,7 @@ ## assert-podman-args "--rm" ## assert-podman-args "--replace" ## assert-podman-args "-d" -## assert-podman-args "--log-driver" "journald" +## assert-podman-args "--log-driver" "passthrough" ## assert-podman-args "--pull=never" ## assert-podman-args "--init" ## assert-podman-args "--runtime" "/usr/bin/crun" @@ -12,7 +12,8 @@ ## 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-podman-args "--read-only" +## !assert-podman-args "--read-only-tmpfs=false" ## assert-key-is "Unit" "RequiresMountsFor" "%t/containers" ## assert-key-is "Service" "KillMode" "mixed" ## assert-key-is "Service" "Delegate" "yes" diff --git a/test/e2e/quadlet/capabilities.container b/test/e2e/quadlet/capabilities.container index ec32ed0237..4faa7ab85f 100644 --- a/test/e2e/quadlet/capabilities.container +++ b/test/e2e/quadlet/capabilities.container @@ -1,8 +1,11 @@ -## assert-podman-args "--cap-drop=all" +## !assert-podman-args "--cap-drop=all" ## assert-podman-args "--cap-add=cap_dac_override" +## assert-podman-args "--cap-add=cap_audit_write" ## assert-podman-args "--cap-add=cap_ipc_owner" [Container] Image=imagename -AddCapability=CAP_DAC_OVERRIDE +# Verify that we can reset to the default cap set +DropCapability= +AddCapability=CAP_DAC_OVERRIDE CAP_AUDIT_WRITE AddCapability=CAP_IPC_OWNER diff --git a/test/e2e/quadlet/capabilities2.container b/test/e2e/quadlet/capabilities2.container new file mode 100644 index 0000000000..b7bda66485 --- /dev/null +++ b/test/e2e/quadlet/capabilities2.container @@ -0,0 +1,9 @@ +## !assert-podman-args "--cap-drop=all" +## assert-podman-args "--cap-drop=cap_dac_override" +## assert-podman-args "--cap-drop=cap_audit_write" +## assert-podman-args "--cap-drop=cap_ipc_owner" + +[Container] +Image=localhost/imagename +DropCapability=CAP_DAC_OVERRIDE CAP_AUDIT_WRITE +DropCapability=CAP_IPC_OWNER diff --git a/test/e2e/quadlet/devices.container b/test/e2e/quadlet/devices.container new file mode 100644 index 0000000000..2e958c0db6 --- /dev/null +++ b/test/e2e/quadlet/devices.container @@ -0,0 +1,7 @@ +## assert-podman-args --device=/dev/fuse +## assert-podman-args --device=/dev/loop0:r + +[Container] +Image=localhost/imagename +AddDevice=/dev/fuse +AddDevice=/dev/loop0:r diff --git a/test/e2e/quadlet/network.container b/test/e2e/quadlet/network.container new file mode 100644 index 0000000000..89179a29ac --- /dev/null +++ b/test/e2e/quadlet/network.container @@ -0,0 +1,5 @@ +## assert-podman-args "--network=host" + +[Container] +Image=localhost/imagename +Network=host diff --git a/test/e2e/quadlet/noimage.container b/test/e2e/quadlet/noimage.container index e55d07f604..909c8c54a1 100644 --- a/test/e2e/quadlet/noimage.container +++ b/test/e2e/quadlet/noimage.container @@ -1,4 +1,4 @@ ## assert-failed -## assert-stderr-contains "No Image key specified" +## assert-stderr-contains "no Image key specified" [Container] diff --git a/test/e2e/quadlet/readonly-notmpfs.container b/test/e2e/quadlet/readonly-notmpfs.container new file mode 100644 index 0000000000..cddc7b7142 --- /dev/null +++ b/test/e2e/quadlet/readonly-notmpfs.container @@ -0,0 +1,6 @@ +## assert-podman-args "--read-only-tmpfs=false" +## assert-podman-args "--read-only" + +[Container] +Image=localhost/imagename +VolatileTmp=no diff --git a/test/e2e/quadlet/readwrite-notmpfs.container b/test/e2e/quadlet/readwrite-notmpfs.container new file mode 100644 index 0000000000..c7349a8ce0 --- /dev/null +++ b/test/e2e/quadlet/readwrite-notmpfs.container @@ -0,0 +1,7 @@ +## !assert-podman-args "--read-only" +## !assert-podman-args "--tmpfs" "/tmp:rw,size=512M,mode=1777" + +[Container] +Image=localhost/imagename +VolatileTmp=no +ReadOnly=no diff --git a/test/e2e/quadlet/readwrite.container b/test/e2e/quadlet/readwrite.container new file mode 100644 index 0000000000..61905c0e6d --- /dev/null +++ b/test/e2e/quadlet/readwrite.container @@ -0,0 +1,6 @@ +## !assert-podman-args "--read-only" +## assert-podman-args "--tmpfs" "/tmp:rw,size=512M,mode=1777" + +[Container] +Image=localhost/imagename +ReadOnly=no diff --git a/test/e2e/quadlet/seccomp.container b/test/e2e/quadlet/seccomp.container new file mode 100644 index 0000000000..5bfddffa5f --- /dev/null +++ b/test/e2e/quadlet/seccomp.container @@ -0,0 +1,5 @@ +## assert-podman-args --security-opt seccomp=unconfined + +[Container] +Image=localhost/imagename +SeccompProfile=unconfined diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index 7da6f0730f..eb05a582bc 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -80,7 +80,7 @@ func findSublist(full []string, sublist []string) int { } func (t *quadletTestcase) assertStdErrContains(args []string, session *PodmanSessionIntegration) bool { - return strings.Contains(session.OutputToString(), args[0]) + return strings.Contains(session.ErrorToString(), args[0]) } func (t *quadletTestcase) assertKeyIs(args []string, unit *parser.UnitFile) bool { @@ -174,7 +174,10 @@ func (t *quadletTestcase) doAssert(check []string, unit *parser.UnitFile, sessio } if !ok { - s, _ := unit.ToString() + s := "(nil)" + if unit != nil { + s, _ = unit.ToString() + } return fmt.Errorf("Failed assertion for %s: %s\n\n%s", t.serviceName, strings.Join(check, " "), s) } return nil @@ -189,12 +192,18 @@ func (t *quadletTestcase) check(generateDir string, session *PodmanSessionIntegr } file := filepath.Join(generateDir, t.serviceName) - if _, err := os.Stat(file); os.IsNotExist(err) && expectFail { - return // Successful fail + _, err := os.Stat(file) + if expectFail { + Expect(err).To(MatchError(os.ErrNotExist)) + } else { + Expect(err).ToNot(HaveOccurred()) } - unit, err := parser.ParseUnitFile(file) - Expect(err).To(BeNil()) + var unit *parser.UnitFile + if !expectFail { + unit, err = parser.ParseUnitFile(file) + Expect(err).To(BeNil()) + } for _, check := range t.checks { err := t.doAssert(check, unit, session) @@ -244,7 +253,7 @@ var _ = Describe("quadlet system generator", func() { Expect(err).To(BeNil()) // Run quadlet to convert the file - session := podmanTest.Quadlet([]string{generatedDir}, quadletDir) + session := podmanTest.Quadlet([]string{"-no-kmsg-log", generatedDir}, quadletDir) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) @@ -260,6 +269,8 @@ var _ = Describe("quadlet system generator", func() { Entry("annotation.container", "annotation.container"), Entry("basepodman.container", "basepodman.container"), Entry("capabilities.container", "capabilities.container"), + Entry("capabilities2.container", "capabilities2.container"), + Entry("devices.container", "devices.container"), Entry("env.container", "env.container"), Entry("escapes.container", "escapes.container"), Entry("exec.container", "exec.container"), @@ -267,6 +278,7 @@ var _ = Describe("quadlet system generator", func() { Entry("install.container", "install.container"), Entry("label.container", "label.container"), Entry("name.container", "name.container"), + Entry("network.container", "network.container"), Entry("noimage.container", "noimage.container"), Entry("noremapuser2.container", "noremapuser2.container"), Entry("noremapuser.container", "noremapuser.container"), @@ -275,7 +287,10 @@ var _ = Describe("quadlet system generator", func() { Entry("podmanargs.container", "podmanargs.container"), Entry("ports.container", "ports.container"), Entry("ports_ipv6.container", "ports_ipv6.container"), - Entry("socketactivated.container", "socketactivated.container"), + Entry("readonly-notmpfs.container", "readonly-notmpfs.container"), + Entry("readwrite.container", "readwrite.container"), + Entry("readwrite-notmpfs.container", "readwrite-notmpfs.container"), + Entry("seccomp.container", "seccomp.container"), Entry("timezone.container", "timezone.container"), Entry("user.container", "user.container"), Entry("user-host.container", "user-host.container"),