Skip to content

Commit 32d6c54

Browse files
Merge pull request #26624 from Odilhao/artifact-quadlet-implementation
Add artifact quadlet unit type support
2 parents d5b5710 + 34254cd commit 32d6c54

File tree

11 files changed

+368
-11
lines changed

11 files changed

+368
-11
lines changed

cmd/quadlet/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ func generateUnitsInfoMap(units []*parser.UnitFile) map[string]*quadlet.UnitInfo
396396
// types), but still breaks the dependency cycle between .volume and .build ([Volume] can
397397
// have Image=some.build, and [Build] can have Volume=some.volume:/some-volume)
398398
resourceName = quadlet.GetBuiltImageName(unit)
399+
case strings.HasSuffix(unit.Filename, ".artifact"):
400+
serviceName = quadlet.GetArtifactServiceName(unit)
399401
case strings.HasSuffix(unit.Filename, ".pod"):
400402
containers = make([]string, 0)
401403
// Prefill resouceNames for .pod files.
@@ -550,6 +552,9 @@ func process() bool {
550552
service, err = quadlet.ConvertImage(unit, unitsInfoMap, isUserFlag)
551553
case strings.HasSuffix(unit.Filename, ".build"):
552554
service, warnings, err = quadlet.ConvertBuild(unit, unitsInfoMap, isUserFlag)
555+
case strings.HasSuffix(unit.Filename, ".artifact"):
556+
warnIfAmbiguousName(unit, quadlet.ArtifactGroup)
557+
service, err = quadlet.ConvertArtifact(unit, unitsInfoMap, isUserFlag)
553558
case strings.HasSuffix(unit.Filename, ".pod"):
554559
service, warnings, err = quadlet.ConvertPod(unit, unitsInfoMap, isUserFlag)
555560
default:

docs/source/markdown/podman-systemd.unit.5.md

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ podman\-systemd.unit - systemd units using Podman Quadlet
66

77
## SYNOPSIS
88

9-
*name*.container, *name*.volume, *name*.network, *name*.kube *name*.image, *name*.build *name*.pod
9+
*name*.container, *name*.volume, *name*.network, *name*.kube *name*.image, *name*.build *name*.pod, *name*.artifact
1010

1111
### Podman rootful unit search path
1212

@@ -48,7 +48,7 @@ the [Service] table and [Install] tables pass directly to systemd and are handle
4848
See systemd.unit(5) man page for more information.
4949

5050
The Podman generator reads the search paths above and reads files with the extensions `.container`
51-
`.volume`, `.network`, `.build`, `.pod` and `.kube`, and for each file generates a similarly named `.service` file. Be aware that
51+
`.volume`, `.network`, `.build`, `.pod`, `.kube`, and `.artifact`, and for each file generates a similarly named `.service` file. Be aware that
5252
existing vendor services (i.e., in `/usr/`) are replaced if they have the same name. The generated unit files can
5353
be started and managed with `systemctl` like any other systemd service. `systemctl {--user} list-unit-files`
5454
lists existing unit files on the system.
@@ -104,7 +104,7 @@ Quadlet requires the use of cgroup v2, use `podman info --format {{.Host.Cgroups
104104

105105
By default, the `Type` field of the `Service` section of the Quadlet file does not need to be set.
106106
Quadlet will set it to `notify` for `.container` and `.kube` files,
107-
`forking` for `.pod` files, and `oneshot` for `.volume`, `.network`, `.build`, and `.image` files.
107+
`forking` for `.pod` files, and `oneshot` for `.volume`, `.network`, `.build`, `.image`, and `.artifact` files.
108108

109109
However, `Type` may be explicitly set to `oneshot` for `.container` and `.kube` files when no containers are expected
110110
to run once `podman` exits.
@@ -2098,6 +2098,123 @@ Override the default architecture variant of the container image.
20982098

20992099
This is equivalent to the Podman `--variant` option.
21002100

2101+
## Artifact units [Artifact]
2102+
2103+
### WARNING: Experimental Unit
2104+
2105+
This unit is considered experimental and still in development. Inputs, options, and outputs are all subject to change.
2106+
2107+
Artifact units are named with a `.artifact` extension and contain a `[Artifact]` section describing
2108+
the container artifact pull command. The generated service is a one-time command that ensures that the artifact
2109+
exists on the host, pulling it if needed.
2110+
2111+
Using artifact units allows containers to depend on artifacts being automatically pulled. This is
2112+
particularly useful for managing artifacts that containers need to mount or access, the **Artifact** key is mandatory inside of the [Artifact] unit.
2113+
2114+
Valid options for `[Artifact]` are listed below:
2115+
2116+
| **[Artifact] options** | **podman artifact pull equivalent** |
2117+
|---------------------------------------------|--------------------------------------------------------|
2118+
| Artifact=quay\.io/foobar/artifact:special | podman artifact pull quay\.io/foobar/artifact:special |
2119+
| AuthFile=/etc/registry/auth\.json | --authfile=/etc/registry/auth\.json |
2120+
| CertDir=/etc/registry/certs | --cert-dir=/etc/registry/certs |
2121+
| ContainersConfModule=/etc/nvd\.conf | --module=/etc/nvd\.conf |
2122+
| Creds=username:password | --creds=username:password |
2123+
| DecryptionKey=/etc/registry\.key | --decryption-key=/etc/registry\.key |
2124+
| GlobalArgs=--log-level=debug | --log-level=debug |
2125+
| PodmanArgs=--pull never | --pull never |
2126+
| Quiet=true | --quiet |
2127+
| Retry=5 | --retry=5 |
2128+
| RetryDelay=10s | --retry-delay=10s |
2129+
| ServiceName=my-artifact | Set the systemd service name to my-artifact.service |
2130+
| TLSVerify=false | --tls-verify=false |
2131+
2132+
### `Artifact=`
2133+
2134+
The artifact to pull from a registry onto the local machine. This is the only required key for artifact units.
2135+
2136+
It is required to use a fully qualified artifact name rather than a short name, both for
2137+
performance and robustness reasons.
2138+
2139+
### `AuthFile=`
2140+
2141+
Path of the authentication file.
2142+
2143+
This is equivalent to the Podman `--authfile` option.
2144+
2145+
### `CertDir=`
2146+
2147+
Use certificates at path (*.crt, *.cert, *.key) to connect to the registry.
2148+
2149+
This is equivalent to the Podman `--cert-dir` option.
2150+
2151+
### `ContainersConfModule=`
2152+
2153+
Load the specified containers.conf(5) module. Equivalent to the Podman `--module` option.
2154+
2155+
This key can be listed multiple times.
2156+
2157+
### `Creds=`
2158+
2159+
The credentials to use when contacting the registry in the format `[username[:password]]`.
2160+
2161+
This is equivalent to the Podman `--creds` option.
2162+
2163+
### `DecryptionKey=`
2164+
2165+
The `[key[:passphrase]]` to be used for decryption of artifacts.
2166+
2167+
This is equivalent to the Podman `--decryption-key` option.
2168+
2169+
### `GlobalArgs=`
2170+
2171+
This key contains a list of arguments passed directly between `podman` and `artifact`
2172+
in the generated file. It can be used to access Podman features otherwise unsupported by the generator. Since the generator is unaware
2173+
of what unexpected interactions can be caused by these arguments, it is not recommended to use
2174+
this option.
2175+
2176+
The format of this is a space separated list of arguments, which can optionally be individually
2177+
escaped to allow inclusion of whitespace and other control characters.
2178+
2179+
This key can be listed multiple times.
2180+
2181+
### `PodmanArgs=`
2182+
2183+
This key contains a list of arguments passed directly to the end of the `podman artifact pull` command
2184+
in the generated file (right before the artifact name in the command line). It can be used to
2185+
access Podman features otherwise unsupported by the generator. Since the generator is unaware
2186+
of what unexpected interactions can be caused by these arguments, it is not recommended to use
2187+
this option.
2188+
2189+
The format of this is a space separated list of arguments, which can optionally be individually
2190+
escaped to allow inclusion of whitespace and other control characters.
2191+
2192+
This key can be listed multiple times.
2193+
2194+
### `Quiet=`
2195+
2196+
Suppress output information when pulling artifacts.
2197+
2198+
This is equivalent to the Podman `--quiet` option.
2199+
2200+
### `Retry=`
2201+
2202+
Number of times to retry the artifact pull when a HTTP error occurs. Equivalent to the Podman `--retry` option.
2203+
2204+
### `RetryDelay=`
2205+
2206+
Delay between retries. Equivalent to the Podman `--retry-delay` option.
2207+
2208+
### `ServiceName=`
2209+
2210+
The (optional) name of the systemd service. If this is not specified, the default value is the same name as the unit, but with a `-artifact` suffix, i.e. a `$name.artifact` file creates a `$name-artifact.service` systemd service.
2211+
2212+
### `TLSVerify=`
2213+
2214+
Require HTTPS and verification of certificates when contacting registries.
2215+
2216+
This is equivalent to the Podman `--tls-verify` option.
2217+
21012218
## Quadlet section [Quadlet]
21022219
Some quadlet specific configuration is shared between different unit types. Those settings
21032220
can be configured in the `[Quadlet]` section.
@@ -2202,6 +2319,29 @@ IPRange=172.16.0.0/28
22022319
Label=org.test.Key=value
22032320
```
22042321

2322+
Example `test.artifact` to only pull the artifact using one auth file:
2323+
```
2324+
[Artifact]
2325+
Artifact=quay.io/example/my-artifact:latest
2326+
AuthFile=/etc/registry/auth.json
2327+
TLSVerify=false
2328+
```
2329+
2330+
Example usage where a container depends on an artifact:
2331+
2332+
`my-artifact.artifact`:
2333+
```
2334+
[Artifact]
2335+
Artifact=quay.io/example/my-config:latest
2336+
```
2337+
2338+
`my-app.container`:
2339+
```
2340+
[Container]
2341+
Image=quay.io/example/my-app:latest
2342+
Mount=type=artifact,source=my-artifact.artifact,destination=/etc/config
2343+
```
2344+
22052345
Example for Container in a Pod:
22062346

22072347
`test.pod`

pkg/systemd/quadlet/quadlet.go

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
UnitDirDistro = "/usr/share/containers/systemd"
2828

2929
// Names of commonly used systemd/quadlet group names
30+
ArtifactGroup = "Artifact"
3031
ContainerGroup = "Container"
3132
InstallGroup = "Install"
3233
KubeGroup = "Kube"
@@ -38,6 +39,7 @@ const (
3839
ImageGroup = "Image"
3940
BuildGroup = "Build"
4041
QuadletGroup = "Quadlet"
42+
XArtifactGroup = "X-Artifact"
4143
XContainerGroup = "X-Container"
4244
XKubeGroup = "X-Kube"
4345
XNetworkGroup = "X-Network"
@@ -61,6 +63,7 @@ const (
6163
KeyAllTags = "AllTags"
6264
KeyAnnotation = "Annotation"
6365
KeyArch = "Arch"
66+
KeyArtifact = "Artifact"
6467
KeyAuthFile = "AuthFile"
6568
KeyAutoUpdate = "AutoUpdate"
6669
KeyCertDir = "CertDir"
@@ -141,6 +144,7 @@ const (
141144
KeyPolicy = "Policy"
142145
KeyPublishPort = "PublishPort"
143146
KeyPull = "Pull"
147+
KeyQuiet = "Quiet"
144148
KeyReadOnly = "ReadOnly"
145149
KeyReadOnlyTmpfs = "ReadOnlyTmpfs"
146150
KeyReloadCmd = "ReloadCmd"
@@ -458,6 +462,25 @@ var (
458462
KeyVolume: true,
459463
},
460464
},
465+
ArtifactGroup: {
466+
GroupName: ArtifactGroup,
467+
XGroupName: XArtifactGroup,
468+
SupportedKeys: map[string]bool{
469+
KeyArtifact: true,
470+
KeyAuthFile: true,
471+
KeyCertDir: true,
472+
KeyContainersConfModule: true,
473+
KeyCreds: true,
474+
KeyDecryptionKey: true,
475+
KeyGlobalArgs: true,
476+
KeyPodmanArgs: true,
477+
KeyQuiet: true,
478+
KeyRetry: true,
479+
KeyRetryDelay: true,
480+
KeyServiceName: true,
481+
KeyTLSVerify: true,
482+
},
483+
},
461484
PodGroup: {
462485
GroupName: PodGroup,
463486
XGroupName: XPodGroup,
@@ -1472,6 +1495,8 @@ func GetUnitServiceName(unit *parser.UnitFile) (string, error) {
14721495
return GetImageServiceName(unit), nil
14731496
case strings.HasSuffix(unit.Filename, ".build"):
14741497
return GetBuildServiceName(unit), nil
1498+
case strings.HasSuffix(unit.Filename, ".artifact"):
1499+
return GetArtifactServiceName(unit), nil
14751500
case strings.HasSuffix(unit.Filename, ".pod"):
14761501
return GetPodServiceName(unit), nil
14771502
default:
@@ -1503,6 +1528,10 @@ func GetBuildServiceName(podUnit *parser.UnitFile) string {
15031528
return getServiceName(podUnit, BuildGroup, "-build")
15041529
}
15051530

1531+
func GetArtifactServiceName(podUnit *parser.UnitFile) string {
1532+
return getServiceName(podUnit, ArtifactGroup, "-artifact")
1533+
}
1534+
15061535
func GetPodServiceName(podUnit *parser.UnitFile) string {
15071536
return getServiceName(podUnit, PodGroup, "-pod")
15081537
}
@@ -1888,7 +1917,7 @@ func handleStorageSource(quadletUnitFile, serviceUnitFile *parser.UnitFile, sour
18881917
if source[0] == '/' {
18891918
// Absolute path
18901919
serviceUnitFile.Add(UnitGroup, "RequiresMountsFor", source)
1891-
} else if strings.HasSuffix(source, ".volume") || (checkImage && strings.HasSuffix(source, ".image")) {
1920+
} else if strings.HasSuffix(source, ".volume") || (checkImage && strings.HasSuffix(source, ".image")) || strings.HasSuffix(source, ".artifact") {
18921921
sourceUnitInfo, ok := unitsInfoMap[source]
18931922
if !ok {
18941923
return "", fmt.Errorf("requested Quadlet source %s was not found", source)
@@ -2057,10 +2086,11 @@ func resolveContainerMountParams(containerUnitFile, serviceUnitFile *parser.Unit
20572086

20582087
// Source resolution is required only for these types of mounts
20592088
sourceResultionRequired := map[string]struct{}{
2060-
"volume": {},
2061-
"bind": {},
2062-
"glob": {},
2063-
"image": {},
2089+
"volume": {},
2090+
"bind": {},
2091+
"glob": {},
2092+
"image": {},
2093+
"artifact": {},
20642094
}
20652095
if _, ok := sourceResultionRequired[mountType]; !ok {
20662096
return mount, nil
@@ -2314,3 +2344,47 @@ func initServiceUnitFile(quadletUnitFile *parser.UnitFile, isUser bool, unitsInf
23142344

23152345
return service, unitInfo, nil
23162346
}
2347+
2348+
func ConvertArtifact(artifact *parser.UnitFile, unitsInfoMap map[string]*UnitInfo, isUser bool) (*parser.UnitFile, error) {
2349+
service, unitInfo, err := initServiceUnitFile(artifact, isUser, unitsInfoMap, ArtifactGroup)
2350+
if err != nil {
2351+
return nil, err
2352+
}
2353+
2354+
artifactName, ok := artifact.Lookup(ArtifactGroup, KeyArtifact)
2355+
if !ok || len(artifactName) == 0 {
2356+
return nil, fmt.Errorf("no Artifact key specified")
2357+
}
2358+
2359+
podman := createBasePodmanCommand(artifact, ArtifactGroup)
2360+
2361+
podman.add("artifact", "pull")
2362+
2363+
stringKeys := map[string]string{
2364+
KeyAuthFile: "--authfile",
2365+
KeyCertDir: "--cert-dir",
2366+
KeyCreds: "--creds",
2367+
KeyDecryptionKey: "--decryption-key",
2368+
KeyRetry: "--retry",
2369+
KeyRetryDelay: "--retry-delay",
2370+
}
2371+
lookupAndAddString(artifact, ArtifactGroup, stringKeys, podman)
2372+
2373+
boolKeys := map[string]string{
2374+
KeyQuiet: "--quiet",
2375+
KeyTLSVerify: "--tls-verify",
2376+
}
2377+
lookupAndAddBoolean(artifact, ArtifactGroup, boolKeys, podman)
2378+
2379+
handlePodmanArgs(artifact, ArtifactGroup, podman)
2380+
2381+
podman.add(artifactName)
2382+
2383+
service.AddCmdline(ServiceGroup, "ExecStart", podman.Args)
2384+
2385+
defaultOneshotServiceGroup(service, true)
2386+
2387+
unitInfo.ResourceName = artifactName
2388+
2389+
return service, nil
2390+
}

pkg/systemd/quadlet/quadlet_common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var (
44
// Key: Extension
55
// Value: Processing order for resource naming dependencies
66
SupportedExtensions = map[string]int{
7+
".artifact": 1,
78
".container": 4,
89
".volume": 2,
910
".kube": 4,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## assert-podman-final-args localhost/imagename
2+
## assert-podman-args "--name" "systemd-%N"
3+
## assert-podman-args "--mount"
4+
## assert-podman-args "type=artifact,source=quay.io/libpod/testartifact:20250206-single,destination=/artifacts"
5+
## assert-podman-args "--rm"
6+
## assert-podman-args "--replace"
7+
## assert-podman-args "-d"
8+
## assert-podman-args "--cgroups=split"
9+
## assert-podman-args "--sdnotify=conmon"
10+
## assert-key-is "Unit" "RequiresMountsFor" "%t/containers"
11+
## assert-key-is "Service" "KillMode" "mixed"
12+
## assert-key-is "Service" "Delegate" "yes"
13+
## assert-key-is "Service" "Type" "notify"
14+
## assert-key-is "Service" "NotifyAccess" "all"
15+
## assert-key-is "Service" "SyslogIdentifier" "%N"
16+
## assert-key-is-regex "Service" "ExecStopPost" "-[/S].*/podman rm -v -f -i systemd-%N"
17+
## assert-key-is-regex "Service" "ExecStop" ".*/podman rm -v -f -i systemd-%N"
18+
## assert-key-is "Service" "Environment" "PODMAN_SYSTEMD_UNIT=%n"
19+
## assert-key-is-regex "Unit" "After" "network-online.target|podman-user-wait-network-online.service"
20+
## assert-key-is-regex "Unit" "Wants" "network-online.target|podman-user-wait-network-online.service"
21+
22+
[Container]
23+
Image=localhost/imagename
24+
Mount=type=artifact,source=quay.io/libpod/testartifact:20250206-single,destination=/artifacts

test/e2e/quadlet/basic.artifact

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## assert-podman-final-args quay.io/libpod/testartifact:20250206-single
2+
## assert-podman-args "artifact"
3+
## assert-podman-args "pull"
4+
## assert-key-is "Service" "Type" "oneshot"
5+
## assert-key-is "Service" "RemainAfterExit" "yes"
6+
## assert-key-is-regex "Unit" "After" "network-online.target|podman-user-wait-network-online.service"
7+
## assert-key-is-regex "Unit" "Wants" "network-online.target|podman-user-wait-network-online.service"
8+
9+
[Artifact]
10+
Artifact=quay.io/libpod/testartifact:20250206-single

0 commit comments

Comments
 (0)