Skip to content

Conversation

@flouthoc
Copy link
Collaborator

@flouthoc flouthoc commented Oct 27, 2025

Add ability to quadlet install to install multiple quadlets from a single file by separating
them with --- delimiters, similar to how multiple quadlets can be
installed from a directory. Each section requires # FileName=<name> comment for custom naming.

Example usage:

  # FileName=web-server
  [Container]
  Image=nginx:latest
  PublishPort=8080:80
  
  ---
  
  # FileName=app-storage
  [Volume]
  Label=app=webapp
$ podman quadlet install webapp.quadlets
/home/user/.config/containers/systemd/web-server.container
/home/user/.config/containers/systemd/app-storage.volume

Does this PR introduce a user-facing change?

quadlet install: add support for multiple quadlets in a single file

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Oct 27, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: flouthoc
Once this PR has been reviewed and has the lgtm label, please assign mheon for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@packit-as-a-service
Copy link

[NON-BLOCKING] Packit jobs failed. @containers/packit-build please check. Everyone else, feel free to ignore.

@flouthoc flouthoc marked this pull request as draft October 27, 2025 14:11
@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 27, 2025
@flouthoc flouthoc force-pushed the multi-file-quadlet branch 2 times, most recently from 6688c20 to 1ea3992 Compare October 27, 2025 15:29
@ygalblum
Copy link
Contributor

The title of the PR and the release notes comment are misleading. This PR does not add this capability to Quadlet, it adds it to quadlet install. It's important to make this distinction so that users that do not use quadlet install and instead manually manage their files, won't expect this capability

@flouthoc flouthoc changed the title quadlet: add support for multiple quadlets in a single file quadlet install: add support for multiple quadlets in a single file Oct 27, 2025
@flouthoc
Copy link
Collaborator Author

The title of the PR and the release notes comment are misleading. This PR does not add this capability to Quadlet, it adds it to quadlet install. It's important to make this distinction so that users that do not use quadlet install and instead manually manage their files, won't expect this capability

I agree this is only applicable for quadlet install, thanks @ygalblum for pointing this out.

@Luap99
Copy link
Member

Luap99 commented Oct 27, 2025

Please add the issue link with Fixes :###

The title of the PR and the release notes comment are misleading. This PR does not add this capability to Quadlet, it adds it to quadlet install. It's important to make this distinction so that users that do not use quadlet install and instead manually manage their files, won't expect this capability

Just to be clear I am still against doing this overall but I guess I was outvoted on that so that doesn't matter.
IMO that is the worst possible outcome of this. Not doing this in quadlet proper will make everything just so much more confusing for end users. It breaks the "declarative" nature of just copying file into the right place as you now force any users who has such a "multi" file to have to run that though podman quadlet install which just seems unnecessary.

@flouthoc flouthoc force-pushed the multi-file-quadlet branch 4 times, most recently from 3ceac95 to 3efc462 Compare October 27, 2025 18:25
@ygalblum
Copy link
Contributor

@Luap99 I completely agree with you. But, I kinda disengaged from the podman quadlet work. So, as you, I assume this was decided somewhere and just want to make sure we keep the distinction.

@Luap99
Copy link
Member

Luap99 commented Oct 28, 2025

@ygalblum there was an internal design doc https://docs.google.com/document/d/1fVjmxVA6k_83iVztQ4j4ISGW7JMVb7yCBfO6vYk9SWs/edit?usp=sharing

If that wasn't shared to you then this is already a big issue as you as main quadlet maintainer should be able to look at these things and give your input. As I noted there design docs shouldn't be internal at all and be public in this repo so all maintainers and community people can actually give feedback https://github.com/containers/podman/tree/main/contrib/design-docs which was also not done here.

So, as you, I assume this was decided somewhere and just want to make sure we keep the distinction.

Well the way I see it decided "somewhere" doesn't count in general, we decide upstream on github what gets merged which includes all maintainers including you of course. So if you think this is a bad idea you can still speak up. I already objected that feature internally and on the issue (#26447) but it seemed like some other maintainers on the team were in favour and others abstained so I didn't push any further against this.

@eriksjolund
Copy link
Contributor

eriksjolund commented Oct 29, 2025

@ygalblum there was an internal design doc https://docs.google.com/document/d/1fVjmxVA6k_83iVztQ4j4ISGW7JMVb7yCBfO6vYk9SWs/edit?usp=sharing

I clicked the link and landed on a web page saying:

You need access
Request access, or switch to an account with access. Learn more

(*) Viewer

( ) Commenter

( ) Editor

There is also a text input field

Message (optional)

Is the document only for internal use?

There is button to click for Request access. I could click the button but I wanted to ask before I do that.

Copy link
Member

@Honny1 Honny1 left a comment

Choose a reason for hiding this comment

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

My only thought is that we could improve performance by reading the input file only once instead of multiple times.

@Luap99
Copy link
Member

Luap99 commented Oct 29, 2025

Is the document only for internal use?

There is no reason why this should be private IMO. I don't own the doc so I cannot share it. As I said for feedback and public knowledge this should have been posted here on github into the design-doc directory instead of being a private doc.

