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
4 changes: 4 additions & 0 deletions internal/localapi/types.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package localapi

import "errors"

// LocalAPIMap is a map of local paths to their target paths in the VM
type LocalAPIMap struct {
ClientPath string `json:"ClientPath,omitempty"`
RemotePath string `json:"RemotePath,omitempty"`
}

var ErrPathNotAbsolute = errors.New("path is not absolute")
26 changes: 19 additions & 7 deletions internal/localapi/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,29 +237,41 @@ func CheckIfImageBuildPathsOnRunningMachine(ctx context.Context, containerFiles
return translatedContainerFiles, options, true
}

// IsHyperVProvider checks if the current machine provider is Hyper-V.
// It returns true if the provider is Hyper-V, false otherwise, or an error if the check fails.
func IsHyperVProvider(ctx context.Context) (bool, error) {
func getVmProviderType(ctx context.Context) (define.VMType, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
logrus.Debugf("Failed to get client connection: %v", err)
return false, err
return define.UnknownVirt, err
}

_, vmProvider, err := FindMachineByPort(conn.URI.String(), conn.URI)
if err != nil {
logrus.Debugf("Failed to get machine hypervisor type: %v", err)
return false, err
return define.UnknownVirt, err
}

return vmProvider.VMType() == define.HyperVVirt, nil
return vmProvider.VMType(), nil
}

// IsHyperVProvider checks if the current machine provider is Hyper-V.
// It returns true if the provider is Hyper-V, false otherwise, or an error if the check fails.
func IsHyperVProvider(ctx context.Context) (bool, error) {
providerType, err := getVmProviderType(ctx)
return providerType == define.HyperVVirt, err
}

// IsWSLProvider checks if the current machine provider is WSL.
// It returns true if the provider is WSL, false otherwise, or an error if the check fails.
func IsWSLProvider(ctx context.Context) (bool, error) {
providerType, err := getVmProviderType(ctx)
return providerType == define.WSLVirt, err
}

// ValidatePathForLocalAPI checks if the provided path satisfies requirements for local API usage.
// It returns an error if the path is not absolute or does not exist on the filesystem.
func ValidatePathForLocalAPI(path string) error {
if !filepath.IsAbs(path) {
return fmt.Errorf("path %q is not absolute", path)
return fmt.Errorf("%w: %q", ErrPathNotAbsolute, path)
}

if err := fileutils.Exists(path); err != nil {
Expand Down
5 changes: 5 additions & 0 deletions internal/localapi/utils_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ func IsHyperVProvider(ctx context.Context) (bool, error) {
return false, nil
}

func IsWSLProvider(ctx context.Context) (bool, error) {
logrus.Debug("IsWSLProvider is not supported")
return false, nil
}

func ValidatePathForLocalAPI(path string) error {
logrus.Debug("ValidatePathForLocalAPI is not supported")
return nil
Expand Down
81 changes: 65 additions & 16 deletions pkg/api/handlers/libpod/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ package libpod
import (
"errors"
"fmt"
"io/fs"
"net/http"
"path/filepath"

"github.com/containers/podman/v6/internal/localapi"
"github.com/containers/podman/v6/libpod"
"github.com/containers/podman/v6/pkg/api/handlers/utils"
api "github.com/containers/podman/v6/pkg/api/types"
Expand Down Expand Up @@ -212,19 +215,21 @@ func BatchRemoveArtifact(w http.ResponseWriter, r *http.Request) {
utils.WriteResponse(w, http.StatusOK, artifacts)
}

type artifactAddRequestQuery struct {
Name string `schema:"name"`
FileName string `schema:"fileName"`
FileMIMEType string `schema:"fileMIMEType"`
Annotations []string `schema:"annotations"`
ArtifactMIMEType string `schema:"artifactMIMEType"`
Append bool `schema:"append"`
Replace bool `schema:"replace"`
Path string `schema:"path"`
}

func AddArtifact(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)

query := struct {
Name string `schema:"name"`
FileName string `schema:"fileName"`
FileMIMEType string `schema:"fileMIMEType"`
Annotations []string `schema:"annotations"`
ArtifactMIMEType string `schema:"artifactMIMEType"`
Append bool `schema:"append"`
Replace bool `schema:"replace"`
}{}
query := artifactAddRequestQuery{}

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))
Expand All @@ -236,6 +241,56 @@ func AddArtifact(w http.ResponseWriter, r *http.Request) {
return
}

artifactBlobs := []entities.ArtifactBlob{{
BlobReader: r.Body,
FileName: query.FileName,
}}

addArtifactHelper(query, artifactBlobs, w, r)
}

func AddLocalArtifact(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)

query := artifactAddRequestQuery{}

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
}

