cmd/podman: add replace flag to quadlet install

Signed-off-by: Celso Henrique Souza Silva <celsohenrique367@gmail.com>
Fixes: #26930
This commit is contained in:
Celso Henrique Souza Silva
2025-10-07 22:16:19 -03:00
committed by nothiaki
parent b342596e1c
commit 0ea4eaee70
5 changed files with 59 additions and 5 deletions

View File

@@ -36,6 +36,7 @@ podman quadlet install https://github.com/containers/podman/blob/main/test/e2e/q
func installFlags(cmd *cobra.Command) { func installFlags(cmd *cobra.Command) {
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&installOptions.ReloadSystemd, "reload-systemd", true, "Reload systemd after installing Quadlets") flags.BoolVar(&installOptions.ReloadSystemd, "reload-systemd", true, "Reload systemd after installing Quadlets")
flags.BoolVarP(&installOptions.Replace, "replace", "r", false, "Replace the installation even if the quadlet already exists")
} }
func init() { func init() {

View File

@@ -28,6 +28,12 @@ Reload systemd after installing Quadlets (default true).
In order to disable it users need to manually set the value In order to disable it users need to manually set the value
of this flag to `false`. of this flag to `false`.
#### **--replace**, **-r**
Replace the Quadlet installation even if the generated unit file already exists (default false).
In order to enable it, users need to manually set the value
of this flag to `true`. This flag is used primarily to update an existing unit.
## EXAMPLES ## EXAMPLES
Install quadlet from a file. Install quadlet from a file.

View File

@@ -4,6 +4,8 @@ package entities
type QuadletInstallOptions struct { type QuadletInstallOptions struct {
// Whether to reload systemd after installation is completed // Whether to reload systemd after installation is completed
ReloadSystemd bool ReloadSystemd bool
// Replace the installation even if the quadlet already exists
Replace bool
} }
// QuadletInstallReport contains the output of the `quadlet install` command // QuadletInstallReport contains the output of the `quadlet install` command

View File

@@ -197,7 +197,7 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
installReport.QuadletErrors[toInstall] = fmt.Errorf("populating temporary file: %w", err) installReport.QuadletErrors[toInstall] = fmt.Errorf("populating temporary file: %w", err)
continue continue
} }
installedPath, err := ic.installQuadlet(ctx, tmpFile.Name(), quadletFileName, installDir, assetFile, validateQuadletFile) installedPath, err := ic.installQuadlet(ctx, tmpFile.Name(), quadletFileName, installDir, assetFile, validateQuadletFile, options.Replace)
if err != nil { if err != nil {
installReport.QuadletErrors[toInstall] = err installReport.QuadletErrors[toInstall] = err
continue continue
@@ -210,7 +210,7 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
continue continue
} }
// If toInstall is a single file, execute the original logic // If toInstall is a single file, execute the original logic
installedPath, err := ic.installQuadlet(ctx, toInstall, "", installDir, assetFile, validateQuadletFile) installedPath, err := ic.installQuadlet(ctx, toInstall, "", installDir, assetFile, validateQuadletFile, options.Replace)
if err != nil { if err != nil {
installReport.QuadletErrors[toInstall] = err installReport.QuadletErrors[toInstall] = err
continue continue
@@ -254,7 +254,7 @@ func getFileName(resp *http.Response, fileURL string) (string, error) {
// Perform some minimal validation, but not much. // Perform some minimal validation, but not much.
// We can't know about a lot of problems without running the Quadlet binary, which we // We can't know about a lot of problems without running the Quadlet binary, which we
// only want to do once. // only want to do once.
func (ic *ContainerEngine) installQuadlet(_ context.Context, path, destName, installDir, assetFile string, isQuadletFile bool) (string, error) { func (ic *ContainerEngine) installQuadlet(_ context.Context, path, destName, installDir, assetFile string, isQuadletFile, replace bool) (string, error) {
// First, validate that the source path exists and is a file // First, validate that the source path exists and is a file
stat, err := os.Stat(path) stat, err := os.Stat(path)
if err != nil { if err != nil {
@@ -274,9 +274,15 @@ func (ic *ContainerEngine) installQuadlet(_ context.Context, path, destName, ins
return "", fmt.Errorf("%q is not a supported Quadlet file type", filepath.Ext(finalPath)) return "", fmt.Errorf("%q is not a supported Quadlet file type", filepath.Ext(finalPath))
} }
file, err := os.OpenFile(finalPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) var osFlags = os.O_CREATE | os.O_WRONLY
if !replace {
osFlags |= os.O_EXCL
}
file, err := os.OpenFile(finalPath, osFlags, 0644)
if err != nil { if err != nil {
if errors.Is(err, fs.ErrExist) { if errors.Is(err, fs.ErrExist) && !replace {
return "", fmt.Errorf("a Quadlet with name %s already exists, refusing to overwrite", filepath.Base(finalPath)) return "", fmt.Errorf("a Quadlet with name %s already exists, refusing to overwrite", filepath.Base(finalPath))
} }
return "", fmt.Errorf("unable to open file %s: %w", filepath.Base(finalPath), err) return "", fmt.Errorf("unable to open file %s: %w", filepath.Base(finalPath), err)

View File

@@ -461,4 +461,43 @@ EOF
assert $status -eq 0 "quadlet rm --ignore should succeed even for non-existent quadlets" assert $status -eq 0 "quadlet rm --ignore should succeed even for non-existent quadlets"
} }
@test "quadlet install --replace" {
local install_dir=$(get_quadlet_install_dir)
# Create a test quadlet file
local quadlet_file=$PODMAN_TMPDIR/alpine-quadlet.container
local initial_exec='Exec=sh -c "echo STARTED CONTAINER; trap '\''exit'\'' SIGTERM; while :; do sleep 0.1; done"'
cat > $quadlet_file <<EOF
[Container]
Image=$IMAGE
$initial_exec
EOF
# Test quadlet install
run_podman quadlet install $quadlet_file
# Verify install output contains the quadlet name on a single line
assert "$output" =~ "alpine-quadlet.container" "install output should contain quadlet name"
# Without replace should fail
run_podman 125 quadlet install $quadlet_file
assert "$output" =~ "refusing to overwrite" "reinstall without --replace must fail with the overwrite error message"
cat > $quadlet_file <<EOF
[Container]
Image=$IMAGE
Exec=sh -c "echo STARTED CONTAINER UPDATED; trap 'exit' SIGTERM; while :; do sleep 0.1; done"
EOF
# With replace should pass and update quadlet
run_podman quadlet install --replace $quadlet_file
# Verify install output contains the quadlet name on a single line
assert "$output" =~ "alpine-quadlet.container" "install output should contain quadlet name"
run_podman quadlet print alpine-quadlet.container
assert "$output" !~ "$initial_exec" "Printed content must not show the initial version"
assert "$output" == "$(<$quadlet_file)" "Printed content must match the updated file content"
# Clean up
run_podman quadlet rm alpine-quadlet.container
}
# vim: filetype=sh # vim: filetype=sh