There have been a ton of design docs internally over the years. I am trying to push all to do these discussions in public here on github for transparency and to allow for feedback outside our team early on in the design phase.

@ygalblum
Copy link
Contributor

I don't have access to this doc either. I remember the discussion we had on Quadlet's support for single file and I agree that Quadlet should not support it.

Generally speaking about podman quadlet, my personal opinion is that I don't see its benefit in Linux systems where users can simply copy the files and use systemctl to manage the services. I can see it in MacOS or Windows where the details of where podman actually runs are hidden from the user and copying the files manually is not obvious.

To me, the podman quadlet command is more of a copy tool than actually Quadlet and that's how I try to differentiate between them. I don't mind having a tool that knows to read a single file and break it into many. But, it should have a distinct extension (one not supported by Quadlet), schema and proper documentation.

@eriksjolund
Copy link
Contributor

There is no reason why this should be private IMO. I don't own the doc so I cannot share it. As I said for feedback and public knowledge this should have been posted here on github into the design-doc directory instead of being a private doc.

There have been a ton of design docs internally over the years. I am trying to push all to do these discussions in public here on github for transparency and to allow for feedback outside our team early on in the design phase.

That is a good idea.

@flouthoc
Copy link
Collaborator Author

I don't have access to this doc either. I remember the discussion we had on Quadlet's support for single file and I agree that Quadlet should not support it.

Generally speaking about podman quadlet, my personal opinion is that I don't see its benefit in Linux systems where users can simply copy the files and use systemctl to manage the services. I can see it in MacOS or Windows where the details of where podman actually runs are hidden from the user and copying the files manually is not obvious.

To me, the podman quadlet command is more of a copy tool than actually Quadlet and that's how I try to differentiate between them. I don't mind having a tool that knows to read a single file and break it into many. But, it should have a distinct extension (one not supported by Quadlet), schema and proper documentation.

@ygalblum You should have access to design doc now.

But, it should have a distinct extension (one not supported by Quadlet), schema and proper documentation

I agree, I think we can limit this to one particular extension maybe like .quadlets. If everyone agrees I can make this change. WDYT ?

@flouthoc flouthoc marked this pull request as ready for review November 3, 2025 20:28
@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Nov 3, 2025
@flouthoc flouthoc force-pushed the multi-file-quadlet branch 4 times, most recently from e394f15 to 03e35a0 Compare November 6, 2025 16:56
@flouthoc
Copy link
Collaborator Author

flouthoc commented Nov 7, 2025

@ygalblum @Luap99 This is ready for review.

Comment on lines 224 to 231
isQuadletsFile := ext == ".quadlets"

// Only check for multi-quadlet content if it's a .quadlets file
var isMulti bool
if isQuadletsFile {
// For .quadlets files, always treat as multi-quadlet (even single quadlets)
isMulti = true
}
Copy link
Member

Choose a reason for hiding this comment

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

I think isMulti always has the same value as isQuadletsFile.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

Enable installing multiple quadlets from one file using '---' delimiters.
Each section requires '# FileName=<name>' comment for custom naming.
Single quadlet files remain unchanged for backward compatibility.

Assited by: claude-4-sonnet

Signed-off-by: flouthoc <flouthoc.git@gmail.com>
@flouthoc flouthoc requested a review from Honny1 November 10, 2025 16:27
@flouthoc
Copy link
Collaborator Author

@Honny1 PTAL

Copy link
Member

@Honny1 Honny1 left a comment

Choose a reason for hiding this comment

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

LGTM

@flouthoc
Copy link
Collaborator Author

@TomSweeneyRedHat @ygalblum PTAL

@TomSweeneyRedHat
Copy link
Member

Code LGTM, and I really like making this into a new file type so the old quadlet files are handled as before. I do have a couple of smal tweaks for the doc that I'd like to see happen, and then I'd like a head nod from @ygalblum

@flouthoc
Copy link
Collaborator Author

Code LGTM, and I really like making this into a new file type so the old quadlet files are handled as before. I do have a couple of smal tweaks for the doc that I'd like to see happen, and then I'd like a head nod from @ygalblum

Done. addressed comments. @ygalblum Could you PTAL

@TomSweeneyRedHat
Copy link
Member

LGTM
thanks @flouthoc I'll plan to merge this tomorrow afternoon, or after getting a LGTM from @ygalblum before then. Ping me if I forget tomorrow.

Copy link
Contributor

@ygalblum ygalblum left a comment

Choose a reason for hiding this comment

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

Sorry for the delay in my review

Image=nginx:latest
ContainerName=web-server
PublishPort=8080:80
Copy link
Contributor

Choose a reason for hiding this comment

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

Are the extra empty lines (before and after the delimiter) required?

