Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions cmd/quadlet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,14 +411,14 @@ func loadUnitDropins(unit *parser.UnitFile, sourcePaths []string) error {
return prevError
}

func generateServiceFile(service *parser.UnitFile) error {
func generateServiceFile(service *parser.UnitFile, fileMode fs.FileMode) error {
Debugf("writing %q", service.Path)

service.PrependComment("",
fmt.Sprintf("Automatically generated by %s", os.Args[0]),
"")

f, err := os.Create(service.Path)
f, err := os.OpenFile(service.Path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, fileMode)
if err != nil {
return err
}
Expand Down Expand Up @@ -607,6 +607,8 @@ func generateUnitsInfoMap(units []*parser.UnitFile) map[string]*quadlet.UnitInfo
// Prefill resouceNames for .pod files.
// This is requires for referencing the pod from .container files
resourceName = quadlet.GetPodResourceName(unit)
case strings.HasSuffix(unit.Filename, ".login"):
serviceName = quadlet.GetLoginServiceName(unit)
default:
Logf("Unsupported file type %q", unit.Filename)
continue
Expand Down Expand Up @@ -706,11 +708,11 @@ func process() bool {
sort.Slice(units, func(i, j int) bool {
getOrder := func(i int) int {
ext := filepath.Ext(units[i].Filename)
order, ok := quadlet.SupportedExtensions[ext]
extensionInfo, ok := quadlet.SupportedExtensions[ext]
if !ok {
return 0
}
return order
return extensionInfo.Order
}
return getOrder(i) < getOrder(j)
})
Expand All @@ -722,24 +724,33 @@ func process() bool {
var service *parser.UnitFile
var warnings, err error

switch {
case strings.HasSuffix(unit.Filename, ".container"):
ext := filepath.Ext(unit.Filename)
extensionInfo, ok := quadlet.SupportedExtensions[ext]
if !ok {
Logf("Unsupported file type %q", unit.Filename)
continue
}

switch ext {
case ".container":
warnIfAmbiguousName(unit, quadlet.ContainerGroup)
service, warnings, err = quadlet.ConvertContainer(unit, isUserFlag, unitsInfoMap)
case strings.HasSuffix(unit.Filename, ".volume"):
case ".volume":
warnIfAmbiguousName(unit, quadlet.VolumeGroup)
service, warnings, err = quadlet.ConvertVolume(unit, unit.Filename, unitsInfoMap, isUserFlag)
case strings.HasSuffix(unit.Filename, ".kube"):
case ".kube":
service, err = quadlet.ConvertKube(unit, unitsInfoMap, isUserFlag)
case strings.HasSuffix(unit.Filename, ".network"):
case ".network":
service, warnings, err = quadlet.ConvertNetwork(unit, unit.Filename, unitsInfoMap, isUserFlag)
case strings.HasSuffix(unit.Filename, ".image"):
case ".image":
warnIfAmbiguousName(unit, quadlet.ImageGroup)
service, err = quadlet.ConvertImage(unit, unitsInfoMap, isUserFlag)
case strings.HasSuffix(unit.Filename, ".build"):
case ".build":
service, warnings, err = quadlet.ConvertBuild(unit, unitsInfoMap, isUserFlag)
case strings.HasSuffix(unit.Filename, ".pod"):
case ".pod":
service, warnings, err = quadlet.ConvertPod(unit, unit.Filename, unitsInfoMap, isUserFlag)
case ".login":
service, err = quadlet.ConvertLogin(unit, unit.Filename, unitsInfoMap, isUserFlag)
default:
Logf("Unsupported file type %q", unit.Filename)
continue
Expand All @@ -765,7 +776,7 @@ func process() bool {
fmt.Printf("---%s---\n%s\n", service.Path, data)
continue
}
if err := generateServiceFile(service); err != nil {
if err := generateServiceFile(service, extensionInfo.ServiceFileMode); err != nil {
reportError(fmt.Errorf("generating service file %s: %w", service.Path, err))
}
enableServiceFile(outputPath, service)
Expand Down
150 changes: 149 additions & 1 deletion docs/source/markdown/podman-systemd.unit.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ Valid options for `[Container]` are listed below:
| AddDevice=/dev/foo | --device /dev/foo |
| AddHost=example\.com:192.168.10.11 | --add-host example.com:192.168.10.11 |
| Annotation="XYZ" | --annotation "XYZ" |
| AuthFile=/etc/registry/auth\.json | --authfile=/etc/registry/auth\.json |
| AutoUpdate=registry | --label "io.containers.autoupdate=registry" |
| CgroupsMode=no-conmon | --cgroups=no-conmon |
| ContainerName=name | --name name |
Expand Down Expand Up @@ -421,6 +422,16 @@ similar to `Environment`.

This key can be listed multiple times.

### `AuthFile=`

Path of the authentication file.

This is equivalent to the `--authfile` option.

Special Cases:

* If the `name` of the AuthFile ends with `.login`, Quadlet will use the authfile created by the corresponding `.login` file, and the generated systemd service contains a dependency on the `$name-login.service` (or the service name set in the `.login` file). Note that the corresponding `.login` file must exist and must include the `AuthFile` key.

### `AutoUpdate=`

Indicates whether the container will be auto-updated ([podman-auto-update(1)](podman-auto-update.1.md)). The following values are supported:
Expand Down Expand Up @@ -1240,6 +1251,7 @@ Valid options for `[Kube]` are listed below:

| **[Kube] options** | **podman kube play equivalent** |
| ------------------------------------| -----------------------------------------------------------------|
| AuthFile=/etc/registry/auth\.json | --authfile=/etc/registry/auth\.json |
| AutoUpdate=registry | --annotation "io.containers.autoupdate=registry" |
| ConfigMap=/tmp/config.map | --config-map /tmp/config.map |
| ContainersConfModule=/etc/nvd\.conf | --module=/etc/nvd\.conf |
Expand All @@ -1256,6 +1268,16 @@ Valid options for `[Kube]` are listed below:

Supported keys in the `[Kube]` section are:

### `AuthFile=`

Path of the authentication file.

This is equivalent to the `--authfile` option.

Special Cases:

* If the `name` of the AuthFile ends with `.login`, Quadlet will use the authfile created by the corresponding `.login` file, and the generated systemd service contains a dependency on the `$name-login.service` (or the service name set in the `.login` file). Note that the corresponding `.login` file must exist and must include the `AuthFile` key.

### `AutoUpdate=`

Indicates whether containers will be auto-updated ([podman-auto-update(1)](podman-auto-update.1.md)). AutoUpdate can be specified multiple times. The following values are supported:
Expand Down Expand Up @@ -1728,6 +1750,10 @@ Path of the authentication file.

This is equivalent to the `--authfile` option of `podman build`.

Special Cases:

* If the `name` of the AuthFile ends with `.login`, Quadlet will use the authfile created by the corresponding `.login` file, and the generated systemd service contains a dependency on the `$name-login.service` (or the service name set in the `.login` file). Note that the corresponding `.login` file must exist and must include the `AuthFile` key.

### `ContainersConfModule=`

Load the specified containers.conf(5) module. Equivalent to the Podman `--module` option.
Expand Down Expand Up @@ -1963,7 +1989,12 @@ This is equivalent to the Podman `--arch` option.

Path of the authentication file.

This is equivalent to the Podman `--authfile` option.
This is equivalent to the `--authfile` option.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This is equivalent to the `--authfile` option.
This is equivalent to the Podman `--authfile` option.

I'm not sure why "Podman" was dropped here, but not in other places?


Special Cases:

* If the `name` of the AuthFile ends with `.login`, Quadlet will use the authfile created by the corresponding `.login` file, and the generated systemd service contains a dependency on the `$name-login.service` (or the service name set in the `.login` file). Note that the corresponding `.login` file must exist and must include the `AuthFile` key.


### `CertDir=`

Expand Down Expand Up @@ -2062,6 +2093,103 @@ Override the default architecture variant of the container image.

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

## Login units [Login]

Login files are named with a `.login` extension and contain a section `[Login]` describing the
login command. The generated service is a one-time command that logs into a registry.

Using login units allows pulling images from private registries without having to login manually.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A note about Systemd unit ordering should be added here (i.e. that users must ensure that the .login unit runs before any .container unit that wants to use these credentials starts - either manually, or by using AuthFile=private-repo.login in the .container unit)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though it might be a little off-topic, hinting users into the right direction might be helpful. How about something like the following:

Note that Systemd generally starts units in parallel. This can cause ordering issues, because a .container unit can't pull images from an authenticated source unless the .login unit with the necessary credentials runs already. Quadlet automatically ensures proper order when adding AuthFile=private-repo.login to a .container unit. However, else one must either add proper After= directives (e.g. After=private-repo-login.service) to the .container units manually, or use a custom synchronisation point like creating a containers.target unit and adding Before=containers.target to all .login units and After=containers.target to all .container units.


There is only one required key, `Registry`, which defines the URL of the registry to log into.

To avoid password leaking, Quadlet will set the permissions of the generated service file as 0600.
It is recommended to do the same for `.login` unit files.

Valid options for `[Login]` are listed below:

| **[Login] options** | **podman login** |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| **[Login] options** | **podman login** |
| **[Login] options** | **podman login equivalent** |

|-------------------------------------------------|----------------------------------------------------------------------|
| AuthFile=/etc/registry/auth\.json | --authfile=/etc/registry/auth\.json |
| CertDir=/etc/certs | --cert-dir=/etc/certs |
| LogoutOnStop=true | Add ExecStopPost to log out of the registry when the unit is stopped |
| Password=mypassword | --password-stdin and set StandardInputText |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just… not do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think Quadlet should block users from doing what podman allows them to

| PodmanArgs=--compat-auth-file=/etc/compat\.json | --compat-auth-file=/etc/registry/compat\.json |
| Registry=quai.io | podman login quai.io |
| Secret=mysecret | --secret=mysecret |
| ServiceName=name | Name the systemd unit `name.service` |
| TLSVerify=false | --tls-verify=false |
| Username=myuser | --username=myuser |

### `AuthFile=`

Path of the authentication file.

This field is mandatory when linking between an `AuthFile` key of a different unit and the `.login` unit.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to instead default to %t/<unit-name>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I'm not sure.
Not setting one, means that the login information is stored in the default file. So, units can depend on on the .login unit (in the Wants and After) without setting AuthKey.
But, maybe it is cleaner

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is likely fine. Though I guess there is the gotcha that one cannot use unit specific specifiers like %n as this would get resolved to a different name in the contianer unit where --authfile would be added. Though stuff like %t would till be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was that if the .login file doesn't specify an AuthFile then login info will be stored in the default file. So, the .container does not need to specify an AuthFile either, but it will need to specify the dependecy.
As for %n, you are correct. I'll add it to the doc

Copy link
Contributor

@PhrozenByte PhrozenByte Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO I don't see much benefit in forcing users to specify AuthFile: Auth files don't have to be unique and can store the credentials of multiple registries (i.e. using the default auth file is totally file). Furthermore, a .login unit doesn't have to be an appendix of a .container unit: Just think about a global quay.io.login unit (i.e. a .login unit per registry, not per container, intentionally also working for podman run on the shell). Let users decide how to use it.

It doesn't even mean that one must add an explicit dependency to .container units manually then: It all depends on Systemd's execution tree, there are many ways to assert that the .login unit is started before any .container unit (e.g. with intermediary .target units).

It should be noted in the docs though…

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth files don't have to be unique and can store the credentials of multiple registries (i.e. using the default auth file is totally file).

I think the main issue here is AFAICS there is no locking for writing the authfile. So if you actually use the same file in many login units they would all race against each other and potentially overwrite a previous login causing hard to find race conditions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a valid point indeed. However, shouldn't this rather be solved by adding a note that users must keep that in mind and act accordingly? Neither a Quadlet-specific default value, nor forcing the user to specify AuthFile manually solves this, because one could easily specify the same AuthFile twice. It's basically the same as for ordering the .login unit before the necessary .container unit(s). Both should be noted in the docs though.

Copy link
Contributor

@PhrozenByte PhrozenByte Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got me thinking: I don't know Quadlet's code, but since AuthFile=private-repo.login in .container units works, Quadlet must be aware of other Quadlet units when generating the actual Systemd units. So, how about grouping .login units that attempt to write to the same auth file together and adding After= directives to the generated Systemd units to ensure that they run sequentially? This would actually solve the issue with race conditions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main issue here is AFAICS there is no locking for writing the authfile. So if you actually use the same file in many login units they would all race against each other and potentially overwrite a previous login causing hard to find race conditions.

https://github.com/containers/image/issues/1365 for the record.

Do not use unit specific systemd specifiers (e.g. `%n`) as they will be resolved differently in the dependent units.

This is equivalent to the Podman `--authfile` option.

### `CertDir=`

Use certificates at path (*.crt, *.cert, *.key) to connect to the registry.

This is equivalent to the Podman `--cert-dir` option.


### `LogoutOnStop=` (defaults to `false`)

When set to `true`, the logout will be called when the service is stopped.
Copy link
Contributor

@PhrozenByte PhrozenByte Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When set to `true`, the logout will be called when the service is stopped.
When set to `true`, `podman logout` will be called when the service stops.


### `Password=`

Use the password to connect to the registry.
In order to prevent password leak, instead of using the `--password` option,
Quadlet will set `--password-stdin` and use systemd `StandardInputText` to pass the data.


### `PodmanArgs=`

This key contains a list of arguments passed directly to the end of the `podman login` command
in the generated file. It can be used to access Podman features otherwise unsupported by the generator.
Since the generator is unaware of the unexpected interactions that can be caused by these arguments,
it is not recommended to use this option.

The format of this is a space separated list of arguments, which can optionally be individually
escaped to allow inclusion of whitespace and other control characters.

This key can be listed multiple times.

### `Registry=`

Use the URL of the registry to connect to. This key is mandatory.

This is equivalent to the last argument of `podman login`.

### `Secret=`

Use a podman secret for the credentials.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Use a podman secret for the credentials.
Use a Podman secret for the credentials.


This is equivalent to the Podman `--secret` option.

### `ServiceName=`

By default, Quadlet will name the systemd service unit by appending `-login` to the name of the Quadlet.
Setting this key overrides this behavior by instructing Quadlet to use the provided name.

Note, the name should not include the `.service` file extension

### `TLSVerify=`

Require HTTPS and verification of certificates when contacting registries.

This is equivalent to the Podman `--tls-verify` option.

### `Username=`

Use the username to connect to the registry.

This is equivalent to the Podman `--username` option.

## Quadlet section [Quadlet]
Some quadlet specific configuration is shared between different unit types. Those settings
can be configured in the `[Quadlet]` section.
Expand Down Expand Up @@ -2241,6 +2369,26 @@ Options=iam_role,endpoint=${AWS_REGION},use_xattr,listobjectsv2,del_cache,use_ca
# `iam_role` assumes inside EC2, if not, Use `profile=` instead
```

Example for pulling from a private repository with a `.login` file

`private-repo.login`
```
[Login]
Registry=private-repo.example.com
AuthFile=/etc/auth/private-repo.json
TLSVerify=false
Username=myuser
Password=mypassword
LogoutOnStop=true
```

`app.container`
```
[Container]
Image=private-repo.example.com/myaccount/myapp:latest
AuthFile=private-repo.login
```

## 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)**,
Expand Down
Loading