mirror of
https://github.com/containers/podman.git
synced 2026-02-05 06:45:31 +01:00
Add POST /libpod/quadlets
Fixes: https://issues.redhat.com/browse/RUN-3743 Signed-off-by: Nicola Sella <nsella@redhat.com>
This commit is contained in:
@@ -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,23 @@
|
||||
package libpod
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.podman.io/storage/pkg/archive"
|
||||
|
||||
"github.com/containers/podman/v6/libpod"
|
||||
"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"
|
||||
"github.com/containers/podman/v6/pkg/domain/infra/abi"
|
||||
"github.com/containers/podman/v6/pkg/systemd/quadlet"
|
||||
"github.com/containers/podman/v6/pkg/util"
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -56,3 +64,177 @@ func GetQuadletPrint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,5 +54,60 @@ func (s *APIServer) registerQuadletHandlers(r *mux.Router) error {
|
||||
// 500:
|
||||
// $ref: "#/responses/internalError"
|
||||
r.HandleFunc(VersionedPath("/libpod/quadlets/{name}/file"), s.APIHandler(libpod.GetQuadletPrint)).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)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user