Merge pull request #16688 from ygalblum/quadlet_network_file

Quadlet: add network support
This commit is contained in:
OpenShift Merge Robot
2022-12-19 14:16:53 -05:00
committed by GitHub
25 changed files with 427 additions and 38 deletions

View File

@ -40,6 +40,7 @@ var (
".container": void,
".volume": void,
".kube": void,
".network": void,
}
)
@ -337,6 +338,8 @@ func main() {
service, err = quadlet.ConvertVolume(unit, name)
case strings.HasSuffix(name, ".kube"):
service, err = quadlet.ConvertKube(unit, isUser)
case strings.HasSuffix(name, ".network"):
service, err = quadlet.ConvertNetwork(unit, name)
default:
Logf("Unsupported file type '%s'", name)
continue

View File

@ -310,9 +310,85 @@ Set one or more OCI labels on the volume. The format is a list of
This key can be listed multiple times.
### Network units
Set one or more OCI labels on the volume. The format is a list of `key=value` items,
similar to `Environment`.
Network files are named with a `.network` extension and contain a section `[Network]` describing the
named Podman network. The generated service is a one-time command that ensures that the network
exists on the host, creating it if needed.
For a network file named `$NAME.network`, the generated Podman network will be called `systemd-$NAME`,
and the generated service file `$NAME-network.service`.
Using network units allows containers to depend on networks being automatically pre-created. This is
particularly interesting when using special options to control network creation, as Podman will
otherwise create networks with the default options.
Supported keys in `Network` section are:
#### `DisableDNS=` (defaults to `no`)
If enabled, disables the DNS plugin for this network.
This is equivalent to the Podman `--disable-dns` option
#### `Driver=` (defaults to `bridge`)
Driver to manage the network. Currently `bridge`, `macvlan` and `ipvlan` are supported.
This is equivalent to the Podman `--driver` option
#### `Gateway=`
Define a gateway for the subnet. If you want to provide a gateway address, you must also provide a subnet option.
This is equivalent to the Podman `--gateway` option
This key can be listed multiple times.
#### `Internal=` (defaults to `no`)
Restrict external access of this network.
This is equivalent to the Podman `--internal` option
#### `IPRange=`
Allocate container IP from a range. The range must be a complete subnet and in CIDR notation. The ip-range option must be used with a subnet option.
This is equivalent to the Podman `--ip-range` option
This key can be listed multiple times.
#### `IPAMDriver=`
Set the ipam driver (IP Address Management Driver) for the network. Currently `host-local`, `dhcp` and `none` are supported.
This is equivalent to the Podman `--ipam-driver` option
#### `IPv6=`
Enable IPv6 (Dual Stack) networking.
This is equivalent to the Podman `--ipv6` option
#### `Options=`
Set driver specific options.
This is equivalent to the Podman `--opt` option
#### `Subnet=`
The subnet in CIDR notation.
This is equivalent to the Podman `--subnet` option
This key can be listed multiple times.
#### `Label=`
Set one or more OCI labels on the network. The format is a list of
`key=value` items, similar to `Environment`.
This key can be listed multiple times.
@ -351,7 +427,17 @@ Group=projectname
Label=org.test.Key=value
```
Example `test.network`:
```
[Network]
Subnet=172.16.0.0/24
Gateway=172.16.0.1
IPRange=172.16.0.0/28
Label=org.test.Key=value
```
## SEE ALSO
**[systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/systemd.unit.html)**,
**[systemd.service(5)](https://www.freedesktop.org/software/systemd/man/systemd.service.html)**,
**[podman-run(1)](podman-run.1.md)**
**[podman-network-create(1)](podman-network-create.1.md)**

View File

@ -25,40 +25,51 @@ const (
XVolumeGroup = "X-Volume"
KubeGroup = "Kube"
XKubeGroup = "X-Kube"
NetworkGroup = "Network"
XNetworkGroup = "X-Network"
)
var validPortRange = regexp.MustCompile(`\d+(-\d+)?(/udp|/tcp)?$`)
// All the supported quadlet keys
const (
KeyContainerName = "ContainerName"
KeyImage = "Image"
KeyEnvironment = "Environment"
KeyExec = "Exec"
KeyNoNewPrivileges = "NoNewPrivileges"
KeyDropCapability = "DropCapability"
KeyAddCapability = "AddCapability"
KeyReadOnly = "ReadOnly"
KeyRemapUsers = "RemapUsers"
KeyRemapUID = "RemapUid"
KeyRemapGID = "RemapGid"
KeyRemapUIDSize = "RemapUidSize"
KeyNotify = "Notify"
KeyExposeHostPort = "ExposeHostPort"
KeyPublishPort = "PublishPort"
KeyUser = "User"
KeyGroup = "Group"
KeyVolume = "Volume"
KeyPodmanArgs = "PodmanArgs"
KeyLabel = "Label"
KeyAnnotation = "Annotation"
KeyRunInit = "RunInit"
KeyVolatileTmp = "VolatileTmp"
KeyTimezone = "Timezone"
KeySeccompProfile = "SeccompProfile"
KeyAddDevice = "AddDevice"
KeyNetwork = "Network"
KeyYaml = "Yaml"
KeyContainerName = "ContainerName"
KeyImage = "Image"
KeyEnvironment = "Environment"
KeyExec = "Exec"
KeyNoNewPrivileges = "NoNewPrivileges"
KeyDropCapability = "DropCapability"
KeyAddCapability = "AddCapability"
KeyReadOnly = "ReadOnly"
KeyRemapUsers = "RemapUsers"
KeyRemapUID = "RemapUid"
KeyRemapGID = "RemapGid"
KeyRemapUIDSize = "RemapUidSize"
KeyNotify = "Notify"
KeyExposeHostPort = "ExposeHostPort"
KeyPublishPort = "PublishPort"
KeyUser = "User"
KeyGroup = "Group"
KeyVolume = "Volume"
KeyPodmanArgs = "PodmanArgs"
KeyLabel = "Label"
KeyAnnotation = "Annotation"
KeyRunInit = "RunInit"
KeyVolatileTmp = "VolatileTmp"
KeyTimezone = "Timezone"
KeySeccompProfile = "SeccompProfile"
KeyAddDevice = "AddDevice"
KeyNetwork = "Network"
KeyYaml = "Yaml"
KeyNetworkDisableDNS = "DisableDNS"
KeyNetworkDriver = "Driver"
KeyNetworkGateway = "Gateway"
KeyNetworkInternal = "Internal"
KeyNetworkIPRange = "IPRange"
KeyNetworkIPAMDriver = "IPAMDriver"
KeyNetworkIPv6 = "IPv6"
KeyNetworkOptions = "Options"
KeyNetworkSubnet = "Subnet"
)
// Supported keys in "Container" group
@ -99,6 +110,20 @@ var supportedVolumeKeys = map[string]bool{
KeyLabel: true,
}
// Supported keys in "Volume" group
var supportedNetworkKeys = map[string]bool{
KeyNetworkDisableDNS: true,
KeyNetworkDriver: true,
KeyNetworkGateway: true,
KeyNetworkInternal: true,
KeyNetworkIPRange: true,
KeyNetworkIPAMDriver: true,
KeyNetworkIPv6: true,
KeyNetworkOptions: true,
KeyNetworkSubnet: true,
KeyLabel: true,
}
// Supported keys in "Kube" group
var supportedKubeKeys = map[string]bool{
KeyYaml: true,
@ -106,6 +131,7 @@ var supportedKubeKeys = map[string]bool{
KeyRemapGID: true,
KeyRemapUsers: true,
KeyRemapUIDSize: true,
KeyNetwork: true,
}
func replaceExtension(name string, extension string, extraPrefix string, extraSuffix string) string {
@ -266,12 +292,7 @@ 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)
}
}
addNetworks(container, ContainerGroup, service, podman)
// Run with a pid1 init to reap zombies by default (as most apps don't do that)
runInit := container.LookupBoolean(ContainerGroup, KeyRunInit, false)
@ -494,10 +515,99 @@ func ConvertContainer(container *parser.UnitFile, isUser bool) (*parser.UnitFile
return service, nil
}
// Convert a quadlet network file (unit file with a Network group) to a systemd
// service file (unit file with Service group) based on the options in the
// Network group.
// The original Network group is kept around as X-Network.
func ConvertNetwork(network *parser.UnitFile, name string) (*parser.UnitFile, error) {
service := network.Dup()
service.Filename = replaceExtension(network.Filename, ".service", "", "-network")
if err := checkForUnknownKeys(network, NetworkGroup, supportedNetworkKeys); err != nil {
return nil, err
}
/* Rename old Network group to x-Network so that systemd ignores it */
service.RenameGroup(NetworkGroup, XNetworkGroup)
networkName := replaceExtension(name, "", "systemd-", "")
// Need the containers filesystem mounted to start podman
service.Add(UnitGroup, "RequiresMountsFor", "%t/containers")
podman := NewPodmanCmdline("network", "create", "--ignore")
if disableDNS := network.LookupBoolean(NetworkGroup, KeyNetworkDisableDNS, false); disableDNS {
podman.add("--disable-dns")
}
driver, ok := network.Lookup(NetworkGroup, KeyNetworkDriver)
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)
if len(subnets) > 0 {
if len(gateways) > len(subnets) {
return nil, fmt.Errorf("cannot set more gateways than subnets")
}
if len(ipRanges) > len(subnets) {
return nil, fmt.Errorf("cannot set more ranges than subnets")
}
for i := range subnets {
podman.addf("--subnet=%s", subnets[i])
if len(gateways) > i {
podman.addf("--gateway=%s", gateways[i])
}
if len(ipRanges) > i {
podman.addf("--ip-range=%s", ipRanges[i])
}
}
} else if len(ipRanges) > 0 || len(gateways) > 0 {
return nil, fmt.Errorf("cannot set gateway or range without subnet")
}
if internal := network.LookupBoolean(NetworkGroup, KeyNetworkInternal, false); internal {
podman.add("--internal")
}
if ipamDriver, ok := network.Lookup(NetworkGroup, KeyNetworkIPAMDriver); ok && len(ipamDriver) > 0 {
podman.addf("--ipam-driver=%s", ipamDriver)
}
if ipv6 := network.LookupBoolean(NetworkGroup, KeyNetworkIPv6, false); ipv6 {
podman.add("--ipv6")
}
networkOptions := network.LookupAllKeyVal(NetworkGroup, KeyNetworkOptions)
if len(networkOptions) > 0 {
podman.addKeys("--opt", networkOptions)
}
if labels := network.LookupAllKeyVal(NetworkGroup, KeyLabel); len(labels) > 0 {
podman.addLabels(labels)
}
podman.add(networkName)
service.AddCmdline(ServiceGroup, "ExecStart", podman.Args)
service.Setv(ServiceGroup,
"Type", "oneshot",
"RemainAfterExit", "yes",
// The default syslog identifier is the exec basename (podman) which isn't very useful here
"SyslogIdentifier", "%N")
return service, nil
}
// Convert a quadlet volume file (unit file with a Volume group) to a systemd
// service file (unit file with Service group) based on the options in the
// Volume group.
// The original Container group is kept around as X-Container.
// The original Volume group is kept around as X-Volume.
func ConvertVolume(volume *parser.UnitFile, name string) (*parser.UnitFile, error) {
service := volume.Dup()
service.Filename = replaceExtension(volume.Filename, ".service", "", "-volume")
@ -627,6 +737,8 @@ func ConvertKube(kube *parser.UnitFile, isUser bool) (*parser.UnitFile, error) {
return nil, err
}
addNetworks(kube, KubeGroup, service, execStart)
execStart.add(yamlPath)
service.AddCmdline(ServiceGroup, "ExecStart", execStart.Args)
@ -686,3 +798,30 @@ func handleUserRemap(unitFile *parser.UnitFile, groupName string, podman *Podman
return nil
}
func addNetworks(quadletUnitFile *parser.UnitFile, groupName string, serviceUnitFile *parser.UnitFile, podman *PodmanCmdline) {
networks := quadletUnitFile.LookupAll(groupName, KeyNetwork)
for _, network := range networks {
if len(network) > 0 {
networkName, options, found := strings.Cut(network, ":")
if strings.HasSuffix(networkName, ".network") {
// the podman network name is systemd-$name
networkName = replaceExtension(networkName, "", "systemd-", "")
// the systemd unit name is $name-network.service
networkServiceName := replaceExtension(networkName, ".service", "", "-network")
serviceUnitFile.Add(UnitGroup, "Requires", networkServiceName)
serviceUnitFile.Add(UnitGroup, "After", networkServiceName)
if found {
network = fmt.Sprintf("%s:%s", networkName, options)
} else {
network = networkName
}
}
podman.addf("--network=%s", network)
}
}
}

View File

@ -0,0 +1,7 @@
## assert-key-is Unit RequiresMountsFor "%t/containers"
## assert-key-is Service Type oneshot
## assert-key-is Service RemainAfterExit yes
## assert-key-is-regex Service ExecStart ".*/podman network create --ignore systemd-basic"
## assert-key-is Service SyslogIdentifier "%N"
[Network]

View File

@ -0,0 +1,5 @@
## assert-podman-final-args systemd-disable-dns
## assert-podman-args "--disable-dns"
[Network]
DisableDNS=yes

View File

@ -0,0 +1,5 @@
## assert-podman-final-args systemd-driver
## assert-podman-args "--driver=macvlan"
[Network]
Driver=macvlan

View File

@ -0,0 +1,7 @@
## assert-failed
## assert-stderr-contains "cannot set more gateways than subnets"
[Network]
Subnet=192.168.1.0/24
Gateway=192.168.1.1
Gateway=192.168.2.1

View File

@ -0,0 +1,6 @@
## assert-podman-final-args systemd-gateway
## assert-podman-args "--subnet=192.168.1.0/24" "--gateway=192.168.1.1"
[Network]
Subnet=192.168.1.0/24
Gateway=192.168.1.1

View File

@ -0,0 +1,5 @@
## assert-failed
## assert-stderr-contains "cannot set gateway or range without subnet"
[Network]
Gateway=192.168.1.1

View File

@ -0,0 +1,5 @@
## assert-podman-final-args systemd-internal
## assert-podman-args "--internal"
[Network]
Internal=yes

View File

@ -0,0 +1,5 @@
## assert-podman-final-args systemd-ipam-driver
## assert-podman-args "--ipam-driver=dhcp"
[Network]
IPAMDriver=dhcp

View File

@ -0,0 +1,5 @@
## assert-podman-final-args systemd-ipv6
## assert-podman-args "--ipv6"
[Network]
IPv6=yes

View File

@ -0,0 +1,11 @@
## assert-podman-final-args systemd-label
## 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"
[Network]
Label=org.foo.Arg1=arg1 "org.foo.Arg2=arg 2" \
org.foo.Arg3=arg3
Label=org.foo.Arg0=arg0

View File

@ -0,0 +1,5 @@
## assert-podman-args "--network=basic"
[Kube]
Yaml=deployment.yml
Network=basic

View File

@ -0,0 +1,7 @@
## assert-podman-args "--network=systemd-basic"
## assert-key-is "Unit" "Requires" "systemd-basic-network.service"
## assert-key-is "Unit" "After" "systemd-basic-network.service"
[Container]
Image=localhost/imagename
Network=basic.network

View File

@ -0,0 +1,8 @@
## assert-podman-args "--network=systemd-basic"
## assert-key-is "Unit" "Requires" "systemd-basic-network.service"
## assert-key-is "Unit" "After" "systemd-basic-network.service"
[Kube]
Yaml=deployment.yml
Network=basic.network

View File

@ -0,0 +1,7 @@
## assert-podman-final-args systemd-options.multiple
## assert-podman-args "--opt" "mtu=1504"
## assert-podman-args "--opt" "isolate=true"
[Network]
Options=mtu=1504
Options=isolate=true

View File

@ -0,0 +1,5 @@
## assert-podman-final-args systemd-options
## assert-podman-args "--opt" "mtu=1504"
[Network]
Options=mtu=1504

View File

@ -0,0 +1,7 @@
## assert-failed
## assert-stderr-contains "cannot set more ranges than subnets"
[Network]
Subnet=192.168.1.0/24
IPRange=192.168.1.0/32
IPRange=192.168.1.127/32

View File

@ -0,0 +1,6 @@
## assert-podman-final-args systemd-range
## assert-podman-args "--subnet=192.168.1.0/24" "--ip-range=192.168.1.0/32"
[Network]
Subnet=192.168.1.0/24
IPRange=192.168.1.0/32

View File

@ -0,0 +1,5 @@
## assert-failed
## assert-stderr-contains "cannot set gateway or range without subnet"
[Network]
IPRange=192.168.1.0/32

View File

@ -0,0 +1,11 @@
## assert-podman-final-args systemd-subnet-trio.multiple
## assert-podman-args "--subnet=192.168.1.0/24" "--gateway=192.168.1.1" "--ip-range=192.168.1.0/32"
## assert-podman-args "--subnet=192.168.2.0/24" "--gateway=192.168.2.1" "--ip-range=192.168.2.0/32"
[Network]
Subnet=192.168.1.0/24
Subnet=192.168.2.0/24
Gateway=192.168.1.1
Gateway=192.168.2.1
IPRange=192.168.1.0/32
IPRange=192.168.2.0/32

View File

@ -0,0 +1,7 @@
## assert-podman-final-args systemd-subnet-trio
## assert-podman-args "--subnet=192.168.1.0/24" "--gateway=192.168.1.1" "--ip-range=192.168.1.0/32"
[Network]
Subnet=192.168.1.0/24
Gateway=192.168.1.1
IPRange=192.168.1.0/32

View File

@ -0,0 +1,7 @@
## assert-podman-final-args systemd-subnets
## assert-podman-args "--subnet=192.168.0.0/24"
## assert-podman-args "--subnet=192.168.1.0/24"
[Network]
Subnet=192.168.0.0/24
Subnet=192.168.1.0/24

View File

@ -30,8 +30,11 @@ func loadQuadletTestcase(path string) *quadletTestcase {
base := filepath.Base(path)
ext := filepath.Ext(base)
service := base[:len(base)-len(ext)]
if ext == ".volume" {
switch ext {
case ".volume":
service += "-volume"
case ".network":
service += "-network"
}
service += ".service"
@ -360,6 +363,7 @@ var _ = Describe("quadlet system generator", func() {
Entry("label.container", "label.container"),
Entry("name.container", "name.container"),
Entry("network.container", "network.container"),
Entry("network.quadlet.container", "network.quadlet.container"),
Entry("noimage.container", "noimage.container"),
Entry("notify.container", "notify.container"),
Entry("other-sections.container", "other-sections.container"),
@ -388,6 +392,27 @@ var _ = Describe("quadlet system generator", func() {
Entry("Kube - User Remap Manual", "remap-manual.kube"),
Entry("Kube - User Remap Auto", "remap-auto.kube"),
Entry("Kube - User Remap Auto with IDs", "remap-auto2.kube"),
Entry("Kube - Network", "network.kube"),
Entry("Kube - Quadlet Network", "network.quadlet.kube"),
Entry("Network - Basic", "basic.network"),
Entry("Network - Label", "label.network"),
Entry("Network - Disable DNS", "disable-dns.network"),
Entry("Network - Driver", "driver.network"),
Entry("Network - Subnets", "subnets.network"),
Entry("Network - Gateway", "gateway.network"),
Entry("Network - Gateway without Subnet", "gateway.no-subnet.network"),
Entry("Network - Gateway not enough Subnet", "gateway.less-subnet.network"),
Entry("Network - Range", "range.network"),
Entry("Network - Range without Subnet", "range.no-subnet.network"),
Entry("Network - Range not enough Subnet", "range.less-subnet.network"),
Entry("Network - subnet, gateway and range", "subnet-trio.network"),
Entry("Network - multiple subnet, gateway and range", "subnet-trio.multiple.network"),
Entry("Network - Internal network", "internal.network"),
Entry("Network - IPAM Driver", "ipam-driver.network"),
Entry("Network - IPv6", "ipv6.network"),
Entry("Network - Options", "options.network"),
Entry("Network - Multiple Options", "options.multiple.network"),
)
})