if query.Name == "" || query.FileName == "" {
utils.Error(w, http.StatusBadRequest, errors.New("name and file parameters are required"))
return
}

cleanPath := filepath.Clean(query.Path)
// Check if the path exists on server side.
// Note: localapi.ValidatePathForLocalAPI returns nil if the file exists and path is absolute, not an error.
switch err := localapi.ValidatePathForLocalAPI(cleanPath); {
case err == nil:
// no error -> continue
case errors.Is(err, localapi.ErrPathNotAbsolute):
utils.Error(w, http.StatusBadRequest, err)
return
case errors.Is(err, fs.ErrNotExist):
utils.Error(w, http.StatusNotFound, fmt.Errorf("file does not exist: %q", cleanPath))
return
default:
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to access file: %w", err))
return
}

artifactBlobs := []entities.ArtifactBlob{{
BlobFilePath: cleanPath,
FileName: query.FileName,
}}

addArtifactHelper(query, artifactBlobs, w, r)
}

func addArtifactHelper(query artifactAddRequestQuery, artifactBlobs []entities.ArtifactBlob, w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
annotations, err := domain_utils.ParseAnnotations(query.Annotations)
if err != nil {
utils.Error(w, http.StatusBadRequest, err)
Expand All @@ -250,13 +305,7 @@ func AddArtifact(w http.ResponseWriter, r *http.Request) {
Replace: query.Replace,
}

artifactBlobs := []entities.ArtifactBlob{{
BlobReader: r.Body,
FileName: query.FileName,
}}

imageEngine := abi.ImageEngine{Libpod: runtime}

artifacts, err := imageEngine.ArtifactAdd(r.Context(), query.Name, artifactBlobs, artifactAddOptions)
if err != nil {
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
Expand Down
9 changes: 6 additions & 3 deletions pkg/api/handlers/libpod/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"strings"

"github.com/containers/buildah"
"github.com/containers/podman/v6/internal/localapi"
"github.com/containers/podman/v6/libpod"
"github.com/containers/podman/v6/libpod/define"
"github.com/containers/podman/v6/pkg/api/handlers"
Expand All @@ -41,7 +42,6 @@ import (
"go.podman.io/storage"
"go.podman.io/storage/pkg/archive"
"go.podman.io/storage/pkg/chrootarchive"
"go.podman.io/storage/pkg/fileutils"
"go.podman.io/storage/pkg/idtools"
)

Expand Down Expand Up @@ -396,10 +396,13 @@ func ImagesLocalLoad(w http.ResponseWriter, r *http.Request) {

cleanPath := filepath.Clean(query.Path)
// Check if the path exists on server side.
// Note: fileutils.Exists returns nil if the file exists, not an error.
switch err := fileutils.Exists(cleanPath); {
// Note: localapi.ValidatePathForLocalAPI returns nil if the file exists and path is absolute, not an error.
switch err := localapi.ValidatePathForLocalAPI(cleanPath); {
case err == nil:
// no error -> continue
case errors.Is(err, localapi.ErrPathNotAbsolute):
utils.Error(w, http.StatusBadRequest, err)
return
case errors.Is(err, fs.ErrNotExist):
utils.Error(w, http.StatusNotFound, fmt.Errorf("file does not exist: %q", cleanPath))
return
Expand Down
59 changes: 59 additions & 0 deletions pkg/api/server/register_artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,65 @@ func (s *APIServer) registerArtifactHandlers(r *mux.Router) error {
// 500:
// $ref: "#/responses/internalError"
r.Handle(VersionedPath("/libpod/artifacts/add"), s.APIHandler(libpod.AddArtifact)).Methods(http.MethodPost)
// swagger:operation POST /libpod/artifacts/local/add libpod ArtifactLocalLibpod
// ---
// tags:
// - artifacts
// summary: Add a local file as an artifact
// description: |
// Add a file from the local filesystem as a new OCI artifact, or append to an existing artifact if 'append' is true.
// produces:
// - application/json
// parameters:
// - name: name
// in: query
// description: Mandatory reference to the artifact (e.g., quay.io/image/artifact:tag)
// required: true
// type: string
// - name: path
// in: query
// description: Absolute path to the local file on the server filesystem to be added
// required: true
// type: string
// - name: fileName
// in: query
// description: Name/title of the file within the artifact
// required: true
// type: string
// - name: fileMIMEType
// in: query
// description: Optionally set the MIME type of the file
// type: string
// - name: annotations
// in: query
// description: Array of annotation strings e.g "test=true"
// type: array
// items:
// type: string
// - name: artifactMIMEType
// in: query
// description: Use type to describe an artifact
// type: string
// - name: append
// in: query
// description: Append files to an existing artifact
// type: boolean
// default: false
// - name: replace
// in: query
// description: Replace an existing artifact with the same name
// type: boolean
// default: false
// responses:
// 201:
// $ref: "#/responses/artifactAddResponse"
// 400:
// $ref: "#/responses/badParamError"
// 404:
// $ref: "#/responses/artifactNotFound"
// 500:
// $ref: "#/responses/internalError"
r.Handle(VersionedPath("/libpod/artifacts/local/add"), s.APIHandler(libpod.AddLocalArtifact)).Methods(http.MethodPost)
// swagger:operation POST /libpod/artifacts/{name}/push libpod ArtifactPushLibpod
// ---
// tags:
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/server/register_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
// name: path
// type: string
// required: true
// description: Path to the image archive file on the server filesystem
// description: Absolute path to the image archive file on the server filesystem
// produces:
// - application/json
// responses:
Expand Down
33 changes: 28 additions & 5 deletions pkg/bindings/artifacts/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,31 @@ import (
"context"
"io"
"net/http"
"net/url"

"github.com/containers/podman/v6/pkg/bindings"
"github.com/containers/podman/v6/pkg/domain/entities"
entitiesTypes "github.com/containers/podman/v6/pkg/domain/entities/types"
)

func Add(ctx context.Context, artifactName string, blobName string, artifactBlob io.Reader, options *AddOptions) (*entitiesTypes.ArtifactAddReport, error) {
conn, err := bindings.GetClient(ctx)
params, err := prepareParams(artifactName, blobName, options)
if err != nil {
return nil, err
}
return helperAdd(ctx, "/artifacts/add", params, artifactBlob)
}

func AddLocal(ctx context.Context, artifactName string, blobName string, blobPath string, options *AddOptions) (*entitiesTypes.ArtifactAddReport, error) {
params, err := prepareParams(artifactName, blobName, options)
if err != nil {
return nil, err
}
params.Set("path", blobPath)
return helperAdd(ctx, "/artifacts/local/add", params, nil)
}

func prepareParams(name string, fileName string, options *AddOptions) (url.Values, error) {
if options == nil {
options = new(AddOptions)
}
Expand All @@ -24,16 +38,25 @@ func Add(ctx context.Context, artifactName string, blobName string, artifactBlob
return nil, err
}

params.Set("name", artifactName)
params.Set("fileName", blobName)
params.Set("name", name)
params.Set("fileName", fileName)

return params, nil
}

func helperAdd(ctx context.Context, endpoint string, params url.Values, artifactBlob io.Reader) (*entities.ArtifactAddReport, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}

response, err := conn.DoRequest(ctx, artifactBlob, http.MethodPost, "/artifacts/add", params, nil)
response, err := conn.DoRequest(ctx, artifactBlob, http.MethodPost, endpoint, params, nil)
if err != nil {
return nil, err
}
defer response.Body.Close()

var artifactAddReport entitiesTypes.ArtifactAddReport
var artifactAddReport entities.ArtifactAddReport
if err := response.Process(&artifactAddReport); err != nil {
return nil, err
}
Expand Down
Loading