Skip to content

Commit 03e35a0

Browse files
committed
quadlet install: multiple quadlets from single file should share app
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>
1 parent 76c6a55 commit 03e35a0

File tree

3 files changed

+77
-31
lines changed

3 files changed

+77
-31
lines changed

docs/source/markdown/podman-quadlet-install.1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ This command allows you to:
1818

1919
* Install multiple Quadlets from a single file with `.quadlets` extension where each Quadlet is separated by a `---` delimiter. When using multiple quadlets in a single file, each quadlet section must include a `# FileName=<name>` comment to specify the name for that quadlet.
2020

21-
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.
21+
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. Similarly, when multiple quadlets are installed from a single `.quadlets` file, they are all considered part of the same application.
2222

2323
Note: In case user wants to install Quadlet application then first path should be the path to application directory.
2424

pkg/domain/infra/abi/quadlet.go

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,15 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
164164
for _, toInstall := range paths {
165165
validateQuadletFile := false
166166
if assetFile == "" {
167-
assetFile = "." + filepath.Base(toInstall) + ".asset"
167+
// Check if this is a .quadlets file - if so, treat as an app
168+
ext := strings.ToLower(filepath.Ext(toInstall))
169+
if ext == ".quadlets" {
170+
// For .quadlets files, use .app extension to group all quadlets as one application
171+
baseName := strings.TrimSuffix(filepath.Base(toInstall), filepath.Ext(toInstall))
172+
assetFile = "." + baseName + ".app"
173+
} else {
174+
assetFile = "." + filepath.Base(toInstall) + ".asset"
175+
}
168176
validateQuadletFile = true
169177
}
170178
switch {
@@ -218,12 +226,6 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
218226
// Only check for multi-quadlet content if it's a .quadlets file
219227
var isMulti bool
220228
if isQuadletsFile {
221-
var err error
222-
isMulti, err = isMultiQuadletFile(toInstall)
223-
if err != nil {
224-
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to check if file is multi-quadlet: %w", err)
225-
continue
226-
}
227229
// For .quadlets files, always treat as multi-quadlet (even single quadlets)
228230
isMulti = true
229231
}
@@ -385,6 +387,14 @@ func (ic *ContainerEngine) installQuadlet(_ context.Context, path, destName, ins
385387
if err != nil {
386388
return "", fmt.Errorf("error while writing non-quadlet filename: %w", err)
387389
}
390+
} else if strings.HasSuffix(assetFile, ".app") {
391+
// For quadlet files that are part of an application (indicated by .app extension),
392+
// also write the quadlet filename to the .app file for proper application tracking
393+
quadletName := filepath.Base(finalPath)
394+
err := appendStringToFile(filepath.Join(installDir, assetFile), quadletName)
395+
if err != nil {
396+
return "", fmt.Errorf("error while writing quadlet filename to app file: %w", err)
397+
}
388398
}
389399
return finalPath, nil
390400
}
@@ -550,24 +560,6 @@ func detectQuadletType(content string) (string, error) {
550560
return "", fmt.Errorf("no recognized quadlet section found (expected [Container], [Volume], [Network], [Kube], [Image], [Build], or [Pod])")
551561
}
552562

553-
// isMultiQuadletFile checks if a file contains multiple quadlets by looking for "---" delimiter
554-
// The delimiter must be on its own line (possibly with whitespace)
555-
func isMultiQuadletFile(filePath string) (bool, error) {
556-
content, err := os.ReadFile(filePath)
557-
if err != nil {
558-
return false, err
559-
}
560-
561-
lines := strings.Split(string(content), "\n")
562-
for _, line := range lines {
563-
trimmed := strings.TrimSpace(line)
564-
if trimmed == "---" {
565-
return true, nil
566-
}
567-
}
568-
return false, nil
569-
}
570-
571563
// buildAppMap scans the given directory for files that start with '.'
572564
// and end with '.app', reads their contents (one filename per line), and
573565
// returns a map where each filename maps to the .app file that contains it.

test/system/254-podman-quadlet-multi.bats

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,8 @@ EOF
121121
# Verify the container quadlet was removed but others remain
122122
run_podman quadlet list
123123
assert "$output" !~ "webserver.container" "list should not contain removed webserver.container"
124-
assert "$output" =~ "appstorage.volume" "list should still contain appstorage.volume"
125-
assert "$output" =~ "appnetwork.network" "list should still contain appnetwork.network"
126-
127-
# Clean up remaining quadlets
128-
run_podman quadlet rm appstorage.volume appnetwork.network
124+
assert "$output" !~ "appstorage.volume" "list should not contain appstorage.volume as app is removed"
125+
assert "$output" !~ "appnetwork.network" "list should not contain appnetwork.network as app is removed"
129126
}
130127

