Skip to content

Commit 3ceac95

Browse files
committed
quadlet: add support for multiple quadlets in a single file
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. Signed-off-by: flouthoc <flouthoc.git@gmail.com>
1 parent 5a0b74b commit 3ceac95

File tree

8 files changed

+1133
-4
lines changed

8 files changed

+1133
-4
lines changed

debug_ext.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
systemdquadlet "github.com/containers/podman/v6/pkg/systemd/quadlet"
7+
)
8+
9+
func main() {
10+
result := systemdquadlet.IsExtSupported("single-test.quadlets")
11+
fmt.Printf("IsExtSupported for .quadlets: %v\n", result)
12+
13+
result2 := systemdquadlet.IsExtSupported("test.container")
14+
fmt.Printf("IsExtSupported for .container: %v\n", result2)
15+
}

debug_multi.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
)
8+
9+
func isMultiQuadletFile(filePath string) (bool, error) {
10+
content, err := os.ReadFile(filePath)
11+
if err != nil {
12+
return false, err
13+
}
14+
15+
lines := strings.Split(string(content), "\n")
16+
for _, line := range lines {
17+
trimmed := strings.TrimSpace(line)
18+
if trimmed == "---" {
19+
return true, nil
20+
}
21+
}
22+
return false, nil
23+
}
24+
25+
func main() {
26+
result, err := isMultiQuadletFile("single-test.quadlets")
27+
if err != nil {
28+
fmt.Printf("Error: %v\n", err)
29+
return
30+
}
31+
fmt.Printf("Is multi-quadlet: %v\n", result)
32+
}

debug_parse.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
type quadletSection struct {
11+
content string
12+
extension string
13+
name string
14+
}
15+
16+
func detectQuadletType(content string) (string, error) {
17+
lines := strings.Split(content, "\n")
18+
for _, line := range lines {
19+
line = strings.TrimSpace(line)
20+
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
21+
sectionName := strings.ToLower(strings.Trim(line, "[]"))
22+
switch sectionName {
23+
case "container":
24+
return ".container", nil
25+
case "volume":
26+
return ".volume", nil
27+
case "network":
28+
return ".network", nil
29+
case "kube":
30+
return ".kube", nil
31+
case "image":
32+
return ".image", nil
33+
case "build":
34+
return ".build", nil
35+
case "pod":
36+
return ".pod", nil
37+
}
38+
}
39+
}
40+
return "", fmt.Errorf("no recognized quadlet section found")
41+
}
42+
43+
func extractFileNameFromSection(content string) (string, error) {
44+
lines := strings.Split(content, "\n")
45+
for _, line := range lines {
46+
line = strings.TrimSpace(line)
47+
if strings.HasPrefix(line, "#") {
48+
commentContent := strings.TrimSpace(line[1:])
49+
if strings.HasPrefix(commentContent, "FileName=") {
50+
fileName := strings.TrimSpace(commentContent[9:])
51+
if fileName == "" {
52+
return "", fmt.Errorf("FileName comment found but no filename specified")
53+
}
54+
if strings.ContainsAny(fileName, "/\\") {
55+
return "", fmt.Errorf("FileName '%s' cannot contain path separators", fileName)
56+
}
57+
if strings.Contains(fileName, ".") {
58+
return "", fmt.Errorf("FileName '%s' should not include file extension", fileName)
59+
}
60+
return fileName, nil
61+
}
62+
}
63+
}
64+
return "", fmt.Errorf("missing required '# FileName=<name>' comment")
65+
}
66+
67+
func parseMultiQuadletFile(filePath string) ([]quadletSection, error) {
68+
content, err := os.ReadFile(filePath)
69+
if err != nil {
70+
return nil, fmt.Errorf("unable to read file %s: %w", filePath, err)
71+
}
72+
73+
lines := strings.Split(string(content), "\n")
74+
var sections []string
75+
var currentSection strings.Builder
76+
77+
for _, line := range lines {
78+
if strings.TrimSpace(line) == "---" {
79+
if currentSection.Len() > 0 {
80+
sections = append(sections, currentSection.String())
81+
currentSection.Reset()
82+
}
83+
} else {
84+
if currentSection.Len() > 0 {
85+
currentSection.WriteString("\n")
86+
}
87+
currentSection.WriteString(line)
88+
}
89+
}
90+
91+
if currentSection.Len() > 0 {
92+
sections = append(sections, currentSection.String())
93+
}
94+
95+
baseName := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
96+
isMultiSection := len(sections) > 1
97+
98+
quadlets := make([]quadletSection, 0, len(sections))
99+
100+
for i, section := range sections {
101+
section = strings.TrimSpace(section)
102+
if section == "" {
103+
continue
104+
}
105+
106+
extension, err := detectQuadletType(section)
107+
if err != nil {
108+
return nil, fmt.Errorf("unable to detect quadlet type in section %d: %w", i+1, err)
109+
}
110+
111+
var name string
112+
if isMultiSection {
113+
fileName, err := extractFileNameFromSection(section)
114+
if err != nil {
115+
return nil, fmt.Errorf("section %d: %w", i+1, err)
116+
}
117+
name = fileName
118+
} else {
119+
name = baseName
120+
}
121+
122+
quadlets = append(quadlets, quadletSection{
123+
content: section,
124+
extension: extension,
125+
name: name,
126+
})
127+
}
128+
129+
if len(quadlets) == 0 {
130+
return nil, fmt.Errorf("no valid quadlet sections found in file %s", filePath)
131+
}
132+
133+
return quadlets, nil
134+
}
135+
136+
func main() {
137+
quadlets, err := parseMultiQuadletFile("single-test.quadlets")
138+
if err != nil {
139+
fmt.Printf("Error parsing: %v\n", err)
140+
return
141+
}
142+
143+
fmt.Printf("Parsed %d quadlets:\n", len(quadlets))
144+
for i, q := range quadlets {
145+
fmt.Printf(" %d: name=%s, ext=%s, content_len=%d\n", i+1, q.name, q.extension, len(q.content))
146+
}
147+
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ This command allows you to:
1616

1717
* Specify a directory containing multiple Quadlet files and other non-Quadlet files for installation ( example a config file for a quadlet container ).
1818

19+
* Install multiple Quadlets from a single file 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.
20+
1921
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.
2022

2123
Note: In case user wants to install Quadlet application then first path should be the path to application directory.
@@ -59,5 +61,32 @@ $ podman quadlet install https://github.com/containers/podman/blob/main/test/e2e
5961
/home/user/.config/containers/systemd/basic.container
6062
```
6163

64+
Install multiple quadlets from a single file
65+
```
66+
$ cat webapp.quadlets
67+
# FileName=web-server
68+
[Container]
69+
Image=nginx:latest
70+
ContainerName=web-server
71+
PublishPort=8080:80
72+
73+
---
74+
75+
# FileName=app-storage
76+
[Volume]
77+
Label=app=webapp
78+
79+
---
80+
81+
# FileName=app-network
82+
[Network]
83+
Subnet=10.0.0.0/24
84+
85+
$ podman quadlet install webapp.quadlets
86+
/home/user/.config/containers/systemd/web-server.container
87+
/home/user/.config/containers/systemd/app-storage.volume
88+
/home/user/.config/containers/systemd/app-network.network
89+
```
90+
6291
## SEE ALSO
6392
**[podman(1)](podman.1.md)**, **[podman-quadlet(1)](podman-quadlet.1.md)**

0 commit comments

Comments
 (0)