From ee0efb9fc64a40132f61651ae1276a715fd52c61 Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Wed, 12 Nov 2025 16:15:07 +0100 Subject: [PATCH] Add GET /quadlets/{name}/file Fixes: https://issues.redhat.com/browse/RUN-3716 Signed-off-by: Nicola Sella --- pkg/api/handlers/libpod/quadlets.go | 21 ++++++++++ pkg/api/handlers/swagger/errors.go | 7 ++++ pkg/api/handlers/swagger/responses.go | 7 ++++ pkg/api/server/register_quadlets.go | 22 +++++++++++ test/apiv2/36-quadlets.at | 56 +++++++++++++++++++++++++-- 5 files changed, 110 insertions(+), 3 deletions(-) diff --git a/pkg/api/handlers/libpod/quadlets.go b/pkg/api/handlers/libpod/quadlets.go index 27457344ddc..cd28f7e0a04 100644 --- a/pkg/api/handlers/libpod/quadlets.go +++ b/pkg/api/handlers/libpod/quadlets.go @@ -12,6 +12,7 @@ import ( "github.com/containers/podman/v6/pkg/domain/entities" "github.com/containers/podman/v6/pkg/domain/infra/abi" "github.com/containers/podman/v6/pkg/util" + "github.com/sirupsen/logrus" ) func ListQuadlets(w http.ResponseWriter, r *http.Request) { @@ -35,3 +36,23 @@ 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 + } +} diff --git a/pkg/api/handlers/swagger/errors.go b/pkg/api/handlers/swagger/errors.go index b3f624fc627..8e7d443d04c 100644 --- a/pkg/api/handlers/swagger/errors.go +++ b/pkg/api/handlers/swagger/errors.go @@ -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 { diff --git a/pkg/api/handlers/swagger/responses.go b/pkg/api/handlers/swagger/responses.go index 11b3eb0cffe..2d60503bc62 100644 --- a/pkg/api/handlers/swagger/responses.go +++ b/pkg/api/handlers/swagger/responses.go @@ -527,3 +527,10 @@ type quadletListResponse struct { // in:body Body []entities.ListQuadlet } + +// Quadlet file +// swagger:response +type quadletFileResponse struct { + // in:body + Body string +} diff --git a/pkg/api/server/register_quadlets.go b/pkg/api/server/register_quadlets.go index b0a55888384..4c35d3ab1d1 100644 --- a/pkg/api/server/register_quadlets.go +++ b/pkg/api/server/register_quadlets.go @@ -32,5 +32,27 @@ 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) return nil } diff --git a/test/apiv2/36-quadlets.at b/test/apiv2/36-quadlets.at index 0355975ea7e..2410bf566eb 100644 --- a/test/apiv2/36-quadlets.at +++ b/test/apiv2/36-quadlets.at @@ -6,10 +6,60 @@ # 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 +## 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 404 for non-existent quadlet +t GET libpod/quadlets/nonexistent.container 404 + +# 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" + +TMPDIR=$(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 +EOF +) + +echo "$quadlet_container_file_content" > $TMPDIR/$quadlet_container_name +echo "$quadlet_build_file_content" > $TMPDIR/$quadlet_build_name + +# this should ensure the .config/containers/systemd directory is created +podman quadlet install $TMPDIR/$quadlet_container_name +podman quadlet install $TMPDIR/$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" + +podman quadlet rm $quadlet_container_name +podman quadlet rm $quadlet_build_name +rm -rf $TMPDIR # vim: filetype=sh