131128
@test "quadlet verb - install multi-quadlet file with empty sections" {
@@ -180,3 +177,60 @@ EOF
180177
run_podman 125 quadlet install $multi_quadlet_file
181178
assert "$output" =~ "missing required.*FileName" "error should mention missing FileName"
182179
}
180+
181+
@test "quadlet verb - multi-quadlet file creates application" {
182+
# Test that quadlets from a .quadlets file are treated as part of the same application
183+
local install_dir=$(get_quadlet_install_dir)
184+
local multi_quadlet_file=$PODMAN_TMPDIR/myapp.quadlets
185+
186+
cat > $multi_quadlet_file <<EOF
187+
# FileName=webapp
188+
[Container]
189+
Image=$IMAGE
190+
ContainerName=webapp
191+
PublishPort=8080:80
192+
193+
---
194+
195+
# FileName=webdb
196+
[Volume]
197+
Label=app=myapp
198+
EOF
199+
200+
# Install the multi-quadlet file
201+
run_podman quadlet install $multi_quadlet_file
202+
203+
# Verify both quadlets were installed
204+
assert "$output" =~ "webapp.container" "install output should contain webapp.container"
205+
assert "$output" =~ "webdb.volume" "install output should contain webdb.volume"
206+
207+
# Check that the .app file was created (not individual .asset files)
208+
[[ -f "$install_dir/.myapp.app" ]] || die ".myapp.app file should exist"
209+
[[ ! -f "$install_dir/.webapp.container.asset" ]] || die "individual .asset files should not exist"
210+
[[ ! -f "$install_dir/.webdb.volume.asset" ]] || die "individual .asset files should not exist"
211+
212+
# Verify the .app file contains both quadlet names
213+
run cat "$install_dir/.myapp.app"
214+
assert "$output" =~ "webapp.container" ".app file should contain webapp.container"
215+
assert "$output" =~ "webdb.volume" ".app file should contain webdb.volume"
216+
217+
# Test quadlet list to verify both quadlets show the same app name
218+
run_podman quadlet list
219+
local webapp_line=$(echo "$output" | grep "webapp.container")
220+
local webdb_line=$(echo "$output" | grep "webdb.volume")
221+
222+
# Both lines should contain the same app name (.myapp.app)
223+
assert "$webapp_line" =~ "\\.myapp\\.app" "webapp should show .myapp.app as app"
224+
assert "$webdb_line" =~ "\\.myapp\\.app" "webdb should show .myapp.app as app"
225+
226+
# Test removing the application by removing one quadlet should remove both
227+
run_podman quadlet rm webapp.container
228+
229+
# Both quadlets should be removed since they're part of the same app
230+
run_podman quadlet list
231+
assert "$output" !~ "webapp.container" "webapp.container should be removed"
232+
assert "$output" !~ "webdb.volume" "webdb.volume should also be removed as part of same app"
233+
234+
# The .app file should also be removed
235+
[[ ! -f "$install_dir/.myapp.app" ]] || die ".myapp.app file should be removed"
236+
}

0 commit comments

Comments
 (0)