diff --git a/libpod/define/errors.go b/libpod/define/errors.go index 09127a9b64..47b50a6a64 100644 --- a/libpod/define/errors.go +++ b/libpod/define/errors.go @@ -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") diff --git a/pkg/api/handlers/libpod/quadlets.go b/pkg/api/handlers/libpod/quadlets.go index 0d1e88f56c..b5c023f48b 100644 --- a/pkg/api/handlers/libpod/quadlets.go +++ b/pkg/api/handlers/libpod/quadlets.go @@ -67,6 +67,25 @@ func GetQuadletPrint(w http.ResponseWriter, r *http.Request) { } } +// 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") diff --git a/pkg/api/server/register_quadlets.go b/pkg/api/server/register_quadlets.go index 6e286b14c3..2172dad23d 100644 --- a/pkg/api/server/register_quadlets.go +++ b/pkg/api/server/register_quadlets.go @@ -54,6 +54,28 @@ 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 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: diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 30d645fa11..6b9c53e20f 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -94,6 +94,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) diff --git a/pkg/domain/infra/abi/quadlet.go b/pkg/domain/infra/abi/quadlet.go index 84dd192c81..939715bd3d 100644 --- a/pkg/domain/infra/abi/quadlet.go +++ b/pkg/domain/infra/abi/quadlet.go @@ -719,6 +719,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 @@ -737,7 +746,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) { diff --git a/pkg/domain/infra/tunnel/quadlet.go b/pkg/domain/infra/tunnel/quadlet.go index 752d0178fc..7c0a29ac04 100644 --- a/pkg/domain/infra/tunnel/quadlet.go +++ b/pkg/domain/infra/tunnel/quadlet.go @@ -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 } diff --git a/test/apiv2/36-quadlets.at b/test/apiv2/36-quadlets.at index 7020f32704..2b616c86f4 100644 --- a/test/apiv2/36-quadlets.at +++ b/test/apiv2/36-quadlets.at @@ -33,6 +33,12 @@ 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) @@ -80,6 +86,12 @@ 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 @@ -319,16 +331,20 @@ 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 -t DELETE "libpod/quadlets/nonexistent.container" 200 \ +# Verify exists returns 404 after deletion +t GET "libpod/quadlets/$quadlet_1/exists" 404 + +t DELETE "libpod/quadlets/nonexistent.container" 404 \ '.Removed|length=0' \ - '.Errors|length=1' \ - '.Errors~.*could not locate quadlet.*' + .cause='no such quadlet' t DELETE "libpod/quadlets/nonexistent.container?ignore=true" 200 \ '.Removed|length=1' \ @@ -353,7 +369,7 @@ podman quadlet install $TMPDIR/$quadlet_2 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'.* + .cause~.*'quadlet is running' t DELETE "libpod/quadlets/$quadlet_2?force=true" 200 fi