mirror of
https://github.com/containers/podman.git
synced 2026-02-05 15:45:08 +01:00
Merge pull request #27730 from inknos/delete-quadlet-api
Add DELETE /libpod/quadlets
This commit is contained in:
@@ -62,6 +62,9 @@ var (
|
||||
ErrCtrStateInvalid = errors.New("container state improper")
|
||||
// ErrCtrStateRunning indicates a container is running.
|
||||
ErrCtrStateRunning = errors.New("container is running")
|
||||
// ErrQuadletRunning indicates the quadlet is running and cannot be
|
||||
// removed without force.
|
||||
ErrQuadletRunning = errors.New("quadlet is running")
|
||||
// ErrExecSessionStateInvalid indicates that an exec session is in an
|
||||
// improper state for the requested operation
|
||||
ErrExecSessionStateInvalid = errors.New("exec session state improper")
|
||||
|
||||
@@ -9,10 +9,12 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.podman.io/storage/pkg/archive"
|
||||
|
||||
"github.com/containers/podman/v6/libpod"
|
||||
"github.com/containers/podman/v6/libpod/define"
|
||||
"github.com/containers/podman/v6/pkg/api/handlers/utils"
|
||||
api "github.com/containers/podman/v6/pkg/api/types"
|
||||
"github.com/containers/podman/v6/pkg/domain/entities"
|
||||
@@ -238,3 +240,115 @@ func InstallQuadlets(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
utils.WriteResponse(w, http.StatusOK, installReport)
|
||||
}
|
||||
|
||||
// RemoveQuadlet handles DELETE /libpod/quadlets/{name} to remove a quadlet file
|
||||
func RemoveQuadlet(w http.ResponseWriter, r *http.Request) {
|
||||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
||||
|
||||
query := struct {
|
||||
Force bool `schema:"force"`
|
||||
Ignore bool `schema:"ignore"`
|
||||
ReloadSystemd bool `schema:"reload-systemd"`
|
||||
}{
|
||||
ReloadSystemd: true, // Default to true like CLI
|
||||
}
|
||||
|
||||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
||||
return
|
||||
}
|
||||
|
||||
name := utils.GetName(r)
|
||||
if name == "" {
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("quadlet name must be provided"))
|
||||
return
|
||||
}
|
||||
|
||||
containerEngine := abi.ContainerEngine{Libpod: runtime}
|
||||
removeOptions := entities.QuadletRemoveOptions{
|
||||
Force: query.Force,
|
||||
Ignore: query.Ignore,
|
||||
ReloadSystemd: query.ReloadSystemd,
|
||||
}
|
||||
|
||||
removeReport, err := containerEngine.QuadletRemove(r.Context(), []string{name}, removeOptions)
|
||||
if err != nil {
|
||||
// For systemd connection errors and other internal errors
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there are errors in the report for this specific quadlet
|
||||
if err, ok := removeReport.Errors[name]; ok {
|
||||
// If ignore=false and quadlet not found, return 404
|
||||
if !query.Ignore && strings.Contains(err.Error(), "no such") {
|
||||
utils.Error(w, http.StatusNotFound, fmt.Errorf("no such quadlet: %s: %w", name, err))
|
||||
return
|
||||
}
|
||||
// If force=false and quadlet is running, return 400
|
||||
if !query.Force && errors.Is(err, define.ErrQuadletRunning) {
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("quadlet %s is running and force is not set, refusing to remove: %w", name, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
utils.WriteResponse(w, http.StatusOK, removeReport)
|
||||
}
|
||||
|
||||
// RemoveQuadlets handles DELETE /libpod/quadlets to remove quadlet files (batch operation)
|
||||
func RemoveQuadlets(w http.ResponseWriter, r *http.Request) {
|
||||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
||||
|
||||
query := struct {
|
||||
All bool `schema:"all"`
|
||||
Force bool `schema:"force"`
|
||||
Ignore bool `schema:"ignore"`
|
||||
ReloadSystemd bool `schema:"reload-systemd"`
|
||||
Quadlets []string `schema:"quadlets"`
|
||||
}{
|
||||
ReloadSystemd: true, // Default to true like CLI
|
||||
}
|
||||
|
||||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that either all=true OR at least one quadlet name is provided
|
||||
if !query.All && len(query.Quadlets) == 0 {
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("must provide at least 1 quadlet to remove or set all=true"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that both all and quadlets are not provided together
|
||||
if query.All && len(query.Quadlets) > 0 {
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("when setting all=true, you may not pass any quadlet names"))
|
||||
return
|
||||
}
|
||||
|
||||
containerEngine := abi.ContainerEngine{Libpod: runtime}
|
||||
removeOptions := entities.QuadletRemoveOptions{
|
||||
Force: query.Force,
|
||||
All: query.All,
|
||||
Ignore: query.Ignore,
|
||||
ReloadSystemd: query.ReloadSystemd,
|
||||
}
|
||||
|
||||
removeReport, err := containerEngine.QuadletRemove(r.Context(), query.Quadlets, removeOptions)
|
||||
if err != nil {
|
||||
// Check if it's a "must provide at least 1 quadlet" error (shouldn't happen due to validation above, but handle it)
|
||||
if strings.Contains(err.Error(), "must provide at least 1 quadlet") {
|
||||
utils.Error(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
// For systemd connection errors and other internal errors
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return 200 with the report containing errors (if any)
|
||||
// The CLI behavior returns success even with partial errors
|
||||
utils.WriteResponse(w, http.StatusOK, removeReport)
|
||||
}
|
||||
|
||||
@@ -534,3 +534,10 @@ type quadletFileResponse struct {
|
||||
// in:body
|
||||
Body string
|
||||
}
|
||||
|
||||
// Quadlet remove
|
||||
// swagger:response
|
||||
type quadletRemoveResponse struct {
|
||||
// in:body
|
||||
Body entities.QuadletRemoveReport
|
||||
}
|
||||
|
||||
@@ -109,5 +109,90 @@ func (s *APIServer) registerQuadletHandlers(r *mux.Router) error {
|
||||
// 500:
|
||||
// $ref: "#/responses/internalError"
|
||||
r.HandleFunc(VersionedPath("/libpod/quadlets"), s.APIHandler(libpod.InstallQuadlets)).Methods(http.MethodPost)
|
||||
// swagger:operation DELETE /libpod/quadlets libpod QuadletDeleteAllLibpod
|
||||
// ---
|
||||
// tags:
|
||||
// - quadlets
|
||||
// summary: Remove quadlet files (batch operation)
|
||||
// description: |
|
||||
// Remove one or more quadlet files. Supports removing specific quadlets by name or all quadlets
|
||||
// for the current user. Can force removal of running quadlets and control systemd reload behavior.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - in: query
|
||||
// name: quadlets
|
||||
// type: array
|
||||
// items:
|
||||
// type: string
|
||||
// description: Names of quadlets to remove (e.g., "myapp.container"). Required unless all=true
|
||||
// - in: query
|
||||
// name: all
|
||||
// type: boolean
|
||||
// default: false
|
||||
// description: Remove all quadlets for the current user
|
||||
// - in: query
|
||||
// name: force
|
||||
// type: boolean
|
||||
// default: false
|
||||
// description: Remove running quadlets by stopping them first
|
||||
// - in: query
|
||||
// name: ignore
|
||||
// type: boolean
|
||||
// default: false
|
||||
// description: Do not error for quadlets that do not exist
|
||||
// - in: query
|
||||
// name: reload-systemd
|
||||
// type: boolean
|
||||
// default: true
|
||||
// description: Reload systemd after removing quadlets
|
||||
// responses:
|
||||
// 200:
|
||||
// $ref: "#/responses/quadletRemoveResponse"
|
||||
// 400:
|
||||
// $ref: "#/responses/badParamError"
|
||||
// 500:
|
||||
// $ref: "#/responses/internalError"
|
||||
r.HandleFunc(VersionedPath("/libpod/quadlets"), s.APIHandler(libpod.RemoveQuadlets)).Methods(http.MethodDelete)
|
||||
// swagger:operation DELETE /libpod/quadlets/{name} libpod QuadletDeleteLibpod
|
||||
// ---
|
||||
// tags:
|
||||
// - quadlets
|
||||
// summary: Remove a quadlet file
|
||||
// description: |
|
||||
// Remove a quadlet file by name. Can force removal of running quadlets and control systemd reload behavior.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - in: path
|
||||
// name: name
|
||||
// type: string
|
||||
// required: true
|
||||
// description: the name of the quadlet with extension (e.g., "myapp.container")
|
||||
// - in: query
|
||||
// name: force
|
||||
// type: boolean
|
||||
// default: false
|
||||
// description: Remove running quadlet by stopping it first
|
||||
// - in: query
|
||||
// name: ignore
|
||||
// type: boolean
|
||||
// default: false
|
||||
// description: Do not error if the quadlet does not exist
|
||||
// - in: query
|
||||
// name: reload-systemd
|
||||
// type: boolean
|
||||
// default: true
|
||||
// description: Reload systemd after removing the quadlet
|
||||
// responses:
|
||||
// 200:
|
||||
// $ref: "#/responses/quadletRemoveResponse"
|
||||
// 400:
|
||||
// $ref: "#/responses/badParamError"
|
||||
// 404:
|
||||
// $ref: "#/responses/quadletNotFound"
|
||||
// 500:
|
||||
// $ref: "#/responses/internalError"
|
||||
r.HandleFunc(VersionedPath("/libpod/quadlets/{name}"), s.APIHandler(libpod.RemoveQuadlet)).Methods(http.MethodDelete)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/podman/v6/libpod/define"
|
||||
"github.com/containers/podman/v6/pkg/domain/entities"
|
||||
"github.com/containers/podman/v6/pkg/rootless"
|
||||
"github.com/containers/podman/v6/pkg/systemd"
|
||||
@@ -896,7 +897,7 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string,
|
||||
needReload = options.ReloadSystemd
|
||||
if unitStatus.ActiveState == "active" {
|
||||
if !options.Force {
|
||||
report.Errors[quadletName] = fmt.Errorf("quadlet %s is running and force is not set, refusing to remove", quadletName)
|
||||
report.Errors[quadletName] = fmt.Errorf("quadlet %s is running and force is not set, refusing to remove: %w", quadletName, define.ErrQuadletRunning)
|
||||
runningQuadlets = append(runningQuadlets, quadletName)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -27,8 +27,11 @@ quadlet_install_dir=$(get_quadlet_install_dir)
|
||||
## Test list endpoint
|
||||
t GET libpod/quadlets/json 200
|
||||
|
||||
# Test 405 for bad endpoint
|
||||
t GET libpod/quadlets/nonexistent.container 405
|
||||
|
||||
# Test 404 for non-existent quadlet
|
||||
t GET libpod/quadlets/nonexistent.container 404
|
||||
t GET libpod/quadlets/nonexistent.container/file 404
|
||||
|
||||
# Install a quadlet with a unique name
|
||||
quadlet_name=quadlet-test-$(cat /proc/sys/kernel/random/uuid)
|
||||
@@ -47,6 +50,7 @@ EOF
|
||||
quadlet_build_file_content=$(cat << EOF
|
||||
[Build]
|
||||
ImageTag=localhost/$quadlet_name
|
||||
File=.
|
||||
EOF
|
||||
)
|
||||
|
||||
@@ -302,4 +306,67 @@ rm -f "$quadlet_install_dir/$containerfile_1"
|
||||
rm -f "$quadlet_install_dir/$containerfile_2"
|
||||
rm -rf $TMPD
|
||||
|
||||
# DELETE endpoint tests
|
||||
TMPDIR=$(mktemp -d podman-apiv2-test.quadlets.XXXXXXXX)
|
||||
quadlet_1=quadlet-test-1-$(cat /proc/sys/kernel/random/uuid).container
|
||||
quadlet_1_content=$(cat << EOF
|
||||
[Container]
|
||||
ContainerName=quadlet-1
|
||||
Image=quay.io/podman/hello
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$quadlet_1_content" > "$TMPDIR/$quadlet_1"
|
||||
podman quadlet install $TMPDIR/$quadlet_1
|
||||
|
||||
t DELETE "libpod/quadlets/$quadlet_1" 200 \
|
||||
'.Removed|length=1' \
|
||||
'.QuadletErrors|length=0'
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_1/file" 404
|
||||
|
||||
t DELETE "libpod/quadlets/nonexistent.container" 200 \
|
||||
'.Removed|length=0' \
|
||||
'.Errors|length=1' \
|
||||
'.Errors~.*could not locate quadlet.*'
|
||||
|
||||
t DELETE "libpod/quadlets/nonexistent.container?ignore=true" 200 \
|
||||
'.Removed|length=1' \
|
||||
'.Errors|length=0' \
|
||||
'.Removed[0]=nonexistent.container'
|
||||
|
||||
# Scenario: install a quadlet, then remove it with --force
|
||||
quadlet_2=quadlet-test-2-$(cat /proc/sys/kernel/random/uuid).container
|
||||
quadlet_2_content=$(cat << EOF
|
||||
[Container]
|
||||
ContainerName=quadlet-2
|
||||
Image=$IMAGE
|
||||
Exec=top
|
||||
EOF
|
||||
)
|
||||
echo "$quadlet_2_content" > "$TMPDIR/$quadlet_2"
|
||||
podman quadlet install $TMPDIR/$quadlet_2
|
||||
|
||||
# these ifs will skip the tests when $DBUS_SESSION_BUS_ADDRESS and
|
||||
# $XDG_RUNTIME_DIR aren't properly defined, therefore running systemctl --user
|
||||
# is not possible without a hack
|
||||
if systemctl --user start "${quadlet_2%.*}.service" > /dev/null 2>&1; then
|
||||
if systemctl --user is-active --quiet ${quadlet_2%.*}.service; then
|
||||
t DELETE "libpod/quadlets/$quadlet_2" 400 \
|
||||
.cause~.*'container'.*'is running and force is not set, refusing to remove'.*
|
||||
|
||||
t DELETE "libpod/quadlets/$quadlet_2?force=true" 200
|
||||
fi
|
||||
fi
|
||||
|
||||
# TODO: enable this test when we have a way to not delete all host's quadlets
|
||||
# https://issues.redhat.com/browse/RUN-4054
|
||||
# t DELETE "libpod/quadlets?all=true" 200
|
||||
|
||||
# clean up
|
||||
rm -rf $TMPDIR
|
||||
|
||||
# bunch of asset files might be left behind so we might need to clean them up
|
||||
find $quadlet_install_dir -type f -regextype posix-extended -regex '.*quadlet-test.*-[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}.*asset$' -delete;
|
||||
|
||||
# vim: filetype=sh
|
||||
|
||||
Reference in New Issue
Block a user