Skip to content

Commit e732ee5

Browse files
committed
Add local artifact add API endpoint
Fixes: https://issues.redhat.com/browse/RUN-3385 Fixes: #26321 Signed-off-by: Jan Rodák <[email protected]>
1 parent a7da73c commit e732ee5

File tree

6 files changed

+290
-36
lines changed

6 files changed

+290
-36
lines changed

pkg/api/handlers/libpod/artifacts.go

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ package libpod
55
import (
66
"errors"
77
"fmt"
8+
"io/fs"
89
"net/http"
10+
"path/filepath"
911

12+
"github.com/containers/podman/v6/internal/localapi"
1013
"github.com/containers/podman/v6/libpod"
1114
"github.com/containers/podman/v6/pkg/api/handlers/utils"
1215
api "github.com/containers/podman/v6/pkg/api/types"
@@ -212,19 +215,21 @@ func BatchRemoveArtifact(w http.ResponseWriter, r *http.Request) {
212215
utils.WriteResponse(w, http.StatusOK, artifacts)
213216
}
214217

218+
type artifactAddRequestQuery struct {
219+
Name string `schema:"name"`
220+
FileName string `schema:"fileName"`
221+
FileMIMEType string `schema:"fileMIMEType"`
222+
Annotations []string `schema:"annotations"`
223+
ArtifactMIMEType string `schema:"artifactMIMEType"`
224+
Append bool `schema:"append"`
225+
Replace bool `schema:"replace"`
226+
Path string `schema:"path"`
227+
}
228+
215229
func AddArtifact(w http.ResponseWriter, r *http.Request) {
216-
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
217230
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
218231

219-
query := struct {
220-
Name string `schema:"name"`
221-
FileName string `schema:"fileName"`
222-
FileMIMEType string `schema:"fileMIMEType"`
223-
Annotations []string `schema:"annotations"`
224-
ArtifactMIMEType string `schema:"artifactMIMEType"`
225-
Append bool `schema:"append"`
226-
Replace bool `schema:"replace"`
227-
}{}
232+
query := artifactAddRequestQuery{}
228233

229234
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
230235
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
@@ -236,6 +241,53 @@ func AddArtifact(w http.ResponseWriter, r *http.Request) {
236241
return
237242
}
238243

244+
artifactBlobs := []entities.ArtifactBlob{{
245+
BlobReader: r.Body,
246+
FileName: query.FileName,
247+
}}
248+
249+
addArtifactHelper(query, artifactBlobs, w, r)
250+
}
251+
252+
func AddLocalArtifact(w http.ResponseWriter, r *http.Request) {
253+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
254+
255+
query := artifactAddRequestQuery{}
256+
257+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
258+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
259+
return
260+
}
261+
262+
if query.Name == "" || query.FileName == "" {
263+
utils.Error(w, http.StatusBadRequest, errors.New("name and file parameters are required"))
264+
return
265+
}
266+
267+
cleanPath := filepath.Clean(query.Path)
268+
// Check if the path exists on server side.
269+
// Note: localapi.ValidatePathForLocalAPI returns nil if the file exists and path is absolute, not an error.
270+
switch err := localapi.ValidatePathForLocalAPI(cleanPath); {
271+
case err == nil:
272+
// no error -> continue
273+
case errors.Is(err, fs.ErrNotExist):
274+
utils.Error(w, http.StatusNotFound, fmt.Errorf("file does not exist: %q", cleanPath))
275+
return
276+
default:
277+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to access file: %w", err))
278+
return
279+
}
280+
281+
artifactBlobs := []entities.ArtifactBlob{{
282+
BlobFilePath: cleanPath,
283+
FileName: query.FileName,
284+
}}
285+
286+
addArtifactHelper(query, artifactBlobs, w, r)
287+
}
288+
289+
func addArtifactHelper(query artifactAddRequestQuery, artifactBlobs []entities.ArtifactBlob, w http.ResponseWriter, r *http.Request) {
290+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
239291
annotations, err := domain_utils.ParseAnnotations(query.Annotations)
240292
if err != nil {
241293
utils.Error(w, http.StatusBadRequest, err)
@@ -250,13 +302,7 @@ func AddArtifact(w http.ResponseWriter, r *http.Request) {
250302
Replace: query.Replace,
251303
}
252304

253-
artifactBlobs := []entities.ArtifactBlob{{
254-
BlobReader: r.Body,
255-
FileName: query.FileName,
256-
}}
257-
258305
imageEngine := abi.ImageEngine{Libpod: runtime}
259-
260306
artifacts, err := imageEngine.ArtifactAdd(r.Context(), query.Name, artifactBlobs, artifactAddOptions)
261307
if err != nil {
262308
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {

pkg/api/server/register_artifacts.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,65 @@ func (s *APIServer) registerArtifactHandlers(r *mux.Router) error {
212212
// 500:
213213
// $ref: "#/responses/internalError"
214214
r.Handle(VersionedPath("/libpod/artifacts/add"), s.APIHandler(libpod.AddArtifact)).Methods(http.MethodPost)
215+
// swagger:operation POST /libpod/artifacts/local/add libpod ArtifactLocalLibpod
216+
// ---
217+
// tags:
218+
// - artifacts
219+
// summary: Add a local file as an artifact
220+
// description: |
221+
// Add a file from the local filesystem as a new OCI artifact, or append to an existing artifact if 'append' is true.
222+
// produces:
223+
// - application/json
224+
// parameters:
225+
// - name: name
226+
// in: query
227+
// description: Mandatory reference to the artifact (e.g., quay.io/image/artifact:tag)
228+
// required: true
229+
// type: string
230+
// - name: path
231+
// in: query
232+
// description: Absolute path to the local file on the server filesystem to be added
233+
// required: true
234+
// type: string
235+
// - name: fileName
236+
// in: query
237+
// description: Name/title of the file within the artifact
238+
// required: true
239+
// type: string
240+
// - name: fileMIMEType
241+
// in: query
242+
// description: Optionally set the MIME type of the file
243+
// type: string
244+
// - name: annotations
245+
// in: query
246+
// description: Array of annotation strings e.g "test=true"
247+
// type: array
248+
// items:
249+
// type: string
250+
// - name: artifactMIMEType
251+
// in: query
252+
// description: Use type to describe an artifact
253+
// type: string
254+
// - name: append
255+
// in: query
256+
// description: Append files to an existing artifact
257+
// type: boolean
258+
// default: false
259+
// - name: replace
260+
// in: query
261+
// description: Replace an existing artifact with the same name
262+
// type: boolean
263+
// default: false
264+
// responses:
265+
// 201:
266+
// $ref: "#/responses/artifactAddResponse"
267+
// 400:
268+
// $ref: "#/responses/badParamError"
269+
// 404:
270+
// $ref: "#/responses/artifactNotFound"
271+
// 500:
272+
// $ref: "#/responses/internalError"
273+
r.Handle(VersionedPath("/libpod/artifacts/local/add"), s.APIHandler(libpod.AddLocalArtifact)).Methods(http.MethodPost)
215274
// swagger:operation POST /libpod/artifacts/{name}/push libpod ArtifactPushLibpod
216275
// ---
217276
// tags:

pkg/bindings/artifacts/add.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,31 @@ import (
44
"context"
55
"io"
66
"net/http"
7+
"net/url"
78

89
"github.com/containers/podman/v6/pkg/bindings"
10+
"github.com/containers/podman/v6/pkg/domain/entities"
911
entitiesTypes "github.com/containers/podman/v6/pkg/domain/entities/types"
1012
)
1113

1214
func Add(ctx context.Context, artifactName string, blobName string, artifactBlob io.Reader, options *AddOptions) (*entitiesTypes.ArtifactAddReport, error) {
13-
conn, err := bindings.GetClient(ctx)
15+
params, err := prepareParams(artifactName, blobName, options)
1416
if err != nil {
1517
return nil, err
1618
}
19+
return helperAdd(ctx, "/artifacts/add", params, artifactBlob)
20+
}
21+
22+
func AddLocal(ctx context.Context, artifactName string, blobName string, blobPath string, options *AddOptions) (*entitiesTypes.ArtifactAddReport, error) {
23+
params, err := prepareParams(artifactName, blobName, options)
24+
if err != nil {
25+
return nil, err
26+
}
27+
params.Set("path", blobPath)
28+
return helperAdd(ctx, "/artifacts/local/add", params, nil)
29+
}
1730

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

27-
params.Set("name", artifactName)
28-
params.Set("fileName", blobName)
41+
params.Set("name", name)
42+
params.Set("fileName", fileName)
43+
44+
return params, nil
45+
}
46+
47+
func helperAdd(ctx context.Context, endpoint string, params url.Values, artifactBlob io.Reader) (*entities.ArtifactAddReport, error) {
48+
conn, err := bindings.GetClient(ctx)
49+
if err != nil {
50+
return nil, err
51+
}
2952

30-
response, err := conn.DoRequest(ctx, artifactBlob, http.MethodPost, "/artifacts/add", params, nil)
53+
response, err := conn.DoRequest(ctx, artifactBlob, http.MethodPost, endpoint, params, nil)
3154
if err != nil {
3255
return nil, err
3356
}
3457
defer response.Body.Close()
3558

36-
var artifactAddReport entitiesTypes.ArtifactAddReport
59+
var artifactAddReport entities.ArtifactAddReport
3760
if err := response.Process(&artifactAddReport); err != nil {
3861
return nil, err
3962
}

pkg/domain/infra/tunnel/artifact.go

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"net/http"
89
"os"
910

11+
"github.com/containers/podman/v6/internal/localapi"
1012
"github.com/containers/podman/v6/pkg/bindings/artifacts"
1113
"github.com/containers/podman/v6/pkg/domain/entities"
14+
"github.com/containers/podman/v6/pkg/errorhandling"
1215
"go.podman.io/image/v5/types"
1316
)
1417

@@ -101,26 +104,57 @@ func (ir *ImageEngine) ArtifactAdd(_ context.Context, name string, artifactBlob
101104
// When adding more than 1 blob, set append true after the first
102105
options.WithAppend(true)
103106
}
104-
f, err := os.Open(blob.BlobFilePath)
105-
if err != nil {
106-
return nil, err
107-
}
108-
defer f.Close()
109107

110-
artifactAddReport, err = artifacts.Add(ir.ClientCtx, name, blob.FileName, f, &options)
111-
if err != nil && i > 0 {
112-
removeOptions := artifacts.RemoveOptions{
113-
Artifacts: []string{name},
108+
var err error
109+
if localMap, ok := localapi.CheckPathOnRunningMachine(ir.ClientCtx, blob.BlobFilePath); ok {
110+
artifactAddReport, err = artifacts.AddLocal(ir.ClientCtx, name, blob.FileName, localMap.RemotePath, &options)
111+
if err == nil {
112+
continue
114113
}
115-
_, recoverErr := artifacts.Remove(ir.ClientCtx, "", &removeOptions)
116-
if recoverErr != nil {
117-
return nil, fmt.Errorf("failed to cleanup unfinished artifact add: %w", errors.Join(err, recoverErr))
114+
var errModel *errorhandling.ErrorModel
115+
if errors.As(err, &errModel) {
116+
switch errModel.ResponseCode {
117+
case http.StatusNotFound, http.StatusMethodNotAllowed:
118+
default:
119+
return nil, artifactAddErrorCleanup(ir.ClientCtx, i, name, err)
120+
}
121+
} else {
122+
return nil, artifactAddErrorCleanup(ir.ClientCtx, i, name, err)
118123
}
119-
return nil, err
120124
}
125+
126+
artifactAddReport, err = addArtifact(ir.ClientCtx, name, i, blob, &options)
121127
if err != nil {
122128
return nil, err
123129
}
124130
}
125131
return artifactAddReport, nil
126132
}
133+
134+
func artifactAddErrorCleanup(ctx context.Context, index int, name string, err error) error {
135+
if index == 0 {
136+
return err
137+
}
138+
removeOptions := artifacts.RemoveOptions{
139+
Artifacts: []string{name},
140+
}
141+
_, recoverErr := artifacts.Remove(ctx, "", &removeOptions)
142+
if recoverErr != nil {
143+
return fmt.Errorf("failed to cleanup unfinished artifact add: %w", errors.Join(err, recoverErr))
144+
}
145+
return err
146+
}
147+
148+
func addArtifact(ctx context.Context, name string, index int, blob entities.ArtifactBlob, options *artifacts.AddOptions) (*entities.ArtifactAddReport, error) {
149+
f, err := os.Open(blob.BlobFilePath)
150+
if err != nil {
151+
return nil, err
152+
}
153+
defer f.Close()
154+
155+
artifactAddReport, err := artifacts.Add(ctx, name, blob.FileName, f, options)
156+
if err != nil {
157+
return nil, artifactAddErrorCleanup(ctx, index, name, err)
158+
}
159+
return artifactAddReport, nil
160+
}

test/apiv2/python/rest_api/fixtures/api_testcase.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ def add(self) -> requests.Response:
8484
os.remove(self.file.name)
8585
return r
8686

87+
def add_local(self) -> requests.Response:
88+
try:
89+
r = requests.post(
90+
self.uri + "/artifacts/local/add",
91+
params=self.parameters,
92+
)
93+
except Exception:
94+
pass
95+
96+
if self.file is not None and os.path.exists(self.file.name):
97+
os.remove(self.file.name)
98+
return r
99+
87100
def do_artifact_inspect_request(self) -> requests.Response:
88101
r = requests.get(
89102
self.uri + "/artifacts/" + self.name + "/json",

0 commit comments

Comments
 (0)