mirror of
https://github.com/containers/podman.git
synced 2025-11-30 01:58:46 +08:00
Merge pull request #27384 from flouthoc/multi-file-quadlet
quadlet install: add support for multiple quadlets in a single file
This commit is contained in:
@@ -16,7 +16,9 @@ This command allows you to:
|
||||
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
Note: In case user wants to install Quadlet application then first path should be the path to application directory.
|
||||
|
||||
@@ -59,5 +61,30 @@ $ podman quadlet install https://github.com/containers/podman/blob/main/test/e2e
|
||||
/home/user/.config/containers/systemd/basic.container
|
||||
```
|
||||
|
||||
Install multiple quadlets from a single .quadlets file
|
||||
```
|
||||
$ cat webapp.quadlets
|
||||
# FileName=web-server
|
||||
[Container]
|
||||
Image=nginx:latest
|
||||
ContainerName=web-server
|
||||
PublishPort=8080:80
|
||||
---
|
||||
# FileName=app-storage
|
||||
[Volume]
|
||||
Label=app=webapp
|
||||
---
|
||||
# FileName=app-network
|
||||
[Network]
|
||||
Subnet=10.0.0.0/24
|
||||
|
||||
$ podman quadlet install webapp.quadlets
|
||||
/home/user/.config/containers/systemd/web-server.container
|
||||
/home/user/.config/containers/systemd/app-storage.volume
|
||||
/home/user/.config/containers/systemd/app-network.network
|
||||
```
|
||||
|
||||
Note: Multi-quadlet functionality requires the `.quadlets` file extension. Files with other extensions will only be processed as single quadlets or asset files.
|
||||
|
||||
## SEE ALSO
|
||||
**[podman(1)](podman.1.md)**, **[podman-quadlet(1)](podman-quadlet.1.md)**, **[podman-systemd.unit(5)](podman-systemd.unit.5.md)**
|
||||
|
||||
@@ -164,7 +164,15 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
|
||||
for _, toInstall := range paths {
|
||||
validateQuadletFile := false
|
||||
if assetFile == "" {
|
||||
// Check if this is a .quadlets file - if so, treat as an app
|
||||
ext := filepath.Ext(toInstall)
|
||||
if ext == ".quadlets" {
|
||||
// For .quadlets files, use .app extension to group all quadlets as one application
|
||||
baseName := strings.TrimSuffix(filepath.Base(toInstall), filepath.Ext(toInstall))
|
||||
assetFile = "." + baseName + ".app"
|
||||
} else {
|
||||
assetFile = "." + filepath.Base(toInstall) + ".asset"
|
||||
}
|
||||
validateQuadletFile = true
|
||||
}
|
||||
switch {
|
||||
@@ -209,7 +217,58 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
|
||||
installReport.QuadletErrors[toInstall] = err
|
||||
continue
|
||||
}
|
||||
// If toInstall is a single file, execute the original logic
|
||||
|
||||
// Check if this file has a supported extension or is a .quadlets file
|
||||
hasValidExt := systemdquadlet.IsExtSupported(toInstall)
|
||||
isQuadletsFile := filepath.Ext(toInstall) == ".quadlets"
|
||||
|
||||
// Handle files with unsupported extensions that are not .quadlets files
|
||||
// If we're installing as part of an app (assetFile is set), allow non-quadlet files as assets
|
||||
// Standalone files with unsupported extensions are not allowed
|
||||
if !hasValidExt && !isQuadletsFile && assetFile == "" {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("%q is not a supported Quadlet file type", filepath.Ext(toInstall))
|
||||
continue
|
||||
}
|
||||
|
||||
if isQuadletsFile {
|
||||
// Parse the multi-quadlet file
|
||||
quadlets, err := parseMultiQuadletFile(toInstall)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = err
|
||||
continue
|
||||
}
|
||||
|
||||
// Install each quadlet section as a separate file
|
||||
for _, quadlet := range quadlets {
|
||||
// Create a temporary file for this quadlet section
|
||||
tmpFile, err := os.CreateTemp("", quadlet.name+"*"+quadlet.extension)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to create temporary file for quadlet section %s: %w", quadlet.name, err)
|
||||
continue
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
// Write the quadlet content to the temporary file
|
||||
_, err = tmpFile.WriteString(quadlet.content)
|
||||
tmpFile.Close()
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to write quadlet section %s to temporary file: %w", quadlet.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Install the quadlet from the temporary file
|
||||
destName := quadlet.name + quadlet.extension
|
||||
installedPath, err := ic.installQuadlet(ctx, tmpFile.Name(), destName, installDir, assetFile, true, options.Replace)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to install quadlet section %s: %w", destName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Record the installation (use a unique key for each section)
|
||||
sectionKey := fmt.Sprintf("%s#%s", toInstall, destName)
|
||||
installReport.InstalledQuadlets[sectionKey] = installedPath
|
||||
}
|
||||
} else {
|
||||
// If toInstall is a single file with a supported extension, execute the original logic
|
||||
installedPath, err := ic.installQuadlet(ctx, toInstall, "", installDir, assetFile, validateQuadletFile, options.Replace)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = err
|
||||
@@ -218,6 +277,7 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
|
||||
installReport.InstalledQuadlets[toInstall] = installedPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should we still do this if the above validation errored?
|
||||
if options.ReloadSystemd {
|
||||
@@ -308,6 +368,14 @@ func (ic *ContainerEngine) installQuadlet(_ context.Context, path, destName, ins
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error while writing non-quadlet filename: %w", err)
|
||||
}
|
||||
} else if strings.HasSuffix(assetFile, ".app") {
|
||||
// For quadlet files that are part of an application (indicated by .app extension),
|
||||
// also write the quadlet filename to the .app file for proper application tracking
|
||||
quadletName := filepath.Base(finalPath)
|
||||
err := appendStringToFile(filepath.Join(installDir, assetFile), quadletName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error while writing quadlet filename to app file: %w", err)
|
||||
}
|
||||
}
|
||||
return finalPath, nil
|
||||
}
|
||||
@@ -325,6 +393,125 @@ func appendStringToFile(filePath, text string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// quadletSection represents a single quadlet extracted from a multi-quadlet file
|
||||
type quadletSection struct {
|
||||
content string
|
||||
extension string
|
||||
name string
|
||||
}
|
||||
|
||||
// parseMultiQuadletFile parses a file that may contain multiple quadlets separated by "---"
|
||||
// Returns a slice of quadletSection structs, each representing a separate quadlet
|
||||
func parseMultiQuadletFile(filePath string) ([]quadletSection, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// Split content by lines and reconstruct sections manually to handle "---" properly
|
||||
lines := strings.Split(string(content), "\n")
|
||||
var sections []string
|
||||
var currentSection strings.Builder
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "---" {
|
||||
// Found separator, save current section and start new one
|
||||
if currentSection.Len() > 0 {
|
||||
sections = append(sections, currentSection.String())
|
||||
currentSection.Reset()
|
||||
}
|
||||
} else {
|
||||
currentSection.WriteString(line)
|
||||
currentSection.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last section
|
||||
if currentSection.Len() > 0 {
|
||||
sections = append(sections, currentSection.String())
|
||||
}
|
||||
|
||||
// Pre-allocate slice with capacity based on number of sections
|
||||
quadlets := make([]quadletSection, 0, len(sections))
|
||||
|
||||
for i, section := range sections {
|
||||
// Trim whitespace from section
|
||||
section = strings.TrimSpace(section)
|
||||
if section == "" {
|
||||
continue // Skip empty sections
|
||||
}
|
||||
|
||||
// Determine quadlet type from section content
|
||||
extension, err := detectQuadletType(section)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to detect quadlet type in section %d: %w", i+1, err)
|
||||
}
|
||||
|
||||
fileName, err := extractFileNameFromSection(section)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("section %d: %w", i+1, err)
|
||||
}
|
||||
name := fileName
|
||||
|
||||
quadlets = append(quadlets, quadletSection{
|
||||
content: section,
|
||||
extension: extension,
|
||||
name: name,
|
||||
})
|
||||
}
|
||||
|
||||
if len(quadlets) == 0 {
|
||||
return nil, fmt.Errorf("no valid quadlet sections found in file %s", filePath)
|
||||
}
|
||||
|
||||
return quadlets, nil
|
||||
}
|
||||
|
||||
// extractFileNameFromSection extracts the FileName from a comment in the quadlet section
|
||||
// The comment must be in the format: # FileName=my-name
|
||||
func extractFileNameFromSection(content string) (string, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
// Look for comment lines starting with #
|
||||
if strings.HasPrefix(line, "#") {
|
||||
// Remove the # and trim whitespace
|
||||
commentContent := strings.TrimSpace(line[1:])
|
||||
// Check if it's a FileName directive
|
||||
if strings.HasPrefix(commentContent, "FileName=") {
|
||||
fileName := strings.TrimSpace(commentContent[9:]) // Remove "FileName="
|
||||
if fileName == "" {
|
||||
return "", fmt.Errorf("FileName comment found but no filename specified")
|
||||
}
|
||||
// Validate filename (basic validation - no path separators)
|
||||
if strings.ContainsAny(fileName, "/\\") {
|
||||
return "", fmt.Errorf("FileName '%s' cannot contain path separators", fileName)
|
||||
}
|
||||
return fileName, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("missing required '# FileName=<name>' comment at the beginning of quadlet section")
|
||||
}
|
||||
|
||||
// detectQuadletType analyzes the content of a quadlet section to determine its type
|
||||
// Returns the appropriate file extension (.container, .volume, .network, etc.)
|
||||
func detectQuadletType(content string) (string, error) {
|
||||
// Look for section headers like [Container], [Volume], [Network], etc.
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
sectionName := strings.ToLower(strings.Trim(line, "[]"))
|
||||
expected := "." + sectionName
|
||||
if systemdquadlet.IsExtSupported("a" + expected) {
|
||||
return expected, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no recognized quadlet section found (expected [Container], [Volume], [Network], [Kube], [Image], [Build], or [Pod])")
|
||||
}
|
||||
|
||||
// buildAppMap scans the given directory for files that start with '.'
|
||||
// and end with '.app', reads their contents (one filename per line), and
|
||||
// returns a map where each filename maps to the .app file that contains it.
|
||||
|
||||
404
test/system/254-podman-quadlet-multi.bats
Normal file
404
test/system/254-podman-quadlet-multi.bats
Normal file
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env bats -*- bats -*-
|
||||
#
|
||||
# Tests for podman quadlet install with multi-quadlet files
|
||||
#
|
||||
|
||||
load helpers
|
||||
load helpers.systemd
|
||||
|
||||
function setup() {
|
||||
skip_if_remote "podman quadlet is not implemented for remote setup yet"
|
||||
skip_if_rootless_cgroupsv1 "Can't use --cgroups=split w/ CGv1 (issue 17456, wontfix)"
|
||||
skip_if_journald_unavailable "Needed for RHEL. FIXME: we might be able to re-enable a subset of tests."
|
||||
|
||||
basic_setup
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
# remove any remaining quadlets from tests
|
||||
run_podman quadlet rm --all -f
|
||||
systemctl daemon-reload
|
||||
basic_teardown
|
||||
}
|
||||
|
||||
# Helper function to get the systemd install directory based on rootless/root mode
|
||||
function get_quadlet_install_dir() {
|
||||
if is_rootless; then
|
||||
# For rootless: $XDG_CONFIG_HOME/containers/systemd or ~/.config/containers/systemd
|
||||
local config_home=${XDG_CONFIG_HOME:-$HOME/.config}
|
||||
echo "$config_home/containers/systemd"
|
||||
else
|
||||
# For root: /etc/containers/systemd
|
||||
echo "/etc/containers/systemd"
|
||||
fi
|
||||
}
|
||||
|
||||
@test "quadlet verb - install multi-quadlet file" {
|
||||
# Determine the install directory path based on rootless/root
|
||||
local install_dir=$(get_quadlet_install_dir)
|
||||
|
||||
# Generate random names for parallelism
|
||||
local app_name="webapp_$(random_string)"
|
||||
local container_name="webserver_$(random_string)"
|
||||
local volume_name="appstorage_$(random_string)"
|
||||
local network_name="appnetwork_$(random_string)"
|
||||
|
||||
# Create a multi-quadlet file with additional systemd sections
|
||||
local multi_quadlet_file=$PODMAN_TMPDIR/${app_name}.quadlets
|
||||
cat > $multi_quadlet_file <<EOF
|
||||
# FileName=$container_name
|
||||
# Web application stack
|
||||
[Unit]
|
||||
Description=Web server container for application
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Container]
|
||||
Image=$IMAGE
|
||||
ContainerName=web-server-$(random_string)
|
||||
PublishPort=8080:80
|
||||
Environment=APP_ENV=production
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
TimeoutStartSec=900
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
---
|
||||
|
||||
# FileName=$volume_name
|
||||
# Database volume
|
||||
[Unit]
|
||||
Description=Database storage volume
|
||||
Documentation=https://example.com/storage-docs
|
||||
|
||||
[Volume]
|
||||
Label=app=$app_name
|
||||
Label=component=database
|
||||
Driver=local
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
---
|
||||
|
||||
# FileName=$network_name
|
||||
# Application network
|
||||
[Unit]
|
||||
Description=Application network for web services
|
||||
|
||||
[Network]
|
||||
Subnet=10.0.0.0/24
|
||||
Gateway=10.0.0.1
|
||||
Label=app=$app_name
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Test quadlet install with multi-quadlet file
|
||||
run_podman quadlet install $multi_quadlet_file
|
||||
|
||||
# Verify install output contains all three quadlet names
|
||||
assert "$output" =~ "${container_name}.container" "install output should contain ${container_name}.container"
|
||||
assert "$output" =~ "${volume_name}.volume" "install output should contain ${volume_name}.volume"
|
||||
assert "$output" =~ "${network_name}.network" "install output should contain ${network_name}.network"
|
||||
|
||||
# Count lines in output (should be 3 lines, one for each quadlet)
|
||||
assert "${#lines[@]}" -eq 3 "install output should contain exactly three lines"
|
||||
|
||||
# Test quadlet list to verify all quadlets were installed
|
||||
run_podman quadlet list
|
||||
assert "$output" =~ "${container_name}.container" "list should contain ${container_name}.container"
|
||||
assert "$output" =~ "${volume_name}.volume" "list should contain ${volume_name}.volume"
|
||||
assert "$output" =~ "${network_name}.network" "list should contain ${network_name}.network"
|
||||
|
||||
# Verify the files exist on disk
|
||||
[[ -f "$install_dir/${container_name}.container" ]] || die "${container_name}.container should exist on disk"
|
||||
[[ -f "$install_dir/${volume_name}.volume" ]] || die "${volume_name}.volume should exist on disk"
|
||||
[[ -f "$install_dir/${network_name}.network" ]] || die "${network_name}.network should exist on disk"
|
||||
|
||||
# Test quadlet print for each installed quadlet and verify systemd sections are preserved
|
||||
run_podman quadlet print ${container_name}.container
|
||||
assert "$output" =~ "\\[Unit\\]" "print should show Unit section"
|
||||
assert "$output" =~ "Description=Web server container" "print should show Unit description"
|
||||
assert "$output" =~ "After=network.target" "print should show After directive"
|
||||
assert "$output" =~ "Wants=network.target" "print should show Wants directive"
|
||||
assert "$output" =~ "\\[Container\\]" "print should show container section"
|
||||
assert "$output" =~ "Image=$IMAGE" "print should show correct image"
|
||||
assert "$output" =~ "Environment=APP_ENV=production" "print should show environment variable"
|
||||
assert "$output" =~ "\\[Service\\]" "print should show Service section"
|
||||
assert "$output" =~ "Restart=always" "print should show Restart directive"
|
||||
assert "$output" =~ "TimeoutStartSec=900" "print should show TimeoutStartSec directive"
|
||||
assert "$output" =~ "\\[Install\\]" "print should show Install section"
|
||||
assert "$output" =~ "WantedBy=multi-user.target" "print should show WantedBy directive"
|
||||
|
||||
run_podman quadlet print ${volume_name}.volume
|
||||
assert "$output" =~ "\\[Unit\\]" "print should show Unit section"
|
||||
assert "$output" =~ "Description=Database storage volume" "print should show Unit description"
|
||||
assert "$output" =~ "Documentation=https://example.com/storage-docs" "print should show Documentation directive"
|
||||
assert "$output" =~ "\\[Volume\\]" "print should show volume section"
|
||||
assert "$output" =~ "Label=app=$app_name" "print should show app label"
|
||||
assert "$output" =~ "Driver=local" "print should show Driver directive"
|
||||
assert "$output" =~ "\\[Install\\]" "print should show Install section"
|
||||
assert "$output" =~ "WantedBy=multi-user.target" "print should show WantedBy directive"
|
||||
|
||||
run_podman quadlet print ${network_name}.network
|
||||
assert "$output" =~ "\\[Unit\\]" "print should show Unit section"
|
||||
assert "$output" =~ "Description=Application network" "print should show Unit description"
|
||||
assert "$output" =~ "\\[Network\\]" "print should show network section"
|
||||
assert "$output" =~ "Subnet=10.0.0.0/24" "print should show subnet"
|
||||
assert "$output" =~ "\\[Install\\]" "print should show Install section"
|
||||
assert "$output" =~ "WantedBy=multi-user.target" "print should show WantedBy directive"
|
||||
|
||||
# Check that the .app file was created (all quadlets are part of the same application)
|
||||
[[ -f "$install_dir/.${app_name}.app" ]] || die ".${app_name}.app file should exist"
|
||||
[[ ! -f "$install_dir/.${container_name}.container.asset" ]] || die "individual .asset files should not exist"
|
||||
[[ ! -f "$install_dir/.${volume_name}.volume.asset" ]] || die "individual .asset files should not exist"
|
||||
[[ ! -f "$install_dir/.${network_name}.network.asset" ]] || die "individual .asset files should not exist"
|
||||
|
||||
# Verify the .app file contains all quadlet names
|
||||
run cat "$install_dir/.${app_name}.app"
|
||||
assert "$output" =~ "${container_name}.container" ".app file should contain ${container_name}.container"
|
||||
assert "$output" =~ "${volume_name}.volume" ".app file should contain ${volume_name}.volume"
|
||||
assert "$output" =~ "${network_name}.network" ".app file should contain ${network_name}.network"
|
||||
|
||||
# Test quadlet list to verify all quadlets show the same app name
|
||||
run_podman quadlet list
|
||||
local webserver_line=$(echo "$output" | grep "${container_name}.container")
|
||||
local appstorage_line=$(echo "$output" | grep "${volume_name}.volume")
|
||||
local appnetwork_line=$(echo "$output" | grep "${network_name}.network")
|
||||
|
||||
# All lines should contain the same app name (.${app_name}.app)
|
||||
assert "$webserver_line" =~ "\\.${app_name}\\.app" "${container_name} should show .${app_name}.app as app"
|
||||
assert "$appstorage_line" =~ "\\.${app_name}\\.app" "${volume_name} should show .${app_name}.app as app"
|
||||
assert "$appnetwork_line" =~ "\\.${app_name}\\.app" "${network_name} should show .${app_name}.app as app"
|
||||
|
||||
# Test quadlet rm for one of the quadlets - should remove entire application
|
||||
run_podman quadlet rm ${container_name}.container
|
||||
assert "$output" =~ "${container_name}.container" "remove output should contain ${container_name}.container"
|
||||
|
||||
# Verify all quadlets were removed since they're part of the same app
|
||||
run_podman quadlet list
|
||||
assert "$output" !~ "${container_name}.container" "list should not contain removed ${container_name}.container"
|
||||
assert "$output" !~ "${volume_name}.volume" "list should not contain ${volume_name}.volume as app is removed"
|
||||
assert "$output" !~ "${network_name}.network" "list should not contain ${network_name}.network as app is removed"
|
||||
|
||||
# The .app file should also be removed
|
||||
[[ ! -f "$install_dir/.${app_name}.app" ]] || die ".${app_name}.app file should be removed"
|
||||
}
|
||||
|
||||
@test "quadlet verb - install multi-quadlet file with empty sections" {
|
||||
# Test handling of empty sections between separators
|
||||
local container_name="testcontainer_$(random_string)"
|
||||
local volume_name="testvolume_$(random_string)"
|
||||
local multi_quadlet_file=$PODMAN_TMPDIR/with-empty_$(random_string).quadlets
|
||||
cat > $multi_quadlet_file <<EOF
|
||||
# FileName=$container_name
|
||||
[Container]
|
||||
Image=$IMAGE
|
||||
ContainerName=test-container-$(random_string)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# FileName=$volume_name
|
||||
[Volume]
|
||||
Label=test=value
|
||||
|
||||
---
|
||||
|
||||
EOF
|
||||
|
||||
# Test quadlet install
|
||||
run_podman quadlet install $multi_quadlet_file
|
||||
|
||||
# Should only install 2 quadlets (empty sections should be skipped)
|
||||
assert "$output" =~ "${container_name}.container" "install output should contain ${container_name}.container"
|
||||
assert "$output" =~ "${volume_name}.volume" "install output should contain ${volume_name}.volume"
|
||||
assert "${#lines[@]}" -eq 2 "install output should contain exactly two lines"
|
||||
|
||||
# Clean up
|
||||
run_podman quadlet rm ${container_name}.container ${volume_name}.volume
|
||||
}
|
||||
|
||||
@test "quadlet verb - install multi-quadlet file missing FileName" {
|
||||
# Test error handling when FileName is missing in multi-quadlet file
|
||||
local volume_name="testvolume_$(random_string)"
|
||||
local multi_quadlet_file=$PODMAN_TMPDIR/missing-filename_$(random_string).quadlets
|
||||
cat > $multi_quadlet_file <<EOF
|
||||
[Container]
|
||||
Image=$IMAGE
|
||||
ContainerName=test-container-$(random_string)
|
||||
|
||||
---
|
||||
|
||||
# FileName=$volume_name
|
||||
[Volume]
|
||||
Label=test=value
|
||||
EOF
|
||||
|
||||
# Test quadlet install should fail
|
||||
run_podman 125 quadlet install $multi_quadlet_file
|
||||
assert "$output" =~ "missing required.*FileName" "error should mention missing FileName"
|
||||
}
|
||||
|
||||
@test "quadlet verb - install single-section .quadlets file missing FileName" {
|
||||
# Test error handling when FileName is missing in a .quadlets file with only one section
|
||||
local multi_quadlet_file=$PODMAN_TMPDIR/single-missing-filename_$(random_string).quadlets
|
||||
cat > $multi_quadlet_file <<EOF
|
||||
[Container]
|
||||
Image=$IMAGE
|
||||
ContainerName=test-container-$(random_string)
|
||||
EOF
|
||||
|
||||
# Test quadlet install should fail
|
||||
run_podman 125 quadlet install $multi_quadlet_file
|
||||
assert "$output" =~ "missing required.*FileName" "error should mention missing FileName"
|
||||
}
|
||||
|
||||
@test "quadlet verb - install directory with mixed individual and .quadlets files" {
|
||||
# Test installing from a directory containing both individual quadlet files and .quadlets files
|
||||
local install_dir=$(get_quadlet_install_dir)
|
||||
local app_name="mixed-app_$(random_string)"
|
||||
local app_dir=$PODMAN_TMPDIR/$app_name
|
||||
mkdir -p "$app_dir"
|
||||
|
||||
# Generate random names for all components
|
||||
local frontend_name="frontend_$(random_string)"
|
||||
local data_name="data_$(random_string)"
|
||||
local api_name="api-server_$(random_string)"
|
||||
local cache_name="cache_$(random_string)"
|
||||
local network_name="app-network_$(random_string)"
|
||||
|
||||
# Create an individual container quadlet file
|
||||
cat > "$app_dir/${frontend_name}.container" <<EOF
|
||||
[Container]
|
||||
Image=$IMAGE
|
||||
ContainerName=frontend-app-$(random_string)
|
||||
PublishPort=3000:3000
|
||||
EOF
|
||||
|
||||
# Create an individual volume quadlet file
|
||||
cat > "$app_dir/${data_name}.volume" <<EOF
|
||||
[Volume]
|
||||
Label=app=$app_name
|
||||
Label=component=storage
|
||||
EOF
|
||||
|
||||
# Create a .quadlets file with multiple quadlets
|
||||
cat > "$app_dir/backend_$(random_string).quadlets" <<EOF
|
||||
# FileName=$api_name
|
||||
[Container]
|
||||
Image=$IMAGE
|
||||
ContainerName=api-server-$(random_string)
|
||||
PublishPort=8080:8080
|
||||
|
||||
---
|
||||
|
||||
# FileName=$cache_name
|
||||
[Volume]
|
||||
Label=app=$app_name
|
||||
Label=component=cache
|
||||
|
||||
---
|
||||
|
||||
# FileName=$network_name
|
||||
[Network]
|
||||
Subnet=192.168.1.0/24
|
||||
Gateway=192.168.1.1
|
||||
Label=app=$app_name
|
||||
EOF
|
||||
|
||||
# Create a non-quadlet asset file (config file)
|
||||
cat > "$app_dir/app.conf" <<EOF
|
||||
# Application configuration
|
||||
debug=true
|
||||
port=3000
|
||||
EOF
|
||||
|
||||
# Install the directory
|
||||
run_podman quadlet install "$app_dir"
|
||||
|
||||
# Verify all quadlets were installed (2 individual + 3 from .quadlets file = 5 total)
|
||||
assert "$output" =~ "${frontend_name}.container" "install output should contain ${frontend_name}.container"
|
||||
assert "$output" =~ "${data_name}.volume" "install output should contain ${data_name}.volume"
|
||||
assert "$output" =~ "${api_name}.container" "install output should contain ${api_name}.container"
|
||||
assert "$output" =~ "${cache_name}.volume" "install output should contain ${cache_name}.volume"
|
||||
assert "$output" =~ "${network_name}.network" "install output should contain ${network_name}.network"
|
||||
|
||||
# Count lines in output (should be 6 lines: 5 quadlets + 1 asset file)
|
||||
assert "${#lines[@]}" -eq 6 "install output should contain exactly six lines"
|
||||
|
||||
# Verify all files exist on disk
|
||||
[[ -f "$install_dir/${frontend_name}.container" ]] || die "${frontend_name}.container should exist on disk"
|
||||
[[ -f "$install_dir/${data_name}.volume" ]] || die "${data_name}.volume should exist on disk"
|
||||
[[ -f "$install_dir/${api_name}.container" ]] || die "${api_name}.container should exist on disk"
|
||||
[[ -f "$install_dir/${cache_name}.volume" ]] || die "${cache_name}.volume should exist on disk"
|
||||
[[ -f "$install_dir/${network_name}.network" ]] || die "${network_name}.network should exist on disk"
|
||||
[[ -f "$install_dir/app.conf" ]] || die "app.conf should exist on disk"
|
||||
|
||||
# Check that the .app file was created (all files are part of one application)
|
||||
[[ -f "$install_dir/.${app_name}.app" ]] || die ".${app_name}.app file should exist"
|
||||
|
||||
# Verify the .app file contains all quadlet names
|
||||
run cat "$install_dir/.${app_name}.app"
|
||||
assert "$output" =~ "${frontend_name}.container" ".app file should contain ${frontend_name}.container"
|
||||
assert "$output" =~ "${data_name}.volume" ".app file should contain ${data_name}.volume"
|
||||
assert "$output" =~ "${api_name}.container" ".app file should contain ${api_name}.container"
|
||||
assert "$output" =~ "${cache_name}.volume" ".app file should contain ${cache_name}.volume"
|
||||
assert "$output" =~ "${network_name}.network" ".app file should contain ${network_name}.network"
|
||||
|
||||
# Test quadlet list to verify all quadlets show the same app name
|
||||
run_podman quadlet list
|
||||
local frontend_line=$(echo "$output" | grep "${frontend_name}.container")
|
||||
local data_line=$(echo "$output" | grep "${data_name}.volume")
|
||||
local api_line=$(echo "$output" | grep "${api_name}.container")
|
||||
local cache_line=$(echo "$output" | grep "${cache_name}.volume")
|
||||
local network_line=$(echo "$output" | grep "${network_name}.network")
|
||||
|
||||
# All lines should contain the same app name (.${app_name}.app)
|
||||
assert "$frontend_line" =~ "\\.${app_name}\\.app" "${frontend_name} should show .${app_name}.app as app"
|
||||
assert "$data_line" =~ "\\.${app_name}\\.app" "${data_name} should show .${app_name}.app as app"
|
||||
assert "$api_line" =~ "\\.${app_name}\\.app" "${api_name} should show .${app_name}.app as app"
|
||||
assert "$cache_line" =~ "\\.${app_name}\\.app" "${cache_name} should show .${app_name}.app as app"
|
||||
assert "$network_line" =~ "\\.${app_name}\\.app" "${network_name} should show .${app_name}.app as app"
|
||||
|
||||
# Verify content of individual quadlet files
|
||||
run cat "$install_dir/${frontend_name}.container"
|
||||
assert "$output" =~ "\\[Container\\]" "frontend container file should contain [Container] section"
|
||||
assert "$output" =~ "ContainerName=frontend-app-" "frontend container file should contain correct name prefix"
|
||||
|
||||
run cat "$install_dir/${api_name}.container"
|
||||
assert "$output" =~ "\\[Container\\]" "api-server container file should contain [Container] section"
|
||||
assert "$output" =~ "ContainerName=api-server-" "api-server container file should contain correct name prefix"
|
||||
|
||||
run cat "$install_dir/${network_name}.network"
|
||||
assert "$output" =~ "\\[Network\\]" "network file should contain [Network] section"
|
||||
assert "$output" =~ "Subnet=192.168.1.0/24" "network file should contain correct subnet"
|
||||
|
||||
# Test that removing one quadlet removes the entire application
|
||||
run_podman quadlet rm ${frontend_name}.container
|
||||
|
||||
# All quadlets should be removed since they're part of the same app
|
||||
run_podman quadlet list
|
||||
assert "$output" !~ "${frontend_name}.container" "${frontend_name}.container should be removed"
|
||||
assert "$output" !~ "${data_name}.volume" "${data_name}.volume should also be removed as part of same app"
|
||||
assert "$output" !~ "${api_name}.container" "${api_name}.container should also be removed as part of same app"
|
||||
assert "$output" !~ "${cache_name}.volume" "${cache_name}.volume should also be removed as part of same app"
|
||||
assert "$output" !~ "${network_name}.network" "${network_name}.network should also be removed as part of same app"
|
||||
|
||||
# The .app file should also be removed
|
||||
[[ ! -f "$install_dir/.${app_name}.app" ]] || die ".${app_name}.app file should be removed"
|
||||
|
||||
# All individual files should be removed
|
||||
[[ ! -f "$install_dir/${frontend_name}.container" ]] || die "${frontend_name}.container should be removed"
|
||||
[[ ! -f "$install_dir/${data_name}.volume" ]] || die "${data_name}.volume should be removed"
|
||||
[[ ! -f "$install_dir/${api_name}.container" ]] || die "${api_name}.container should be removed"
|
||||
[[ ! -f "$install_dir/${cache_name}.volume" ]] || die "${cache_name}.volume should be removed"
|
||||
[[ ! -f "$install_dir/${network_name}.network" ]] || die "${network_name}.network should be removed"
|
||||
[[ ! -f "$install_dir/app.conf" ]] || die "app.conf should be removed"
|
||||
}
|
||||
Reference in New Issue
Block a user