mirror of
https://github.com/containers/podman.git
synced 2026-07-12 00:05:01 -04:00
Merge pull request #28029 from inknos/backport-quadlets-to-5.8
Backport quadlets to 5.8
This commit is contained in:
@@ -323,6 +323,9 @@ sub operation_name {
|
||||
elsif ($action eq "delete" && $endpoint eq "/libpod/play/kube") {
|
||||
$action = "KubeDown"
|
||||
}
|
||||
elsif ($action eq "list" && $endpoint eq "/libpod/quadlets") {
|
||||
$action = "Install"
|
||||
}
|
||||
# Grrrrrr, this one is annoying: some operations get an extra 'All'
|
||||
elsif ($action =~ /^(delete|get|stats)$/ && $endpoint !~ /\{/) {
|
||||
$action .= "All";
|
||||
|
||||
@@ -29,6 +29,9 @@ var (
|
||||
// does not exist.
|
||||
ErrNoSuchExitCode = errors.New("no such exit code")
|
||||
|
||||
// ErrNoSuchQuadlet indicates the requested quadlet does not exist
|
||||
ErrNoSuchQuadlet = errors.New("no such quadlet")
|
||||
|
||||
// ErrDepExists indicates that the current object has dependencies and
|
||||
// cannot be removed before them.
|
||||
ErrDepExists = errors.New("dependency exists")
|
||||
@@ -62,6 +65,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")
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -202,34 +201,6 @@ func processCacheTo(query *BuildQuery, queryValues url.Values) ([]reference.Name
|
||||
return processCacheReferences(query.CacheTo, "cacheto", queryValues)
|
||||
}
|
||||
|
||||
// validateContentType validates the Content-Type header and determines if multipart processing is needed.
|
||||
func validateContentType(r *http.Request) (bool, error) {
|
||||
multipart := false
|
||||
if hdr, found := r.Header["Content-Type"]; found && len(hdr) > 0 {
|
||||
contentType, _, err := mime.ParseMediaType(hdr[0])
|
||||
if err != nil {
|
||||
return false, utils.GetBadRequestError("Content-Type", hdr[0], err)
|
||||
}
|
||||
|
||||
switch contentType {
|
||||
case "application/tar":
|
||||
logrus.Infof("tar file content type is %s, should use \"application/x-tar\" content type", contentType)
|
||||
case "application/x-tar":
|
||||
break
|
||||
case "multipart/form-data":
|
||||
logrus.Infof("Received %s", hdr[0])
|
||||
multipart = true
|
||||
default:
|
||||
if utils.IsLibpodRequest(r) && !utils.IsLibpodLocalRequest(r) {
|
||||
return false, utils.GetBadRequestError("Content-Type", hdr[0],
|
||||
fmt.Errorf("Content-Type: %s is not supported. Should be \"application/x-tar\"", hdr[0]))
|
||||
}
|
||||
logrus.Infof("tar file content type is %s, should use \"application/x-tar\" content type", contentType)
|
||||
}
|
||||
}
|
||||
return multipart, nil
|
||||
}
|
||||
|
||||
// parseBuildQuery parses HTTP query parameters into a BuildQuery struct with defaults.
|
||||
func parseBuildQuery(r *http.Request, conf *config.Config, queryValues url.Values) (*BuildQuery, error) {
|
||||
query := &BuildQuery{
|
||||
@@ -1039,7 +1010,7 @@ func buildImage(w http.ResponseWriter, r *http.Request, getBuildContextFunc getB
|
||||
|
||||
// If we have a multipart we use the operations, if not default extraction for main context
|
||||
// Validate content type
|
||||
multipart, err := validateContentType(r)
|
||||
multipart, err := utils.ValidateContentType(r)
|
||||
if err != nil {
|
||||
utils.ProcessBuildError(w, err)
|
||||
return
|
||||
|
||||
@@ -3,15 +3,26 @@
|
||||
package libpod
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.podman.io/storage/pkg/archive"
|
||||
|
||||
"github.com/containers/podman/v5/libpod"
|
||||
"github.com/containers/podman/v5/libpod/define"
|
||||
"github.com/containers/podman/v5/pkg/api/handlers/utils"
|
||||
api "github.com/containers/podman/v5/pkg/api/types"
|
||||
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||
"github.com/containers/podman/v5/pkg/domain/infra/abi"
|
||||
"github.com/containers/podman/v5/pkg/systemd/quadlet"
|
||||
"github.com/containers/podman/v5/pkg/util"
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func ListQuadlets(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -35,3 +46,328 @@ func ListQuadlets(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
utils.WriteResponse(w, http.StatusOK, quadlets)
|
||||
}
|
||||
|
||||
func GetQuadletPrint(w http.ResponseWriter, r *http.Request) {
|
||||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||||
name := utils.GetName(r)
|
||||
|
||||
containerEngine := abi.ContainerEngine{Libpod: runtime}
|
||||
|
||||
quadletContents, err := containerEngine.QuadletPrint(r.Context(), name)
|
||||
if err != nil {
|
||||
utils.Error(w, http.StatusNotFound, fmt.Errorf("no such quadlet: %s: %w", name, err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write([]byte(quadletContents)); err != nil {
|
||||
logrus.Errorf("Failed to write quadlet contents: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// QuadletExists checks if a quadlet exists by name
|
||||
func QuadletExists(w http.ResponseWriter, r *http.Request) {
|
||||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||||
name := utils.GetName(r)
|
||||
|
||||
containerEngine := abi.ContainerEngine{Libpod: runtime}
|
||||
|
||||
report, err := containerEngine.QuadletExists(r.Context(), name)
|
||||
if err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
if !report.Value {
|
||||
utils.Error(w, http.StatusNotFound, fmt.Errorf("no such quadlet: %s", name))
|
||||
return
|
||||
}
|
||||
utils.WriteResponse(w, http.StatusNoContent, "")
|
||||
}
|
||||
|
||||
// extractQuadletFiles extracts quadlet files from tar archive to a temporary directory
|
||||
func extractQuadletFiles(tempDir string, r io.ReadCloser) ([]string, error) {
|
||||
quadletDir := filepath.Join(tempDir, "quadlets")
|
||||
err := os.Mkdir(quadletDir, 0o700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = archive.Untar(r, quadletDir, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect all files from the extracted directory
|
||||
var filePaths []string
|
||||
err = filepath.Walk(quadletDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
filePaths = append(filePaths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return filePaths, err
|
||||
}
|
||||
|
||||
// processMultipartQuadlets processes multipart form data and saves files to temporary directory
|
||||
func processMultipartQuadlets(tempDir string, r *http.Request) ([]string, error) {
|
||||
quadletDir := filepath.Join(tempDir, "quadlets")
|
||||
err := os.Mkdir(quadletDir, 0o700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create multipart reader: %w", err)
|
||||
}
|
||||
|
||||
var filePaths []string
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read multipart: %w", err)
|
||||
}
|
||||
defer part.Close()
|
||||
|
||||
filename := part.FileName()
|
||||
if filename == "" {
|
||||
// Skip parts without filenames
|
||||
continue
|
||||
}
|
||||
|
||||
// Create file in temp directory
|
||||
filePath := filepath.Join(quadletDir, filename)
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create file %s: %w", filename, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write file %s: %w", filename, err)
|
||||
}
|
||||
|
||||
filePaths = append(filePaths, filePath)
|
||||
}
|
||||
|
||||
return filePaths, nil
|
||||
}
|
||||
|
||||
func InstallQuadlets(w http.ResponseWriter, r *http.Request) {
|
||||
// Create temporary directory for processing
|
||||
contextDirectory, err := os.MkdirTemp("", "libpod_quadlet")
|
||||
if err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(contextDirectory); err != nil {
|
||||
logrus.Warn(fmt.Errorf("failed to remove libpod_quadlet tmp directory %q: %w", contextDirectory, err))
|
||||
}
|
||||
}()
|
||||
|
||||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
||||
|
||||
// Parse query parameters
|
||||
query := struct {
|
||||
Replace bool `schema:"replace"`
|
||||
ReloadSystemd bool `schema:"reload-systemd"`
|
||||
}{
|
||||
Replace: false,
|
||||
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
|
||||
}
|
||||
|
||||
multipart, err := utils.ValidateContentType(r)
|
||||
if err != nil {
|
||||
utils.Error(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
var filePaths []string
|
||||
if multipart {
|
||||
logrus.Debug("Processing multipart form data")
|
||||
filePaths, err = processMultipartQuadlets(contextDirectory, r)
|
||||
if err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logrus.Debug("Processing tar archive")
|
||||
filePaths, err = extractQuadletFiles(contextDirectory, r.Body)
|
||||
if err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(filePaths) == 0 {
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("no files found in request"))
|
||||
return
|
||||
}
|
||||
|
||||
countQuadletFiles := 0
|
||||
for _, filePath := range filePaths {
|
||||
isQuadletFile := quadlet.IsExtSupported(filePath)
|
||||
if isQuadletFile {
|
||||
countQuadletFiles++
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case countQuadletFiles > 1:
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("only a single quadlet file is allowed per request"))
|
||||
return
|
||||
case countQuadletFiles == 0:
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("no quadlet files found in request"))
|
||||
return
|
||||
}
|
||||
|
||||
containerEngine := abi.ContainerEngine{Libpod: runtime}
|
||||
installOptions := entities.QuadletInstallOptions{
|
||||
Replace: query.Replace,
|
||||
ReloadSystemd: query.ReloadSystemd,
|
||||
}
|
||||
|
||||
installReport, err := containerEngine.QuadletInstall(r.Context(), filePaths, installOptions)
|
||||
if err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
if len(installReport.QuadletErrors) > 0 {
|
||||
var errs []error
|
||||
for path, err := range installReport.QuadletErrors {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", path, err))
|
||||
}
|
||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("errors occurred installing some Quadlets: %w", errors.Join(errs...)))
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,13 @@ type podNotFound struct {
|
||||
Body errorhandling.ErrorModel
|
||||
}
|
||||
|
||||
// No such quadlet
|
||||
// swagger:response
|
||||
type quadletNotFound struct {
|
||||
// in:body
|
||||
Body errorhandling.ErrorModel
|
||||
}
|
||||
|
||||
// No such manifest
|
||||
// swagger:response
|
||||
type manifestNotFound struct {
|
||||
|
||||
@@ -527,3 +527,17 @@ type quadletListResponse struct {
|
||||
// in:body
|
||||
Body []entities.ListQuadlet
|
||||
}
|
||||
|
||||
// Quadlet file
|
||||
// swagger:response
|
||||
type quadletFileResponse struct {
|
||||
// in:body
|
||||
Body string
|
||||
}
|
||||
|
||||
// Quadlet remove
|
||||
// swagger:response
|
||||
type quadletRemoveResponse struct {
|
||||
// in:body
|
||||
Body entities.QuadletRemoveReport
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package utils
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -36,6 +37,34 @@ func IsLibpodLocalRequest(r *http.Request) bool {
|
||||
return apiutil.IsLibpodLocalRequest(r)
|
||||
}
|
||||
|
||||
// ValidateContentType validates the Content-Type header and determines if multipart processing is needed.
|
||||
func ValidateContentType(r *http.Request) (bool, error) {
|
||||
multipart := false
|
||||
if hdr, found := r.Header["Content-Type"]; found && len(hdr) > 0 {
|
||||
contentType, _, err := mime.ParseMediaType(hdr[0])
|
||||
if err != nil {
|
||||
return false, GetBadRequestError("Content-Type", hdr[0], err)
|
||||
}
|
||||
|
||||
switch contentType {
|
||||
case "application/tar":
|
||||
logrus.Infof("tar file content type is %s, should use \"application/x-tar\" content type", contentType)
|
||||
case "application/x-tar":
|
||||
break
|
||||
case "multipart/form-data":
|
||||
logrus.Infof("Received %s", hdr[0])
|
||||
multipart = true
|
||||
default:
|
||||
if IsLibpodRequest(r) && !IsLibpodLocalRequest(r) {
|
||||
return false, GetBadRequestError("Content-Type", hdr[0],
|
||||
fmt.Errorf("Content-Type: %s is not supported. Should be \"application/x-tar\"", hdr[0]))
|
||||
}
|
||||
logrus.Infof("tar file content type is %s, should use \"application/x-tar\" content type", contentType)
|
||||
}
|
||||
}
|
||||
return multipart, nil
|
||||
}
|
||||
|
||||
// SupportedVersion validates that the version provided by client is included in the given condition
|
||||
// https://github.com/blang/semver#ranges provides the details for writing conditions
|
||||
// If a version is not given in URL path, ErrVersionNotGiven is returned
|
||||
|
||||
@@ -32,5 +32,189 @@ func (s *APIServer) registerQuadletHandlers(r *mux.Router) error {
|
||||
// 500:
|
||||
// $ref: "#/responses/internalError"
|
||||
r.HandleFunc(VersionedPath("/libpod/quadlets/json"), s.APIHandler(libpod.ListQuadlets)).Methods(http.MethodGet)
|
||||
// swagger:operation GET /libpod/quadlets/{name}/file libpod QuadletFileLibpod
|
||||
// ---
|
||||
// tags:
|
||||
// - quadlets
|
||||
// summary: Get quadlet file
|
||||
// description: Get the contents of a Quadlet, displaying the file including all comments
|
||||
// produces:
|
||||
// - text/plain
|
||||
// parameters:
|
||||
// - in: path
|
||||
// name: name
|
||||
// type: string
|
||||
// required: true
|
||||
// description: the name of the quadlet with extension (e.g., "myapp.container")
|
||||
// responses:
|
||||
// 200:
|
||||
// $ref: "#/responses/quadletFileResponse"
|
||||
// 404:
|
||||
// $ref: "#/responses/quadletNotFound"
|
||||
// 500:
|
||||
// $ref: "#/responses/internalError"
|
||||
r.HandleFunc(VersionedPath("/libpod/quadlets/{name}/file"), s.APIHandler(libpod.GetQuadletPrint)).Methods(http.MethodGet)
|
||||
// swagger:operation GET /libpod/quadlets/{name}/exists libpod QuadletExistsLibpod
|
||||
// ---
|
||||
// tags:
|
||||
// - quadlets
|
||||
// summary: Check if quadlet exists
|
||||
// description: Check if a quadlet exists by name
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - in: path
|
||||
// name: name
|
||||
// type: string
|
||||
// required: true
|
||||
// description: the name of the quadlet with extension (e.g., "myapp.container")
|
||||
// responses:
|
||||
// 204:
|
||||
// description: quadlet exists
|
||||
// 404:
|
||||
// $ref: "#/responses/quadletNotFound"
|
||||
// 500:
|
||||
// $ref: "#/responses/internalError"
|
||||
r.HandleFunc(VersionedPath("/libpod/quadlets/{name}/exists"), s.APIHandler(libpod.QuadletExists)).Methods(http.MethodGet)
|
||||
// swagger:operation POST /libpod/quadlets libpod QuadletInstallLibpod
|
||||
// ---
|
||||
// tags:
|
||||
// - quadlets
|
||||
// summary: Install quadlet files
|
||||
// description: |
|
||||
// Install one or more files for a quadlet application. Each request should contain a single quadlet file
|
||||
// and optionally more files such as containerfile, kube yaml or configuration files. Supports both tar
|
||||
// archives and multipart form data uploads.
|
||||
// consumes:
|
||||
// - application/x-tar
|
||||
// - multipart/form-data
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - in: query
|
||||
// name: replace
|
||||
// type: boolean
|
||||
// default: false
|
||||
// description: Replace the installation files even if the files already exists
|
||||
// - in: query
|
||||
// name: reload-systemd
|
||||
// type: boolean
|
||||
// default: true
|
||||
// description: Reload systemd after installing quadlets
|
||||
// - in: body
|
||||
// name: request
|
||||
// description: |
|
||||
// Quadlet files to install. Can be provided as:
|
||||
// - application/x-tar: A tar archive containing one quadlet file and optionally additional files
|
||||
// - multipart/form-data: One quadlet file as form data and optionally additional files
|
||||
// schema:
|
||||
// type: string
|
||||
// format: binary
|
||||
// responses:
|
||||
// 200:
|
||||
// description: Quadlet installation report
|
||||
// schema:
|
||||
// type: object
|
||||
// properties:
|
||||
// InstalledQuadlets:
|
||||
// type: object
|
||||
// additionalProperties:
|
||||
// type: string
|
||||
// description: Map of source path to installed path for successfully installed quadlets
|
||||
// QuadletErrors:
|
||||
// type: object
|
||||
// additionalProperties:
|
||||
// type: string
|
||||
// description: Map of source path to error message for failed installations
|
||||
// 400:
|
||||
// $ref: "#/responses/badParamError"
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ type ContainerEngine interface { //nolint:interfacebloat
|
||||
PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error)
|
||||
PodTop(ctx context.Context, options PodTopOptions) (*StringSliceReport, error)
|
||||
PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error)
|
||||
QuadletExists(ctx context.Context, name string) (*BoolReport, error)
|
||||
QuadletInstall(ctx context.Context, pathsOrURLs []string, options QuadletInstallOptions) (*QuadletInstallReport, error)
|
||||
QuadletList(ctx context.Context, options QuadletListOptions) ([]*ListQuadlet, error)
|
||||
QuadletPrint(ctx context.Context, quadlet string) (string, error)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/podman/v5/libpod/define"
|
||||
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||
"github.com/containers/podman/v5/pkg/rootless"
|
||||
"github.com/containers/podman/v5/pkg/systemd"
|
||||
@@ -531,6 +532,15 @@ func (ic *ContainerEngine) QuadletList(ctx context.Context, options entities.Qua
|
||||
return finalReports, nil
|
||||
}
|
||||
|
||||
// QuadletExists checks whether a quadlet with the given name exists.
|
||||
func (ic *ContainerEngine) QuadletExists(_ context.Context, name string) (*entities.BoolReport, error) {
|
||||
_, err := getQuadletPathByName(name)
|
||||
if err != nil && !errors.Is(err, define.ErrNoSuchQuadlet) {
|
||||
return nil, err
|
||||
}
|
||||
return &entities.BoolReport{Value: err == nil}, nil
|
||||
}
|
||||
|
||||
// Retrieve path to a Quadlet file given full name including extension
|
||||
func getQuadletPathByName(name string) (string, error) {
|
||||
// Check if we were given a valid extension
|
||||
@@ -549,7 +559,7 @@ func getQuadletPathByName(name string) (string, error) {
|
||||
}
|
||||
return testPath, nil
|
||||
}
|
||||
return "", fmt.Errorf("could not locate quadlet %q in any supported quadlet directory", name)
|
||||
return "", fmt.Errorf("could not locate quadlet %q in any supported quadlet directory: %w", name, define.ErrNoSuchQuadlet)
|
||||
}
|
||||
|
||||
func (ic *ContainerEngine) QuadletPrint(_ context.Context, quadlet string) (string, error) {
|
||||
@@ -709,7 +719,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
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import (
|
||||
|
||||
var errNotImplemented = errors.New("not implemented for the remote Podman client")
|
||||
|
||||
func (ic *ContainerEngine) QuadletExists(_ context.Context, _ string) (*entities.BoolReport, error) {
|
||||
return nil, errNotImplemented
|
||||
}
|
||||
|
||||
func (ic *ContainerEngine) QuadletInstall(_ context.Context, _ []string, _ entities.QuadletInstallOptions) (*entities.QuadletInstallReport, error) {
|
||||
return nil, errNotImplemented
|
||||
}
|
||||
|
||||
@@ -6,10 +6,383 @@
|
||||
# NOTE: Once podman-remote quadlet support is added we can enable the podman quadlet tests in
|
||||
# test/system/253-podman-quadlet.bats which should cover it in more detail then.
|
||||
|
||||
## list volume
|
||||
|
||||
function is_rootless() {
|
||||
[ "$(id -u)" -ne 0 ]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
quadlet_install_dir=$(get_quadlet_install_dir)
|
||||
|
||||
## Test list endpoint
|
||||
t GET libpod/quadlets/json 200
|
||||
|
||||
# Example with filter applied (uncomment once needed)
|
||||
# t GET libpod/quadlets/json?filters='{"name":["name.*"]}' 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/file 404
|
||||
|
||||
# Test 404 for non-existent quadlet exists endpoint
|
||||
t GET libpod/quadlets/nonexistent.container/exists 404
|
||||
|
||||
# Test 500 for invalid quadlet extension (not a user-facing "not found" but an input error)
|
||||
t GET libpod/quadlets/invalid.badext/exists 500
|
||||
|
||||
# Install a quadlet with a unique name
|
||||
quadlet_name=quadlet-test-$(cat /proc/sys/kernel/random/uuid)
|
||||
|
||||
quadlet_container_name="$quadlet_name.container"
|
||||
quadlet_build_name="$quadlet_name.build"
|
||||
|
||||
TMPD=$(mktemp -d podman-apiv2-test.quadlet.XXXXXXXX)
|
||||
|
||||
quadlet_container_file_content=$(cat << EOF
|
||||
[Container]
|
||||
Image=$IMAGE
|
||||
EOF
|
||||
)
|
||||
|
||||
quadlet_build_file_content=$(cat << EOF
|
||||
[Build]
|
||||
ImageTag=localhost/$quadlet_name
|
||||
File=.
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$quadlet_container_file_content" > $TMPD/$quadlet_container_name
|
||||
echo "$quadlet_build_file_content" > $TMPD/$quadlet_build_name
|
||||
|
||||
# this should ensure the .config/containers/systemd directory is created
|
||||
podman quadlet install $TMPD/$quadlet_container_name
|
||||
podman quadlet install $TMPD/$quadlet_build_name
|
||||
|
||||
filter_param=$(printf '{"name":["%s"]}' "$quadlet_name")
|
||||
t GET "libpod/quadlets/json?filters=$filter_param" 200 \
|
||||
length=2 \
|
||||
.[0].Name="$quadlet_build_name" \
|
||||
.[1].Name="$quadlet_container_name"
|
||||
|
||||
filter_param=$(printf '{"name":["%s"]}' "$quadlet_container_name")
|
||||
t GET "libpod/quadlets/json?filters=$filter_param" 200 \
|
||||
length=1 \
|
||||
.[0].Name="$quadlet_container_name"
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_name/file" 404
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_container_name/file" 200
|
||||
is "$output" "$quadlet_container_file_content"
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_build_name/file" 200
|
||||
is "$output" "$quadlet_build_file_content"
|
||||
|
||||
# Test exists endpoint returns 204 for existing quadlet
|
||||
t GET "libpod/quadlets/$quadlet_container_name/exists" 204
|
||||
|
||||
# Test exists endpoint returns 500 for quadlet without extension
|
||||
t GET "libpod/quadlets/$quadlet_name/exists" 500
|
||||
|
||||
podman quadlet rm $quadlet_container_name
|
||||
podman quadlet rm $quadlet_build_name
|
||||
rm -rf $TMPD
|
||||
|
||||
|
||||
TMPD=$(mktemp -d podman-apiv2-test.quadlets.XXXXXXXX)
|
||||
|
||||
# Scenario: try to send nothing
|
||||
t POST "libpod/quadlets" 400 \
|
||||
.cause~.*'Content-Type: application/json is not supported. Should be "application/x-tar"'
|
||||
|
||||
# Scenario: send an empty tar archive will fail with no files found in request
|
||||
tar -C "$TMPD" -cvf "$TMPD/empty.tar" -T /dev/null &> /dev/null
|
||||
t POST "libpod/quadlets" "$TMPD/empty.tar" 400 \
|
||||
.cause="no files found in request"
|
||||
|
||||
# Scenario: send a plaintext file will fail with no quadlet files found in request
|
||||
echo "test" > "$TMPD/test.txt"
|
||||
t POST "libpod/quadlets" --form="test.txt=@$TMPD/test.txt" 400 \
|
||||
.cause="no quadlet files found in request"
|
||||
|
||||
# Scenario: send an invalid quadlet type in a tar archive will fail with no quadlet files found in request
|
||||
echo "test" > "$TMPD/test.txt"
|
||||
tar -C "$TMPD" -cvf "$TMPD/test.tar" "test.txt" &> /dev/null
|
||||
t POST "libpod/quadlets" "$TMPD/test.tar" 400 \
|
||||
.cause="no quadlet files found in request"
|
||||
|
||||
# Scenario 1: install a single quadlet
|
||||
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" > "$TMPD/$quadlet_1"
|
||||
tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_1.tar" "$quadlet_1" &> /dev/null
|
||||
|
||||
t POST "libpod/quadlets" "$TMPD/$quadlet_1.tar" 200 \
|
||||
'.InstalledQuadlets|length=1' \
|
||||
'.QuadletErrors|length=0'
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_1/file" 200
|
||||
is "$output" "$quadlet_1_content" "quadlet-1 should be installed"
|
||||
|
||||
# Scenario: install a quadlet that already exists, verify it won't be overwritten
|
||||
# then use replace=true to overwrite it and verify
|
||||
quadlet_2=$quadlet_1
|
||||
quadlet_2_content=$(cat << EOF
|
||||
[Container]
|
||||
ContainerName=quadlet-2
|
||||
Image=quay.io/podman/hello
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$quadlet_2_content" > "$TMPD/$quadlet_2"
|
||||
tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_2.tar" "$quadlet_2" &> /dev/null
|
||||
|
||||
t POST "libpod/quadlets" "$TMPD/$quadlet_2.tar" 400 \
|
||||
.cause~.*"a Quadlet with name $quadlet_2 already exists, refusing to overwrite"
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_1/file" 200
|
||||
is "$output" "$quadlet_1_content" "quadlet-1 should not be overwritten"
|
||||
|
||||
#replace
|
||||
t POST "libpod/quadlets?replace=true" "$TMPD/$quadlet_2.tar" 200 \
|
||||
'.InstalledQuadlets|length=1' \
|
||||
'.QuadletErrors|length=0'
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_2/file" 200
|
||||
is "$output" "$quadlet_2_content" "quadlet-1 should be overwritten by quadlet-2"
|
||||
|
||||
# Scenario: install multiple quadlets at once in a single tar will fail
|
||||
quadlet_3=quadlet-test-3-$(cat /proc/sys/kernel/random/uuid).container
|
||||
quadlet_4=quadlet-test-4-$(cat /proc/sys/kernel/random/uuid).container
|
||||
|
||||
quadlet_3_content=$(cat << EOF
|
||||
[Container]
|
||||
ContainerName=quadlet-3
|
||||
Image=quay.io/podman/hello
|
||||
EOF
|
||||
)
|
||||
|
||||
quadlet_4_content=$(cat << EOF
|
||||
[Container]
|
||||
ContainerName=quadlet-4
|
||||
Image=quay.io/podman/hello
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$quadlet_3_content" > "$TMPD/$quadlet_3"
|
||||
echo "$quadlet_4_content" > "$TMPD/$quadlet_4"
|
||||
tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_3_4.tar" "$quadlet_3" "$quadlet_4" &> /dev/null
|
||||
|
||||
t POST "libpod/quadlets" "$TMPD/$quadlet_3_4.tar" 400 \
|
||||
.cause="only a single quadlet file is allowed per request"
|
||||
|
||||
# Scenario: install tar that contains one quadlet file and a non-quadlet file will succeed
|
||||
# then update the quadlet file, and the non-quadlet file, and verify the update is successful
|
||||
quadlet_5=quadlet-test-5-$(cat /proc/sys/kernel/random/uuid).container
|
||||
containerfile_1=quadlet-test-containerfile-1-$(cat /proc/sys/kernel/random/uuid).Containerfile
|
||||
|
||||
containerfile_1_content=$(cat << EOF
|
||||
FROM quay.io/podman/hello
|
||||
CMD ["echo", "hello"]
|
||||
EOF
|
||||
)
|
||||
|
||||
quadlet_5_content=$(cat << EOF
|
||||
[Container]
|
||||
ContainerName=quadlet-5
|
||||
Image=quay.io/podman/hello
|
||||
EOF
|
||||
)
|
||||
|
||||
quadlet_5_updated_content=$(cat << EOF
|
||||
[Container]
|
||||
ContainerName=quadlet-5-updated
|
||||
Image=quay.io/podman/hello
|
||||
EOF
|
||||
)
|
||||
|
||||
containerfile_1_updated_content=$(cat << EOF
|
||||
FROM quay.io/podman/hello
|
||||
CMD ["echo", "Updated"]
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$quadlet_5_content" > "$TMPD/$quadlet_5"
|
||||
echo "$containerfile_1_content" > "$TMPD/$containerfile_1"
|
||||
tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_5$containerfile_1.tar" "$quadlet_5" "$containerfile_1" &> /dev/null
|
||||
|
||||
t POST "libpod/quadlets" "$TMPD/$quadlet_5$containerfile_1.tar" 200 \
|
||||
'.InstalledQuadlets|length=2' \
|
||||
'.QuadletErrors|length=0'
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_5/file" 200
|
||||
is "$output" "$quadlet_5_content" "quadlet-5 should be installed"
|
||||
is "$(cat "$quadlet_install_dir/$containerfile_1")" "$containerfile_1_content" "containerfile_1 should be installed"
|
||||
|
||||
echo "$quadlet_5_updated_content" > "$TMPD/$quadlet_5"
|
||||
echo "$containerfile_1_updated_content" > "$TMPD/$containerfile_1"
|
||||
tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_5$containerfile_1.tar" "$quadlet_5" "$containerfile_1" &> /dev/null
|
||||
|
||||
# update with no replace and check nothing changed
|
||||
t POST "libpod/quadlets" "$TMPD/$quadlet_5$containerfile_1.tar" 400
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_5/file" 200
|
||||
is "$output" "$quadlet_5_content" "quadlet-5 should be installed"
|
||||
is "$(cat "$quadlet_install_dir/$containerfile_1")" "$containerfile_1_content" "containerfile_1 should be installed"
|
||||
|
||||
# replace
|
||||
t POST "libpod/quadlets?replace=true" "$TMPD/$quadlet_5$containerfile_1.tar" 200 \
|
||||
'.InstalledQuadlets|length=2' \
|
||||
'.QuadletErrors|length=0'
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_5/file" 200
|
||||
is "$output" "$quadlet_5_updated_content" "quadlet-5 should be updated"
|
||||
is "$(cat "$quadlet_install_dir/$containerfile_1")" "$containerfile_1_updated_content" "containerfile_1 should be installed"
|
||||
|
||||
# Scenario: test a multipart call, then update without replace, and then replace
|
||||
quadlet_6=quadlet-test-6-$(cat /proc/sys/kernel/random/uuid).container
|
||||
containerfile_2=quadlet-test-containerfile-2-$(cat /proc/sys/kernel/random/uuid).Containerfile
|
||||
|
||||
quadlet_6_content=$(cat << EOF
|
||||
[Container]
|
||||
ContainerName=quadlet-6
|
||||
Image=quay.io/podman/hello
|
||||
EOF
|
||||
)
|
||||
|
||||
containerfile_2_content=$(cat << EOF
|
||||
FROM quay.io/podman/hello
|
||||
CMD ["echo", "hello"]
|
||||
EOF
|
||||
)
|
||||
|
||||
quadlet_6_updated_content=$(cat << EOF
|
||||
[Container]
|
||||
ContainerName=quadlet-6-updated
|
||||
Image=quay.io/podman/hello
|
||||
EOF
|
||||
)
|
||||
|
||||
containerfile_2_updated_content=$(cat << EOF
|
||||
FROM quay.io/podman/hello
|
||||
CMD ["echo", "Updated"]
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$quadlet_6_content" > "$TMPD/$quadlet_6"
|
||||
echo "$containerfile_2_content" > "$TMPD/$containerfile_2"
|
||||
|
||||
t POST "libpod/quadlets" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 200
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_6/file" 200
|
||||
is "$output" "$quadlet_6_content" "quadlet-6 should be installed"
|
||||
is "$(cat "$quadlet_install_dir/$containerfile_2")" "$containerfile_2_content" "containerfile_2 should be installed"
|
||||
|
||||
# update with no replace and check nothing changed
|
||||
echo "$quadlet_6_updated_content" > "$TMPD/$quadlet_6"
|
||||
echo "$containerfile_2_updated_content" > "$TMPD/$containerfile_2"
|
||||
t POST "libpod/quadlets" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 400
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_6/file" 200
|
||||
is "$output" "$quadlet_6_content" "quadlet-6 should not be updated"
|
||||
is "$(cat "$quadlet_install_dir/$containerfile_2")" "$containerfile_2_content" "containerfile_2 should not be updated"
|
||||
|
||||
# replace
|
||||
t POST "libpod/quadlets?replace=true" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 200
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_6/file" 200
|
||||
is "$output" "$quadlet_6_updated_content" "quadlet-6 should be updated"
|
||||
is "$(cat "$quadlet_install_dir/$containerfile_2")" "$containerfile_2_updated_content" "containerfile_2 should be updated"
|
||||
|
||||
# clean up
|
||||
|
||||
podman quadlet rm "$quadlet_1" \
|
||||
"$quadlet_5" \
|
||||
"$quadlet_6"
|
||||
|
||||
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 GET "libpod/quadlets/$quadlet_1/exists" 204
|
||||
|
||||
t DELETE "libpod/quadlets/$quadlet_1" 200 \
|
||||
'.Removed|length=1' \
|
||||
'.QuadletErrors|length=0'
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_1/file" 404
|
||||
|
||||
# Verify exists returns 404 after deletion
|
||||
t GET "libpod/quadlets/$quadlet_1/exists" 404
|
||||
|
||||
t DELETE "libpod/quadlets/nonexistent.container" 404 \
|
||||
'.Removed|length=0' \
|
||||
.cause='no such 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~.*'quadlet is running'
|
||||
|
||||
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