Skip to content

Commit

Permalink
Merge pull request containers#16688 from ygalblum/quadlet_network_file
Browse files Browse the repository at this point in the history
Quadlet: add network support
  • Loading branch information
openshift-merge-robot authored Dec 19, 2022
2 parents 2be7238 + d974a79 commit b0acb59
Show file tree
Hide file tree
Showing 25 changed files with 427 additions and 38 deletions.
3 changes: 3 additions & 0 deletions cmd/quadlet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var (
".container": void,
".volume": void,
".kube": void,
".network": void,
}
)

Expand Down Expand Up @@ -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
Expand Down
90 changes: 88 additions & 2 deletions docs/source/markdown/podman-systemd.unit.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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)**
209 changes: 174 additions & 35 deletions pkg/systemd/quadlet/quadlet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -99,13 +110,28 @@ 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,
KeyRemapUID: true,
KeyRemapGID: true,
KeyRemapUsers: true,
KeyRemapUIDSize: true,
KeyNetwork: true,
}

func replaceExtension(name string, extension string, extraPrefix string, extraSuffix string) string {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
}
7 changes: 7 additions & 0 deletions test/e2e/quadlet/basic.network
Original file line number Diff line number Diff line change
@@ -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]
Loading

0 comments on commit b0acb59

Please sign in to comment.