diff --git a/pkg/api/handlers/libpod/quadlets.go b/pkg/api/handlers/libpod/quadlets.go index e606bcf9bd7..2557f502eed 100644 --- a/pkg/api/handlers/libpod/quadlets.go +++ b/pkg/api/handlers/libpod/quadlets.go @@ -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) } @@ -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) } diff --git a/pkg/api/server/register_quadlets.go b/pkg/api/server/register_quadlets.go index d082f6e6efb..0d1b347bcbc 100644 --- a/pkg/api/server/register_quadlets.go +++ b/pkg/api/server/register_quadlets.go @@ -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" @@ -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" diff --git a/pkg/bindings/quadlets/quadlets.go b/pkg/bindings/quadlets/quadlets.go new file mode 100644 index 00000000000..5bffb0eb3dc --- /dev/null +++ b/pkg/bindings/quadlets/quadlets.go @@ -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) +} diff --git a/pkg/bindings/quadlets/types.go b/pkg/bindings/quadlets/types.go new file mode 100644 index 00000000000..0feaf2ddf36 --- /dev/null +++ b/pkg/bindings/quadlets/types.go @@ -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 +} diff --git a/pkg/bindings/quadlets/types_exists_options.go b/pkg/bindings/quadlets/types_exists_options.go new file mode 100644 index 00000000000..b4619194075 --- /dev/null +++ b/pkg/bindings/quadlets/types_exists_options.go @@ -0,0 +1,18 @@ +// Code generated by go generate; DO NOT EDIT. +package quadlets + +import ( + "net/url" + + "go.podman.io/podman/v6/pkg/bindings/internal/util" +) + +// Changed returns true if named field has been set +func (o *ExistsOptions) Changed(fieldName string) bool { + return util.Changed(o, fieldName) +} + +// ToParams formats struct fields to be passed to API service +func (o *ExistsOptions) ToParams() (url.Values, error) { + return util.ToParams(o) +} diff --git a/pkg/bindings/quadlets/types_install_options.go b/pkg/bindings/quadlets/types_install_options.go new file mode 100644 index 00000000000..4c35aa4d0d9 --- /dev/null +++ b/pkg/bindings/quadlets/types_install_options.go @@ -0,0 +1,63 @@ +// Code generated by go generate; DO NOT EDIT. +package quadlets + +import ( + "net/url" + + "go.podman.io/podman/v6/pkg/bindings/internal/util" +) + +// Changed returns true if named field has been set +func (o *InstallOptions) Changed(fieldName string) bool { + return util.Changed(o, fieldName) +} + +// ToParams formats struct fields to be passed to API service +func (o *InstallOptions) ToParams() (url.Values, error) { + return util.ToParams(o) +} + +// WithReplace set field Replace to given value +func (o *InstallOptions) WithReplace(value bool) *InstallOptions { + o.Replace = &value + return o +} + +// GetReplace returns value of field Replace +func (o *InstallOptions) GetReplace() bool { + if o.Replace == nil { + var z bool + return z + } + return *o.Replace +} + +// WithReloadSystemd set field ReloadSystemd to given value +func (o *InstallOptions) WithReloadSystemd(value bool) *InstallOptions { + o.ReloadSystemd = &value + return o +} + +// GetReloadSystemd returns value of field ReloadSystemd +func (o *InstallOptions) GetReloadSystemd() bool { + if o.ReloadSystemd == nil { + var z bool + return z + } + return *o.ReloadSystemd +} + +// WithApplication set field Application to given value +func (o *InstallOptions) WithApplication(value string) *InstallOptions { + o.Application = &value + return o +} + +// GetApplication returns value of field Application +func (o *InstallOptions) GetApplication() string { + if o.Application == nil { + var z string + return z + } + return *o.Application +} diff --git a/pkg/bindings/quadlets/types_list_options.go b/pkg/bindings/quadlets/types_list_options.go new file mode 100644 index 00000000000..b55ba1f6945 --- /dev/null +++ b/pkg/bindings/quadlets/types_list_options.go @@ -0,0 +1,33 @@ +// Code generated by go generate; DO NOT EDIT. +package quadlets + +import ( + "net/url" + + "go.podman.io/podman/v6/pkg/bindings/internal/util" +) + +// Changed returns true if named field has been set +func (o *ListOptions) Changed(fieldName string) bool { + return util.Changed(o, fieldName) +} + +// ToParams formats struct fields to be passed to API service +func (o *ListOptions) ToParams() (url.Values, error) { + return util.ToParams(o) +} + +// WithFilters set field Filters to given value +func (o *ListOptions) WithFilters(value map[string][]string) *ListOptions { + o.Filters = value + return o +} + +// GetFilters returns value of field Filters +func (o *ListOptions) GetFilters() map[string][]string { + if o.Filters == nil { + var z map[string][]string + return z + } + return o.Filters +} diff --git a/pkg/bindings/quadlets/types_print_options.go b/pkg/bindings/quadlets/types_print_options.go new file mode 100644 index 00000000000..8c1cce7f82c --- /dev/null +++ b/pkg/bindings/quadlets/types_print_options.go @@ -0,0 +1,18 @@ +// Code generated by go generate; DO NOT EDIT. +package quadlets + +import ( + "net/url" + + "go.podman.io/podman/v6/pkg/bindings/internal/util" +) + +// Changed returns true if named field has been set +func (o *PrintOptions) Changed(fieldName string) bool { + return util.Changed(o, fieldName) +} + +// ToParams formats struct fields to be passed to API service +func (o *PrintOptions) ToParams() (url.Values, error) { + return util.ToParams(o) +} diff --git a/pkg/bindings/quadlets/types_remove_options.go b/pkg/bindings/quadlets/types_remove_options.go new file mode 100644 index 00000000000..353fa3a192a --- /dev/null +++ b/pkg/bindings/quadlets/types_remove_options.go @@ -0,0 +1,93 @@ +// Code generated by go generate; DO NOT EDIT. +package quadlets + +import ( + "net/url" + + "go.podman.io/podman/v6/pkg/bindings/internal/util" +) + +// Changed returns true if named field has been set +func (o *RemoveOptions) Changed(fieldName string) bool { + return util.Changed(o, fieldName) +} + +// ToParams formats struct fields to be passed to API service +func (o *RemoveOptions) ToParams() (url.Values, error) { + return util.ToParams(o) +} + +// WithForce set field Force to given value +func (o *RemoveOptions) WithForce(value bool) *RemoveOptions { + o.Force = &value + return o +} + +// GetForce returns value of field Force +func (o *RemoveOptions) GetForce() bool { + if o.Force == nil { + var z bool + return z + } + return *o.Force +} + +// WithAll set field All to given value +func (o *RemoveOptions) WithAll(value bool) *RemoveOptions { + o.All = &value + return o +} + +// GetAll returns value of field All +func (o *RemoveOptions) GetAll() bool { + if o.All == nil { + var z bool + return z + } + return *o.All +} + +// WithIgnore set field Ignore to given value +func (o *RemoveOptions) WithIgnore(value bool) *RemoveOptions { + o.Ignore = &value + return o +} + +// GetIgnore returns value of field Ignore +func (o *RemoveOptions) GetIgnore() bool { + if o.Ignore == nil { + var z bool + return z + } + return *o.Ignore +} + +// WithReloadSystemd set field ReloadSystemd to given value +func (o *RemoveOptions) WithReloadSystemd(value bool) *RemoveOptions { + o.ReloadSystemd = &value + return o +} + +// GetReloadSystemd returns value of field ReloadSystemd +func (o *RemoveOptions) GetReloadSystemd() bool { + if o.ReloadSystemd == nil { + var z bool + return z + } + return *o.ReloadSystemd +} + +// WithRecursive set field Recursive to given value +func (o *RemoveOptions) WithRecursive(value bool) *RemoveOptions { + o.Recursive = &value + return o +} + +// GetRecursive returns value of field Recursive +func (o *RemoveOptions) GetRecursive() bool { + if o.Recursive == nil { + var z bool + return z + } + return *o.Recursive +} diff --git a/pkg/domain/entities/quadlet.go b/pkg/domain/entities/quadlet.go index 3ce6fde8840..ba2de8c0f6a 100644 --- a/pkg/domain/entities/quadlet.go +++ b/pkg/domain/entities/quadlet.go @@ -1,5 +1,10 @@ package entities +import ( + "encoding/json" + "errors" +) + // QuadletInstallOptions contains options to the `podman quadlet install` command type QuadletInstallOptions struct { // Whether to reload systemd after installation is completed @@ -78,3 +83,63 @@ type QuadletRemoveReport struct { // Errors is a map of Quadlet name to error that occurred during removal. Errors map[string]error } + +func (r *QuadletInstallReport) MarshalJSON() ([]byte, error) { + helper := struct { + InstalledQuadlets map[string]string `json:"InstalledQuadlets"` + QuadletErrors map[string]string `json:"QuadletErrors"` + }{ + InstalledQuadlets: r.InstalledQuadlets, + QuadletErrors: make(map[string]string, len(r.QuadletErrors)), + } + for k, v := range r.QuadletErrors { + helper.QuadletErrors[k] = v.Error() + } + return json.Marshal(helper) +} + +func (r *QuadletInstallReport) UnmarshalJSON(data []byte) error { + var helper struct { + InstalledQuadlets map[string]string `json:"InstalledQuadlets"` + QuadletErrors map[string]string `json:"QuadletErrors"` + } + if err := json.Unmarshal(data, &helper); err != nil { + return err + } + r.InstalledQuadlets = helper.InstalledQuadlets + r.QuadletErrors = make(map[string]error, len(helper.QuadletErrors)) + for k, v := range helper.QuadletErrors { + r.QuadletErrors[k] = errors.New(v) + } + return nil +} + +func (r *QuadletRemoveReport) MarshalJSON() ([]byte, error) { + helper := struct { + Removed []string `json:"Removed"` + Errors map[string]string `json:"Errors"` + }{ + Removed: r.Removed, + Errors: make(map[string]string, len(r.Errors)), + } + for k, v := range r.Errors { + helper.Errors[k] = v.Error() + } + return json.Marshal(helper) +} + +func (r *QuadletRemoveReport) UnmarshalJSON(data []byte) error { + var helper struct { + Removed []string `json:"Removed"` + Errors map[string]string `json:"Errors"` + } + if err := json.Unmarshal(data, &helper); err != nil { + return err + } + r.Removed = helper.Removed + r.Errors = make(map[string]error, len(helper.Errors)) + for k, v := range helper.Errors { + r.Errors[k] = errors.New(v) + } + return nil +} diff --git a/pkg/domain/infra/tunnel/quadlet.go b/pkg/domain/infra/tunnel/quadlet.go index 2a12162a8a5..af53462c72d 100644 --- a/pkg/domain/infra/tunnel/quadlet.go +++ b/pkg/domain/infra/tunnel/quadlet.go @@ -2,29 +2,207 @@ package tunnel import ( "context" - "errors" + "fmt" + "io" + "mime" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "github.com/sirupsen/logrus" + "go.podman.io/podman/v6/pkg/bindings/quadlets" "go.podman.io/podman/v6/pkg/domain/entities" ) -var errNotImplemented = errors.New("not implemented for the remote Podman client") +func (ic *ContainerEngine) QuadletExists(_ context.Context, name string) (*entities.BoolReport, error) { + exists, err := quadlets.Exists(ic.ClientCtx, name, nil) + if err != nil { + return nil, err + } + return &entities.BoolReport{Value: exists}, nil +} + +func (ic *ContainerEngine) QuadletInstall(_ context.Context, pathsOrURLs []string, opts entities.QuadletInstallOptions) (*entities.QuadletInstallReport, error) { + options := new(quadlets.InstallOptions). + WithReplace(opts.Replace). + WithReloadSystemd(opts.ReloadSystemd) + + if opts.Application != "" { + options = options.WithApplication(opts.Application) + } + + files, cleanup, err := resolveInstallPaths(pathsOrURLs, opts.Application) + if err != nil { + return nil, err + } + defer cleanup() + + filePaths := make([]string, len(files)) + fileNames := make([]string, len(files)) + for i, f := range files { + filePaths[i] = f.localPath + fileNames[i] = f.uploadName + } + + return quadlets.Install(ic.ClientCtx, filePaths, fileNames, options) +} + +func (ic *ContainerEngine) QuadletList(_ context.Context, opts entities.QuadletListOptions) ([]*entities.ListQuadlet, error) { + options := new(quadlets.ListOptions) + if len(opts.Filters) > 0 { + filterMap := make(map[string][]string) + for _, f := range opts.Filters { + fname, filter, hasFilter := strings.Cut(f, "=") + if hasFilter { + filterMap[fname] = append(filterMap[fname], filter) + } + } + options.Filters = filterMap + } + return quadlets.List(ic.ClientCtx, options) +} + +func (ic *ContainerEngine) QuadletPrint(_ context.Context, quadlet string) (string, error) { + return quadlets.Print(ic.ClientCtx, quadlet, nil) +} + +func (ic *ContainerEngine) QuadletRemove(_ context.Context, names []string, opts entities.QuadletRemoveOptions) (*entities.QuadletRemoveReport, error) { + options := new(quadlets.RemoveOptions). + WithForce(opts.Force). + WithAll(opts.All). + WithIgnore(opts.Ignore). + WithReloadSystemd(opts.ReloadSystemd). + WithRecursive(opts.Recursive) -func (ic *ContainerEngine) QuadletExists(_ context.Context, _ string) (*entities.BoolReport, error) { - return nil, errNotImplemented + return quadlets.Remove(ic.ClientCtx, names, options) } -func (ic *ContainerEngine) QuadletInstall(_ context.Context, _ []string, _ entities.QuadletInstallOptions) (*entities.QuadletInstallReport, error) { - return nil, errNotImplemented +type installFile struct { + localPath string + uploadName string } -func (ic *ContainerEngine) QuadletList(_ context.Context, _ entities.QuadletListOptions) ([]*entities.ListQuadlet, error) { - return nil, errNotImplemented +// resolveInstallPaths resolves pathsOrURLs into a list of local file paths +// paired with their upload names. URLs are downloaded to temp files. +// Directories are walked recursively, preserving relative paths. +func resolveInstallPaths(pathsOrURLs []string, application string) ([]installFile, func(), error) { + var result []installFile + var tempDirs []string + cleanup := func() { + for _, d := range tempDirs { + if err := os.RemoveAll(d); err != nil { + logrus.Warnf("Failed to remove temp dir %s: %v", d, err) + } + } + } + + for _, arg := range pathsOrURLs { + switch { + case strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://"): + tmpFile, err := downloadToTemp(arg) + if err != nil { + cleanup() + return nil, nil, fmt.Errorf("downloading %s: %w", arg, err) + } + tempDirs = append(tempDirs, filepath.Dir(tmpFile)) + result = append(result, installFile{ + localPath: tmpFile, + uploadName: filepath.Base(tmpFile), + }) + + default: + info, err := os.Stat(arg) + if err != nil { + cleanup() + return nil, nil, fmt.Errorf("cannot stat %s: %w", arg, err) + } + if info.IsDir() { + if application == "" { + cleanup() + return nil, nil, fmt.Errorf("application name cannot be empty when installing from directory") + } + base := arg + err := filepath.WalkDir(arg, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + rel, relErr := filepath.Rel(base, path) + if relErr != nil { + return relErr + } + result = append(result, installFile{ + localPath: path, + uploadName: rel, + }) + return nil + }) + if err != nil { + cleanup() + return nil, nil, fmt.Errorf("walking directory %s: %w", arg, err) + } + } else { + result = append(result, installFile{ + localPath: arg, + uploadName: filepath.Base(arg), + }) + } + } + } + + return result, cleanup, nil } -func (ic *ContainerEngine) QuadletPrint(_ context.Context, _ string) (string, error) { - return "", errNotImplemented +func downloadToTemp(fileURL string) (string, error) { + resp, err := http.Get(fileURL) //nolint:gosec,noctx + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", fmt.Errorf("downloading %s: %s", fileURL, resp.Status) + } + + filename := getFileNameFromResponse(resp, fileURL) + + tmpDir, err := os.MkdirTemp("", "quadlet-download-*") + if err != nil { + return "", err + } + + tmpPath := filepath.Join(tmpDir, filename) + tmpFile, err := os.Create(tmpPath) + if err != nil { + os.RemoveAll(tmpDir) + return "", err + } + defer tmpFile.Close() + + if _, err := io.Copy(tmpFile, resp.Body); err != nil { + os.RemoveAll(tmpDir) + return "", err + } + return tmpPath, nil } -func (ic *ContainerEngine) QuadletRemove(_ context.Context, _ []string, _ entities.QuadletRemoveOptions) (*entities.QuadletRemoveReport, error) { - return nil, errNotImplemented +func getFileNameFromResponse(resp *http.Response, fileURL string) string { + if cd := resp.Header.Get("Content-Disposition"); cd != "" { + _, params, err := mime.ParseMediaType(cd) + if err == nil { + if filename := params["filename"]; filename != "" { + return filename + } + } + } + u, err := url.Parse(fileURL) + if err != nil { + return "quadlet-download" + } + return path.Base(u.Path) } diff --git a/test/system/253-podman-quadlet.bats b/test/system/253-podman-quadlet.bats index 1d04fd35e9e..8a26fbe7d5f 100644 --- a/test/system/253-podman-quadlet.bats +++ b/test/system/253-podman-quadlet.bats @@ -9,7 +9,6 @@ load helpers.registry load helpers.systemd function setup() { - skip_if_remote "podman quadlet is not implemented for remote setup yet" skip_if_journald_unavailable "Needed for RHEL. FIXME: we might be able to re-enable a subset of tests." test -x "$QUADLET" || die "Cannot run quadlet tests without executable \$QUADLET ($QUADLET)" diff --git a/test/system/254-podman-quadlet-multi.bats b/test/system/254-podman-quadlet-multi.bats index a88d263a021..14bd077c956 100644 --- a/test/system/254-podman-quadlet-multi.bats +++ b/test/system/254-podman-quadlet-multi.bats @@ -7,7 +7,6 @@ load helpers load helpers.systemd function setup() { - skip_if_remote "podman quadlet is not implemented for remote setup yet" skip_if_journald_unavailable "Needed for RHEL. FIXME: we might be able to re-enable a subset of tests." basic_setup