diff --git a/Makefile b/Makefile index 59dd2153de..ec2c50975b 100644 --- a/Makefile +++ b/Makefile @@ -544,6 +544,7 @@ man-page-check: bin/podman hack/man-page-checker hack/xref-helpmsgs-manpages hack/man-page-table-check + hack/xref-quadlet-docs .PHONY: swagger-check swagger-check: diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index 2c2a07c9ab..eecee580e0 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -162,18 +162,18 @@ Valid options for `[Container]` are listed below: | ContainerName=name | --name name | | ContainersConfModule=/etc/nvd\.conf | --module=/etc/nvd\.conf | | DNS=192.168.55.1 | --dns=192.168.55.1 | -| DNSSearch=foo.com | --dns-search=foo.com | | DNSOption=ndots:1 | --dns-option=ndots:1 | +| DNSSearch=foo.com | --dns-search=foo.com | | DropCapability=CAP | --cap-drop=CAP | +| Entrypoint=/foo.sh | --entrypoint=/foo.sh | | Environment=foo=bar | --env foo=bar | | EnvironmentFile=/tmp/env | --env-file /tmp/env | | EnvironmentHost=true | --env-host | -| Entrypoint=/foo.sh | --entrypoint=/foo.sh | | Exec=/usr/bin/command | Command after image specification - /usr/bin/command | | ExposeHostPort=50-59 | --expose 50-59 | | GIDMap=0:10000:10 | --gidmap=0:10000:10 | -| Group=1234 | --user UID:1234 | | GlobalArgs=--log-level=debug | --log-level=debug | +| Group=1234 | --user UID:1234 | | HealthCmd=/usr/bin/command | --health-cmd=/usr/bin/command | | HealthInterval=2m | --health-interval=2m | | HealthOnFailure=kill | --health-on-failure=kill | @@ -195,7 +195,6 @@ Valid options for `[Container]` are listed below: | Mount=type=... | --mount type=... | | Network=host | --net host | | NoNewPrivileges=true | --security-opt no-new-privileges | -| Rootfs=/var/lib/rootfs | --rootfs /var/lib/rootfs | | Notify=true | --sdnotify container | | PidsLimit=10000 | --pids-limit 10000 | | Pod=pod-name | --pod=pod-name | @@ -204,8 +203,10 @@ Valid options for `[Container]` are listed below: | Pull=never | --pull=never | | ReadOnly=true | --read-only | | ReadOnlyTmpfs=true | --read-only-tmpfs | +| Rootfs=/var/lib/rootfs | --rootfs /var/lib/rootfs | | RunInit=true | --init | | SeccompProfile=/tmp/s.json | --security-opt seccomp=/tmp/s.json | +| Secret=secret | --secret=secret[,opt=opt ...] | | SecurityLabelDisable=true | --security-opt label=disable | | SecurityLabelFileType=usr_t | --security-opt label=filetype:usr_t | | SecurityLabelLevel=s0:c1,c2 | --security-opt label=level:s0:c1,c2 | @@ -306,6 +307,12 @@ For example: DropCapability=CAP_DAC_OVERRIDE CAP_IPC_OWNER ``` +### `Entrypoint=` + +Override the default ENTRYPOINT from the image. +Equivalent to the Podman `--entrypoint` option. +Specify multi option commands in the form of a json string. + ### `Environment=` Set an environment variable in the container. This uses the same format as @@ -322,12 +329,6 @@ This key may be used multiple times, and the order persists when passed to `podm Use the host environment inside of the container. -#### `Entrypoint=` - -Override the default ENTRYPOINT from the image. -Equivalent to the Podman `--entrypoint` option. -Specify multi option commands in the form of a json string. - ### `Exec=` If this is set then it defines what command line to run in the container. If it is not set the @@ -499,14 +500,6 @@ This key can be listed multiple times. If enabled, this disables the container processes from gaining additional privileges via things like setuid and file capabilities. -### `Rootfs=` - -The rootfs to use for the container. Rootfs points to a directory on the system that contains the content to be run within the container. This option conflicts with the `Image` option. - -The format of the rootfs is the same as when passed to `podman run --rootfs`, so it supports overlay mounts as well. - -Note: On SELinux systems, the rootfs needs the correct label, which is by default unconfined_u:object_r:container_file_t:s0. - ### `Notify=` (defaults to `no`) By default, Podman is run in such a way that the systemd startup notify command is handled by @@ -576,6 +569,14 @@ If enabled, makes the image read-only. If ReadOnly is set to `yes`, mount a read-write tmpfs on /dev, /dev/shm, /run, /tmp, and /var/tmp. +### `Rootfs=` + +The rootfs to use for the container. Rootfs points to a directory on the system that contains the content to be run within the container. This option conflicts with the `Image` option. + +The format of the rootfs is the same as when passed to `podman run --rootfs`, so it supports overlay mounts as well. + +Note: On SELinux systems, the rootfs needs the correct label, which is by default unconfined_u:object_r:container_file_t:s0. + ### `RunInit=` (default to `no`) If enabled, the container has a minimal init process inside the @@ -586,6 +587,11 @@ container that forwards signals and reaps processes. 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. +### `Secret=` + +Use a Podman secret in the container either as a file or an environment variable. +This is equivalent to the Podman `--secret` option and generally has the form `secret[,opt=opt ...]` + ### `SecurityLabelDisable=` Turn off label separation for the container. @@ -606,11 +612,6 @@ Allow SecurityLabels to function within the container. This allows separation of Set the label process type for the container processes. -### `Secret=` - -Use a Podman secret in the container either as a file or an environment variable. -This is equivalent to the Podman `--secret` option and generally has the form `secret[,opt=opt ...]` - ### `ShmSize=` Size of /dev/shm. @@ -646,6 +647,10 @@ For example: Sysctl=net.ipv6.conf.all.disable_ipv6=1 net.ipv6.conf.all.use_tempaddr=1 ``` +### `Timezone=` (if unset uses system-configured default) + +The timezone to run the container in. + ### `Tmpfs=` Mount a tmpfs in the container. This is equivalent to the Podman `--tmpfs` option, and @@ -653,10 +658,6 @@ generally has the form `CONTAINER-DIR[:OPTIONS]`. This key can be listed multiple times. -### `Timezone=` (if unset uses system-configured default) - -The timezone to run the container in. - ### `UIDMap=` Run the container in a new user namespace using the supplied UID mapping. @@ -835,6 +836,7 @@ Valid options for `[Kube]` are listed below: | AutoUpdate=registry | --annotation "io.containers.autoupdate=registry" | | ConfigMap=/tmp/config.map | --config-map /tmp/config.map | | ContainersConfModule=/etc/nvd\.conf | --module=/etc/nvd\.conf | +| ExitCodePropagation=how | How to propagate container error status | | GlobalArgs=--log-level=debug | --log-level=debug | | KubeDownForce=true | --force (for `podman kube down`) | | LogDriver=journald | --log-driver journald | @@ -1145,6 +1147,8 @@ Valid options for `[Volume]` are listed below: | Label="foo=bar" | --label "foo=bar" | | Options=XYZ | --opt XYZ | | PodmanArgs=--driver=image | --driver=image | +| Type=type | Filesystem type of Device | +| User=123 | --opt uid=123 | | VolumeName=foo | podman volume create foo | Supported keys in `[Volume]` section are: diff --git a/hack/xref-quadlet-docs b/hack/xref-quadlet-docs new file mode 100755 index 0000000000..a85d403ff8 --- /dev/null +++ b/hack/xref-quadlet-docs @@ -0,0 +1,251 @@ +#!/usr/bin/perl +# +# xref-quadlet-docs - cross-validate quadlet man page vs actual source +# +# $Id: .perl-template,v 1.2 2020/03/03 20:08:31 esm Exp esm $ +# +package Podman::CrossrefQuadletDocs; + +use v5.14; +use utf8; + +use strict; +use warnings; + +(our $ME = $0) =~ s|.*/||; +our $VERSION = '0.1'; + +############################################################################### +# BEGIN user-customizable section + +our $Go = 'pkg/systemd/quadlet/quadlet.go'; +our $Doc = 'docs/source/markdown/podman-systemd.unit.5.md'; + +# END user-customizable section +############################################################################### + +############################################################################### +# BEGIN boilerplate args checking, usage messages + +sub usage { + print <<"END_USAGE"; +Usage: $ME [OPTIONS] + +$ME cross-checks quadlet documentation between the Go source[Go] +and the man page[MD]. + + [Go]: $Go + [MD]: $Doc + +We check that: + + * all keys in [Go] are documented in [MD] + * all keys in [MD] exist in [Go] + * any keys listed in [MD] tables also have a description block + and vice-versa + * all keys everywhere are in sorted order + +OPTIONS: + + --help display this message + --version display program name and version +END_USAGE + + exit; +} + +# Command-line options. Note that this operates directly on @ARGV ! +our $debug = 0; +sub handle_opts { + use Getopt::Long; + GetOptions( + 'debug!' => \$debug, + + help => \&usage, + man => \&man, + version => sub { print "$ME version $VERSION\n"; exit 0 }, + ) or die "Try `$ME --help' for help\n"; +} + +# END boilerplate args checking, usage messages +############################################################################### + +############################## CODE BEGINS HERE ############################### + +# The term is "modulino". +__PACKAGE__->main() unless caller(); + +# Main code. +sub main { + # Note that we operate directly on @ARGV, not on function parameters. + # This is deliberate: it's because Getopt::Long only operates on @ARGV + # and there's no clean way to make it use @_. + handle_opts(); # will set package globals + + # No command-line args + die "$ME: Too many arguments; try $ME --help\n" if @ARGV; + + my $errs = 0; + $SIG{__WARN__} = sub { + print STDERR "@_"; + ++$errs; + }; + + # Assume that Go source file has Truth + my $true_keys = read_go($Go); + + # Read md file, compare against Truth + crossref_doc($Doc, $true_keys); + + exit $errs; +} + + +############# +# read_go # Returns list of key strings found in quadlet.go +############# +sub read_go { + my $path = shift; + open my $fh, '<', $path + or die "$ME: Cannot read $path: $!\n";; + + my @found; # List of key strings + my $last_constname; # Most recently seen const name + + while (my $line = <$fh>) { + # Only interested in lines of the form KeyFoo = "Foo" + if ($line =~ /^\s+Key(\S+)\s+=\s+"(\S+)"/) { + my ($constname, $keystring) = ($1, $2); + + my $deprecated = ($line =~ m!\s//\s+deprecated!i); + + # const name must be the same as the string + $constname eq $keystring + or warn "$ME: $path:$.: mismatched strings: Key$constname = \"$keystring\"\n"; + + # Sorting check. + if ($last_constname) { + if (lc($constname) lt lc($last_constname)) { + warn "$ME: $path:$.: out-of-order variable name 'Key$constname' should precede 'Key$last_constname'\n"; + } + } + $last_constname = $constname; + + push @found, $keystring + unless $deprecated; + } + } + close $fh; + + \@found; +} + +################## +# crossref_doc # Read the markdown page, cross-check against Truth +################## +sub crossref_doc { + my $path = shift; # in: path to .md file + my $true_keys = shift; # in: AREF, list of keys from .go + + open my $fh, '<', $path + or die "$ME: Cannot read $path: $!\n";; + + my $unit = ''; + my %documented; + my @found_in_table; + my @described; + + # Helper function: when done reading description blocks, + # make sure that there's one block for each key listed + # in the table. Defined as a local function because we + # need to call it from two different places. + my $crossref_against_table = sub { + for my $k (@found_in_table) { + grep { $_ eq $k } @described + or warn "$ME: key not documented: '$k' listed in table for unit '$unit' but not actually documented\n"; + } + }; + + # Main loop: read the docs line by line + while (my $line = <$fh>) { + chomp $line; + + # New section, with its own '| table |' and '### Keyword blocks' + if ($line =~ /^##\s+(\S+)\s+units\s+\[(\S+)\]/) { + my $new_unit = $1; + $new_unit eq $2 + or warn "$ME: $path:$.: inconsistent block names in '$line'\n"; + + $crossref_against_table->(); + + $unit = $new_unit; + + # Reset, because each section has its own table & blocks + @found_in_table = (); + @described = (); + next; + } + + # Table line + if ($line =~ s/^\|\s+//) { + next if $line =~ /^\*\*/; # title + next if $line =~ /^-----/; # divider + + if ($line =~ /^([A-Z][A-Za-z6]+)=/) { + my $key = $1; + + grep { $_ eq $key } @$true_keys + or warn "$ME: $path:$.: unknown key '$key' (not present in $Go)\n"; + + # Sorting check + if (@found_in_table) { + if (lc($key) lt lc($found_in_table[-1])) { + warn "$ME: $path:$.: out-of-order key '$key' in table\n"; + } + } + + push @found_in_table, $key; + $documented{$key}++; + } + else { + warn "$ME: $path:$.: cannot grok table line '$line'\n"; + } + } + + # Description block + elsif ($line =~ /^###\s+`(\S+)=`/) { + my $key = $1; + + # Check for dups and for out-of-order + if (@described) { + if (lc($key) lt lc($described[-1])) { + warn "$ME: $path:$.: out-of-order key '$key'\n"; + } + if (grep { lc($_) eq lc($key) } @described) { + warn "$ME: $path:$.: duplicate key '$key'\n"; + } + } + + grep { $_ eq $key } @found_in_table + or warn "$ME: $path:$.: key '$key' is not listed in table for unit '$unit'\n"; + + push @described, $key; + $documented{$key}++; + } + } + + close $fh; + + # Final cross-check between table and description blocks + $crossref_against_table->(); + + # Check that no Go keys are missing + + (my $md_basename = $path) =~ s|^.*/||; + for my $k (@$true_keys) { + $documented{$k} + or warn "$ME: undocumented key: '$k' not found anywhere in $md_basename\n"; + } +} + +1; diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index e18857aa7d..62c2f53596 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -57,25 +57,27 @@ const ( KeyAuthFile = "AuthFile" KeyAutoUpdate = "AutoUpdate" KeyCertDir = "CertDir" - KeyCreds = "Creds" - KeyDecryptionKey = "DecryptionKey" KeyConfigMap = "ConfigMap" KeyContainerName = "ContainerName" KeyContainersConfModule = "ContainersConfModule" KeyCopy = "Copy" + KeyCreds = "Creds" + KeyDecryptionKey = "DecryptionKey" KeyDevice = "Device" + KeyDisableDNS = "DisableDNS" KeyDNS = "DNS" KeyDNSOption = "DNSOption" KeyDNSSearch = "DNSSearch" KeyDriver = "Driver" KeyDropCapability = "DropCapability" + KeyEntrypoint = "Entrypoint" KeyEnvironment = "Environment" KeyEnvironmentFile = "EnvironmentFile" KeyEnvironmentHost = "EnvironmentHost" - KeyEntrypoint = "Entrypoint" KeyExec = "Exec" KeyExitCodePropagation = "ExitCodePropagation" KeyExposeHostPort = "ExposeHostPort" + KeyGateway = "Gateway" KeyGIDMap = "GIDMap" KeyGlobalArgs = "GlobalArgs" KeyGroup = "Group" @@ -91,42 +93,37 @@ const ( KeyHealthStartupTimeout = "HealthStartupTimeout" KeyHealthTimeout = "HealthTimeout" KeyHostName = "HostName" - KeyIP = "IP" - KeyIP6 = "IP6" KeyImage = "Image" KeyImageTag = "ImageTag" + KeyInternal = "Internal" + KeyIP = "IP" + KeyIP6 = "IP6" + KeyIPAMDriver = "IPAMDriver" + KeyIPRange = "IPRange" + KeyIPv6 = "IPv6" KeyKubeDownForce = "KubeDownForce" KeyLabel = "Label" KeyLogDriver = "LogDriver" KeyMask = "Mask" KeyMount = "Mount" KeyNetwork = "Network" - KeyNetworkDisableDNS = "DisableDNS" - KeyNetworkDriver = "Driver" - KeyNetworkGateway = "Gateway" - KeyNetworkIPAMDriver = "IPAMDriver" - KeyNetworkIPRange = "IPRange" - KeyNetworkIPv6 = "IPv6" - KeyNetworkInternal = "Internal" KeyNetworkName = "NetworkName" - KeyNetworkOptions = "Options" - KeyNetworkSubnet = "Subnet" KeyNoNewPrivileges = "NoNewPrivileges" KeyNotify = "Notify" KeyOptions = "Options" KeyOS = "OS" KeyPidsLimit = "PidsLimit" + KeyPod = "Pod" KeyPodmanArgs = "PodmanArgs" KeyPodName = "PodName" - KeyPod = "Pod" KeyPublishPort = "PublishPort" KeyPull = "Pull" KeyReadOnly = "ReadOnly" KeyReadOnlyTmpfs = "ReadOnlyTmpfs" - KeyRemapGID = "RemapGid" - KeyRemapUID = "RemapUid" - KeyRemapUIDSize = "RemapUidSize" - KeyRemapUsers = "RemapUsers" + KeyRemapGid = "RemapGid" //nolint:stylecheck // deprecated + KeyRemapUid = "RemapUid" //nolint:stylecheck // deprecated + KeyRemapUidSize = "RemapUidSize" //nolint:stylecheck // deprecated + KeyRemapUsers = "RemapUsers" // deprecated KeyRootfs = "Rootfs" KeyRunInit = "RunInit" KeySeccompProfile = "SeccompProfile" @@ -140,6 +137,7 @@ const ( KeyShmSize = "ShmSize" KeyStopTimeout = "StopTimeout" KeySubGIDMap = "SubGIDMap" + KeySubnet = "Subnet" KeySubUIDMap = "SubUIDMap" KeySysctl = "Sysctl" KeyTimezone = "Timezone" @@ -152,7 +150,7 @@ const ( KeyUser = "User" KeyUserNS = "UserNS" KeyVariant = "Variant" - KeyVolatileTmp = "VolatileTmp" + KeyVolatileTmp = "VolatileTmp" // deprecated KeyVolume = "Volume" KeyVolumeName = "VolumeName" KeyWorkingDir = "WorkingDir" @@ -217,9 +215,9 @@ var ( KeyPull: true, KeyReadOnly: true, KeyReadOnlyTmpfs: true, - KeyRemapGID: true, - KeyRemapUID: true, - KeyRemapUIDSize: true, + KeyRemapGid: true, + KeyRemapUid: true, + KeyRemapUidSize: true, KeyRemapUsers: true, KeyRootfs: true, KeyRunInit: true, @@ -270,16 +268,16 @@ var ( KeyDNS: true, KeyContainersConfModule: true, KeyGlobalArgs: true, - KeyNetworkDisableDNS: true, - KeyNetworkDriver: true, - KeyNetworkGateway: true, - KeyNetworkIPAMDriver: true, - KeyNetworkIPRange: true, - KeyNetworkIPv6: true, - KeyNetworkInternal: true, + KeyDisableDNS: true, + KeyDriver: true, + KeyGateway: true, + KeyIPAMDriver: true, + KeyIPRange: true, + KeyIPv6: true, + KeyInternal: true, KeyNetworkName: true, - KeyNetworkOptions: true, - KeyNetworkSubnet: true, + KeyOptions: true, + KeySubnet: true, KeyPodmanArgs: true, } @@ -295,9 +293,9 @@ var ( KeyNetwork: true, KeyPodmanArgs: true, KeyPublishPort: true, - KeyRemapGID: true, - KeyRemapUID: true, - KeyRemapUIDSize: true, + KeyRemapGid: true, + KeyRemapUid: true, + KeyRemapUidSize: true, KeyRemapUsers: true, KeySetWorkingDirectory: true, KeyUserNS: true, @@ -826,7 +824,7 @@ func ConvertNetwork(network *parser.UnitFile, name string) (*parser.UnitFile, st podman.add("network", "create", "--ignore") - if disableDNS := network.LookupBooleanWithDefault(NetworkGroup, KeyNetworkDisableDNS, false); disableDNS { + if disableDNS := network.LookupBooleanWithDefault(NetworkGroup, KeyDisableDNS, false); disableDNS { podman.add("--disable-dns") } @@ -835,14 +833,14 @@ func ConvertNetwork(network *parser.UnitFile, name string) (*parser.UnitFile, st podman.addf("--dns=%s", ipAddr) } - driver, ok := network.Lookup(NetworkGroup, KeyNetworkDriver) + driver, ok := network.Lookup(NetworkGroup, KeyDriver) if ok && len(driver) > 0 { podman.addf("--driver=%s", driver) } - subnets := network.LookupAll(NetworkGroup, KeyNetworkSubnet) - gateways := network.LookupAll(NetworkGroup, KeyNetworkGateway) - ipRanges := network.LookupAll(NetworkGroup, KeyNetworkIPRange) + subnets := network.LookupAll(NetworkGroup, KeySubnet) + gateways := network.LookupAll(NetworkGroup, KeyGateway) + ipRanges := network.LookupAll(NetworkGroup, KeyIPRange) if len(subnets) > 0 { if len(gateways) > len(subnets) { return nil, "", fmt.Errorf("cannot set more gateways than subnets") @@ -863,19 +861,19 @@ func ConvertNetwork(network *parser.UnitFile, name string) (*parser.UnitFile, st return nil, "", fmt.Errorf("cannot set gateway or range without subnet") } - if internal := network.LookupBooleanWithDefault(NetworkGroup, KeyNetworkInternal, false); internal { + if internal := network.LookupBooleanWithDefault(NetworkGroup, KeyInternal, false); internal { podman.add("--internal") } - if ipamDriver, ok := network.Lookup(NetworkGroup, KeyNetworkIPAMDriver); ok && len(ipamDriver) > 0 { + if ipamDriver, ok := network.Lookup(NetworkGroup, KeyIPAMDriver); ok && len(ipamDriver) > 0 { podman.addf("--ipam-driver=%s", ipamDriver) } - if ipv6 := network.LookupBooleanWithDefault(NetworkGroup, KeyNetworkIPv6, false); ipv6 { + if ipv6 := network.LookupBooleanWithDefault(NetworkGroup, KeyIPv6, false); ipv6 { podman.add("--ipv6") } - networkOptions := network.LookupAllKeyVal(NetworkGroup, KeyNetworkOptions) + networkOptions := network.LookupAllKeyVal(NetworkGroup, KeyOptions) if len(networkOptions) > 0 { podman.addKeys("--opt", networkOptions) } @@ -1390,8 +1388,8 @@ func handleUserMappings(unitFile *parser.UnitFile, groupName string, podman *Pod } if mappingsDefined { - _, hasRemapUID := unitFile.Lookup(groupName, KeyRemapUID) - _, hasRemapGID := unitFile.Lookup(groupName, KeyRemapGID) + _, hasRemapUID := unitFile.Lookup(groupName, KeyRemapUid) + _, hasRemapGID := unitFile.Lookup(groupName, KeyRemapGid) _, RemapUsers := unitFile.LookupLast(groupName, KeyRemapUsers) if hasRemapUID || hasRemapGID || RemapUsers { return fmt.Errorf("deprecated Remap keys are set along with explicit mapping keys") @@ -1403,8 +1401,8 @@ func handleUserMappings(unitFile *parser.UnitFile, groupName string, podman *Pod } func handleUserRemap(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline, isUser, supportManual bool) error { - uidMaps := unitFile.LookupAllStrv(groupName, KeyRemapUID) - gidMaps := unitFile.LookupAllStrv(groupName, KeyRemapGID) + uidMaps := unitFile.LookupAllStrv(groupName, KeyRemapUid) + gidMaps := unitFile.LookupAllStrv(groupName, KeyRemapGid) remapUsers, _ := unitFile.LookupLast(groupName, KeyRemapUsers) switch remapUsers { case "": @@ -1433,7 +1431,7 @@ func handleUserRemap(unitFile *parser.UnitFile, groupName string, podman *Podman for _, gidMap := range gidMaps { autoOpts = append(autoOpts, "gidmapping="+gidMap) } - uidSize := unitFile.LookupUint32(groupName, KeyRemapUIDSize, 0) + uidSize := unitFile.LookupUint32(groupName, KeyRemapUidSize, 0) if uidSize > 0 { autoOpts = append(autoOpts, fmt.Sprintf("size=%v", uidSize)) }