if assetFile == "" {
assetFile = "." + filepath.Base(toInstall) + ".asset"
// Check if this is a .quadlets file - if so, treat as an app
ext := strings.ToLower(filepath.Ext(toInstall))
Copy link
Contributor

Choose a reason for hiding this comment

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

Why ToLower? We don't do that on any other extensions (see

func IsExtSupported(filename string) bool {
)


// Check if this file has a supported extension or is a .quadlets file
hasValidExt := systemdquadlet.IsExtSupported(toInstall)
ext := strings.ToLower(filepath.Ext(toInstall))
Copy link
Contributor

Choose a reason for hiding this comment

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

same comment regarding ToLower

Comment on lines 229 to 232
if assetFile != "" {
// This is part of an app installation, allow non-quadlet files as assets
// Don't validate as quadlet file (validateQuadletFile will be false)
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't add empty contexts

Suggested change
if assetFile != "" {
// This is part of an app installation, allow non-quadlet files as assets
// Don't validate as quadlet file (validateQuadletFile will be false)
} else {
// Allow non-quadlet files as assets
// Don't validate as quadlet file (validateQuadletFile will be false)
if assetFile == "" {

line = strings.TrimSpace(line)
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
sectionName := strings.ToLower(strings.Trim(line, "[]"))
switch sectionName {
Copy link
Contributor

Choose a reason for hiding this comment

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

You are already missing artifact. This should take a different approach. Something like:

expected := "." + sectionName
if systemdquadlet.IsExtSupported("a" + expectedExt) {
    return expected, nil
}

This way when a new extension is added it will work out of the box

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@@ -0,0 +1,371 @@
#!/usr/bin/env bats -*- bats -*-
Copy link
Contributor

Choose a reason for hiding this comment

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

None of the test use additional systemd sections. Need to make sure that these section both don't interfere with the creation of the standalone files and that they are copied correctly

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done, added in first test.

local install_dir=$(get_quadlet_install_dir)

# Create a multi-quadlet file
local multi_quadlet_file=$PODMAN_TMPDIR/webapp.quadlets
Copy link
Contributor

Choose a reason for hiding this comment

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

Using $(random_string) allows parallelism

Suggested change
local multi_quadlet_file=$PODMAN_TMPDIR/webapp.quadlets
local multi_quadlet_file=$PODMAN_TMPDIR/webapp_$(random_string).quadlets

Also for the file names set inside the file. To keep track, you'll need to calculate the names outside of the file writing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

assert "$output" =~ "Image=$IMAGE" "container file should contain correct image"
assert "$output" =~ "ContainerName=web-server" "container file should contain container name"

run cat "$install_dir/appstorage.volume"
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need both cut and podman quadlet print?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed.

assert "$output" =~ "missing required.*FileName" "error should mention missing FileName"
}

@test "quadlet verb - multi-quadlet file creates application" {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't you cover this with the first test?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@flouthoc flouthoc force-pushed the multi-file-quadlet branch 2 times, most recently from 12cc6f9 to 2415f5b Compare November 13, 2025 21:48
@flouthoc flouthoc requested a review from ygalblum November 13, 2025 21:48
Quadlets installed from `.quadlet` file now belongs to a single
application, anyone file removed from this application removes all the
other files as well.

Assited by: claude-4-sonnet

Signed-off-by: flouthoc <flouthoc.git@gmail.com>
* Specify a directory containing multiple Quadlet files and other non-Quadlet files for installation ( example a config file for a quadlet container ).

Note: If a quadlet is part of an application, removing that specific quadlet will remove the entire application. When a quadlet is installed from a directory, all files installed from that directory—including both quadlet and non-quadlet files—are considered part of a single application.
* Install multiple Quadlets from a single file with the `.quadlets` extension, where each Quadlet is separated by a `---` delimiter. When using multiple quadlets in a single `.quadlets` file, each quadlet section must include a `# FileName=<name>` comment to specify the name for that quadlet, extension of `FileName` is not required as it will be generated by parser internally.
Copy link
Contributor

Choose a reason for hiding this comment

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

extension of FileName is not required

That's not the case. It's not that the extension is not required. The code fails if it finds an extension.
Having said that, see my comments on that check

_, err = tmpFile.WriteString(quadlet.content)
tmpFile.Close()
if err != nil {
os.Remove(tmpFile.Name())
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line, you've already deferred the call to remove

installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to write quadlet section %s to temporary file: %w", quadlet.name, err)
continue
}
tmpFile.Close()
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line, you've already closed the file

destName := quadlet.name + quadlet.extension
installedPath, err := ic.installQuadlet(ctx, tmpFile.Name(), destName, installDir, assetFile, true, options.Replace)
if err != nil {
os.Remove(tmpFile.Name())
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line, you've already deferred the call to remove

}

// Clean up temporary file
os.Remove(tmpFile.Name())
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line, you've already deferred the call to remove

}

// Extract name for this quadlet section
var name string
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
var name string
name := baseName

And remove the else context


// Extract name for this quadlet section
var name string
if isMultiSection {
Copy link
Contributor

Choose a reason for hiding this comment

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

So if there's only one section, FileName is not required?

if strings.ContainsAny(fileName, "/\\") {
return "", fmt.Errorf("FileName '%s' cannot contain path separators", fileName)
}
if strings.Contains(fileName, ".") {
Copy link
Contributor

Choose a reason for hiding this comment

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

. is a valid character for a file name regardless to the file's extension. For example the filename ygal.blum.log is a valid filename with .log being the extension. So, not sure if we should validate it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants