Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions pkg/api/handlers/libpod/quadlets.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,23 +135,33 @@ func processMultipartQuadlets(tempDir string, r *http.Request) ([]string, error)
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
part.Close()
continue
}

// Create file in temp directory
filename = filepath.Clean(filename)
if filepath.IsAbs(filename) || strings.HasPrefix(filename, "..") {
part.Close()
return nil, fmt.Errorf("invalid filename %q: path traversal not allowed", filename)
}

filePath := filepath.Join(quadletDir, filename)
if err := os.MkdirAll(filepath.Dir(filePath), 0o700); err != nil {
part.Close()
return nil, fmt.Errorf("failed to create directory for %s: %w", filename, err)
}
file, err := os.Create(filePath)
if err != nil {
part.Close()
return nil, fmt.Errorf("failed to create file %s: %w", filename, err)
}
defer file.Close()

_, err = io.Copy(file, part)
file.Close()
part.Close()
if err != nil {
return nil, fmt.Errorf("failed to write file %s: %w", filename, err)
}
Expand Down Expand Up @@ -254,14 +264,6 @@ func InstallQuadlets(w http.ResponseWriter, r *http.Request) {
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)
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/api/server/register_quadlets.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ func (s *APIServer) registerQuadletHandlers(r *mux.Router) error {
// type: boolean
// default: true
// description: Reload systemd after removing quadlets
// - in: query
// name: recursive
// type: boolean
// default: false
// description: Remove all quadlets belonging to the specified application and its directory
// responses:
// 200:
// $ref: "#/responses/quadletRemoveResponse"
Expand Down Expand Up @@ -214,6 +219,11 @@ func (s *APIServer) registerQuadletHandlers(r *mux.Router) error {
// type: boolean
// default: true
// description: Reload systemd after removing the quadlet
// - in: query
// name: recursive
// type: boolean
// default: false
// description: Remove all quadlets belonging to the specified application and its directory
// responses:
// 200:
// $ref: "#/responses/quadletRemoveResponse"
Expand Down
158 changes: 158 additions & 0 deletions pkg/bindings/quadlets/quadlets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package quadlets

import (
"context"
"io"
"mime/multipart"
"net/http"
"os"

"go.podman.io/podman/v6/pkg/bindings"
"go.podman.io/podman/v6/pkg/domain/entities"
)

// List returns a list of quadlets on the server.
func List(ctx context.Context, options *ListOptions) ([]*entities.ListQuadlet, error) {
if options == nil {
options = new(ListOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}

var quadlets []*entities.ListQuadlet
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/quadlets/json", params, nil)
if err != nil {
return nil, err
}
defer response.Body.Close()

return quadlets, response.Process(&quadlets)
}

// Exists checks whether a quadlet with the given name exists on the server.
func Exists(ctx context.Context, name string, _ *ExistsOptions) (bool, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return false, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/quadlets/%s/exists", nil, nil, name)
if err != nil {
return false, err
}
defer response.Body.Close()

return response.IsSuccess(), nil
}

// Print returns the contents of a quadlet file from the server.
func Print(ctx context.Context, name string, _ *PrintOptions) (string, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return "", err
}
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/quadlets/%s/file", nil, nil, name)
if err != nil {
return "", err
}
defer response.Body.Close()

if !response.IsSuccess() {
return "", response.Process(nil)
}

body, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
return string(body), nil
}

// Install sends local quadlet files to the server for installation via multipart upload.
// fileNames provides the upload filename for each filePath (preserving relative paths
// for directory structures). It must be non-nil and the same length as filePaths.
func Install(ctx context.Context, filePaths []string, fileNames []string, options *InstallOptions) (*entities.QuadletInstallReport, error) {
if options == nil {
options = new(InstallOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}

pr, pw := io.Pipe()
writer := multipart.NewWriter(pw)

go func() {
defer pw.Close()
defer writer.Close()

for i, filePath := range filePaths {
filename := fileNames[i]
part, err := writer.CreateFormFile(filename, filename)
if err != nil {
pw.CloseWithError(err)
return
}
file, err := os.Open(filePath)
if err != nil {
pw.CloseWithError(err)
return
}
_, err = io.Copy(part, file)
file.Close()
if err != nil {
pw.CloseWithError(err)
return
}
}
}()

header := make(http.Header)
header.Set("Content-Type", writer.FormDataContentType())

var report entities.QuadletInstallReport
response, err := conn.DoRequest(ctx, pr, http.MethodPost, "/quadlets", params, header)
if err != nil {
return nil, err
}
defer response.Body.Close()

return &report, response.Process(&report)
}

// Remove removes one or more quadlets from the server (batch operation).
func Remove(ctx context.Context, quadletNames []string, options *RemoveOptions) (*entities.QuadletRemoveReport, error) {
if options == nil {
options = new(RemoveOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}
for _, name := range quadletNames {
params.Add("quadlets", name)
}

var report entities.QuadletRemoveReport
response, err := conn.DoRequest(ctx, nil, http.MethodDelete, "/quadlets", params, nil)
if err != nil {
return nil, err
}
defer response.Body.Close()

return &report, response.Process(&report)
Comment thread
inknos marked this conversation as resolved.
}
38 changes: 38 additions & 0 deletions pkg/bindings/quadlets/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package quadlets

// ListOptions are optional options for listing quadlets
//
//go:generate go run ../generator/generator.go ListOptions
type ListOptions struct {
Filters map[string][]string
}

// ExistsOptions are optional options for checking if a quadlet exists
//
//go:generate go run ../generator/generator.go ExistsOptions
type ExistsOptions struct{}

// PrintOptions are optional options for printing quadlet file contents
//
//go:generate go run ../generator/generator.go PrintOptions
type PrintOptions struct{}

// InstallOptions are optional options for installing quadlets
//
//go:generate go run ../generator/generator.go InstallOptions
type InstallOptions struct {
Replace *bool
ReloadSystemd *bool
Application *string
}

// RemoveOptions are optional options for removing quadlets
//
//go:generate go run ../generator/generator.go RemoveOptions
type RemoveOptions struct {
Force *bool
All *bool
Ignore *bool
ReloadSystemd *bool
Recursive *bool
}
18 changes: 18 additions & 0 deletions pkg/bindings/quadlets/types_exists_options.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions pkg/bindings/quadlets/types_install_options.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions pkg/bindings/quadlets/types_list_options.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading