Skip to content

Commit badf6b8

Browse files
Merge pull request #26111 from ninja-quokka/restful_art
feat: Add OCI Artifact support to the Podman REST API
2 parents 2d234fa + 99cfdc0 commit badf6b8

File tree

15 files changed

+1648
-78
lines changed

15 files changed

+1648
-78
lines changed

cmd/podman/artifact/add.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package artifact
22

33
import (
44
"fmt"
5+
"path/filepath"
56

67
"github.com/containers/common/pkg/completion"
78
"github.com/containers/podman/v5/cmd/podman/common"
@@ -61,6 +62,8 @@ func init() {
6162
}
6263

6364
func add(cmd *cobra.Command, args []string) error {
65+
artifactName := args[0]
66+
blobs := args[1:]
6467
opts := new(entities.ArtifactAddOptions)
6568

6669
annots, err := utils.ParseAnnotations(addOpts.Annotations)
@@ -72,7 +75,18 @@ func add(cmd *cobra.Command, args []string) error {
7275
opts.Append = addOpts.Append
7376
opts.FileType = addOpts.FileType
7477

75-
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), args[0], args[1:], opts)
78+
artifactBlobs := make([]entities.ArtifactBlob, 0, len(blobs))
79+
80+
for _, blobPath := range blobs {
81+
artifactBlob := entities.ArtifactBlob{
82+
BlobFilePath: blobPath,
83+
FileName: filepath.Base(blobPath),
84+
}
85+
86+
artifactBlobs = append(artifactBlobs, artifactBlob)
87+
}
88+
89+
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), artifactName, artifactBlobs, opts)
7690
if err != nil {
7791
return err
7892
}

pkg/api/handlers/libpod/artifacts.go

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
//go:build !remote
2+
3+
package libpod
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
10+
"github.com/containers/image/v5/oci/layout"
11+
"github.com/containers/image/v5/types"
12+
"github.com/containers/podman/v5/libpod"
13+
"github.com/containers/podman/v5/pkg/api/handlers/utils"
14+
api "github.com/containers/podman/v5/pkg/api/types"
15+
"github.com/containers/podman/v5/pkg/auth"
16+
"github.com/containers/podman/v5/pkg/domain/entities"
17+
"github.com/containers/podman/v5/pkg/domain/infra/abi"
18+
domain_utils "github.com/containers/podman/v5/pkg/domain/utils"
19+
libartifact_types "github.com/containers/podman/v5/pkg/libartifact/types"
20+
"github.com/docker/distribution/registry/api/errcode"
21+
"github.com/gorilla/schema"
22+
)
23+
24+
func InspectArtifact(w http.ResponseWriter, r *http.Request) {
25+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
26+
27+
name := utils.GetName(r)
28+
29+
imageEngine := abi.ImageEngine{Libpod: runtime}
30+
31+
report, err := imageEngine.ArtifactInspect(r.Context(), name, entities.ArtifactInspectOptions{})
32+
if err != nil {
33+
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
34+
utils.ArtifactNotFound(w, name, err)
35+
return
36+
} else {
37+
utils.InternalServerError(w, err)
38+
return
39+
}
40+
}
41+
42+
utils.WriteResponse(w, http.StatusOK, report)
43+
}
44+
45+
func ListArtifact(w http.ResponseWriter, r *http.Request) {
46+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
47+
48+
imageEngine := abi.ImageEngine{Libpod: runtime}
49+
50+
artifacts, err := imageEngine.ArtifactList(r.Context(), entities.ArtifactListOptions{})
51+
if err != nil {
52+
utils.InternalServerError(w, err)
53+
return
54+
}
55+
56+
utils.WriteResponse(w, http.StatusOK, artifacts)
57+
}
58+
59+
func PullArtifact(w http.ResponseWriter, r *http.Request) {
60+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
61+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
62+
63+
query := struct {
64+
Name string `schema:"name"`
65+
Retry uint `schema:"retry"`
66+
RetryDelay string `schema:"retryDelay"`
67+
TLSVerify types.OptionalBool `schema:"tlsVerify"`
68+
}{}
69+
70+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
71+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
72+
return
73+
}
74+
75+
if query.Name == "" {
76+
utils.Error(w, http.StatusBadRequest, errors.New("name parameter is required"))
77+
return
78+
}
79+
80+
artifactsPullOptions := entities.ArtifactPullOptions{}
81+
82+
// If TLS verification is explicitly specified (True or False) in the query,
83+
// set the InsecureSkipTLSVerify option accordingly.
84+
// If TLSVerify was not set in the query, OptionalBoolUndefined is used and
85+
// handled later based off the target registry configuration.
86+
switch query.TLSVerify {
87+
case types.OptionalBoolTrue:
88+
artifactsPullOptions.InsecureSkipTLSVerify = types.NewOptionalBool(false)
89+
case types.OptionalBoolFalse:
90+
artifactsPullOptions.InsecureSkipTLSVerify = types.NewOptionalBool(true)
91+
case types.OptionalBoolUndefined:
92+
// If the user doesn't define TLSVerify in the query, do nothing and pass
93+
// it to the backend code to handle.
94+
default: // Should never happen
95+
panic("Unexpected handling occurred for TLSVerify")
96+
}
97+
98+
if _, found := r.URL.Query()["retry"]; found {
99+
artifactsPullOptions.MaxRetries = &query.Retry
100+
}
101+
102+
if len(query.RetryDelay) != 0 {
103+
artifactsPullOptions.RetryDelay = query.RetryDelay
104+
}
105+
106+
authConf, authfile, err := auth.GetCredentials(r)
107+
if err != nil {
108+
utils.Error(w, http.StatusBadRequest, err)
109+
return
110+
}
111+
defer auth.RemoveAuthfile(authfile)
112+
113+
artifactsPullOptions.AuthFilePath = authfile
114+
if authConf != nil {
115+
artifactsPullOptions.Username = authConf.Username
116+
artifactsPullOptions.Password = authConf.Password
117+
artifactsPullOptions.IdentityToken = authConf.IdentityToken
118+
}
119+
120+
imageEngine := abi.ImageEngine{Libpod: runtime}
121+
122+
artifacts, err := imageEngine.ArtifactPull(r.Context(), query.Name, artifactsPullOptions)
123+
if err != nil {
124+
var errcd errcode.ErrorCoder
125+
// Check to see if any of the wrapped errors is an errcode.ErrorCoder returned from the registry
126+
if errors.As(err, &errcd) {
127+
rc := errcd.ErrorCode().Descriptor().HTTPStatusCode
128+
// Check if the returned error is 401 StatusUnauthorized indicating the request was unauthorized
129+
if rc == http.StatusUnauthorized {
130+
utils.Error(w, http.StatusUnauthorized, errcd.ErrorCode())
131+
return
132+
}
133+
// Check if the returned error is 404 StatusNotFound indicating the artifact was not found
134+
if rc == http.StatusNotFound {
135+
utils.Error(w, http.StatusNotFound, errcd.ErrorCode())
136+
return
137+
}
138+
}
139+
utils.InternalServerError(w, err)
140+
return
141+
}
142+
143+
utils.WriteResponse(w, http.StatusOK, artifacts)
144+
}
145+
146+
func RemoveArtifact(w http.ResponseWriter, r *http.Request) {
147+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
148+
imageEngine := abi.ImageEngine{Libpod: runtime}
149+
150+
name := utils.GetName(r)
151+
152+
artifacts, err := imageEngine.ArtifactRm(r.Context(), name, entities.ArtifactRemoveOptions{})
153+
if err != nil {
154+
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
155+
utils.ArtifactNotFound(w, name, err)
156+
return
157+
}
158+
utils.InternalServerError(w, err)
159+
return
160+
}
161+
162+
utils.WriteResponse(w, http.StatusOK, artifacts)
163+
}
164+
165+
func AddArtifact(w http.ResponseWriter, r *http.Request) {
166+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
167+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
168+
169+
query := struct {
170+
Name string `schema:"name"`
171+
FileName string `schema:"fileName"`
172+
FileMIMEType string `schema:"fileMIMEType"`
173+
Annotations []string `schema:"annotations"`
174+
ArtifactMIMEType string `schema:"artifactMIMEType"`
175+
Append bool `schema:"append"`
176+
}{}
177+
178+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
179+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
180+
return
181+
}
182+
183+
if query.Name == "" || query.FileName == "" {
184+
utils.Error(w, http.StatusBadRequest, errors.New("name and file parameters are required"))
185+
return
186+
}
187+
188+
annotations, err := domain_utils.ParseAnnotations(query.Annotations)
189+
if err != nil {
190+
utils.Error(w, http.StatusBadRequest, err)
191+
return
192+
}
193+
194+
artifactAddOptions := &entities.ArtifactAddOptions{
195+
Append: query.Append,
196+
Annotations: annotations,
197+
ArtifactType: query.ArtifactMIMEType,
198+
FileType: query.FileMIMEType,
199+
}
200+
201+
artifactBlobs := []entities.ArtifactBlob{{
202+
BlobReader: r.Body,
203+
FileName: query.FileName,
204+
}}
205+
206+
imageEngine := abi.ImageEngine{Libpod: runtime}
207+
208+
artifacts, err := imageEngine.ArtifactAdd(r.Context(), query.Name, artifactBlobs, artifactAddOptions)
209+
if err != nil {
210+
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
211+
utils.ArtifactNotFound(w, query.Name, err)
212+
return
213+
}
214+
utils.InternalServerError(w, err)
215+
return
216+
}
217+
218+
utils.WriteResponse(w, http.StatusCreated, artifacts)
219+
}
220+
221+
func PushArtifact(w http.ResponseWriter, r *http.Request) {
222+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
223+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
224+
225+
query := struct {
226+
Retry uint `schema:"retry"`
227+
RetryDelay string `schema:"retrydelay"`
228+
TLSVerify types.OptionalBool `schema:"tlsVerify"`
229+
}{}
230+
231+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
232+
utils.Error(w, http.StatusBadRequest, errors.New("name parameter is required"))
233+
return
234+
}
235+
236+
name := utils.GetName(r)
237+
238+
artifactsPushOptions := entities.ArtifactPushOptions{}
239+
240+
// If TLS verification is explicitly specified (True or False) in the query,
241+
// set the SkipTLSVerify option accordingly.
242+
// If TLSVerify was not set in the query, OptionalBoolUndefined is used and
243+
// handled later based off the target registry configuration.
244+
switch query.TLSVerify {
245+
case types.OptionalBoolTrue:
246+
artifactsPushOptions.SkipTLSVerify = types.NewOptionalBool(false)
247+
case types.OptionalBoolFalse:
248+
artifactsPushOptions.SkipTLSVerify = types.NewOptionalBool(true)
249+
case types.OptionalBoolUndefined:
250+
// If the user doesn't define TLSVerify in the query, do nothing and pass
251+
// it to the backend code to handle.
252+
default: // Should never happen
253+
panic("Unexpected handling occurred for TLSVerify")
254+
}
255+
256+
if _, found := r.URL.Query()["retry"]; found {
257+
artifactsPushOptions.Retry = &query.Retry
258+
}
259+
260+
if len(query.RetryDelay) != 0 {
261+
artifactsPushOptions.RetryDelay = query.RetryDelay
262+
}
263+
264+
authConf, authfile, err := auth.GetCredentials(r)
265+
if err != nil {
266+
utils.Error(w, http.StatusBadRequest, err)
267+
return
268+
}
269+
defer auth.RemoveAuthfile(authfile)
270+
271+
if authConf != nil {
272+
artifactsPushOptions.Username = authConf.Username
273+
artifactsPushOptions.Password = authConf.Password
274+
}
275+
276+
imageEngine := abi.ImageEngine{Libpod: runtime}
277+
278+
artifacts, err := imageEngine.ArtifactPush(r.Context(), name, artifactsPushOptions)
279+
if err != nil {
280+
var errcd errcode.ErrorCoder
281+
// Check to see if any of the wrapped errors is an errcode.ErrorCoder returned from the registry
282+
if errors.As(err, &errcd) {
283+
rc := errcd.ErrorCode().Descriptor().HTTPStatusCode
284+
// Check if the returned error is 401 indicating the request was unauthorized
285+
if rc == 401 {
286+
utils.Error(w, 401, errcd.ErrorCode())
287+
return
288+
}
289+
}
290+
291+
var notFoundErr layout.ImageNotFoundError
292+
if errors.As(err, &notFoundErr) {
293+
utils.ArtifactNotFound(w, name, notFoundErr)
294+
return
295+
}
296+
297+
utils.InternalServerError(w, err)
298+
return
299+
}
300+
301+
utils.WriteResponse(w, http.StatusOK, artifacts)
302+
}
303+
304+
func ExtractArtifact(w http.ResponseWriter, r *http.Request) {
305+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
306+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
307+
308+
query := struct {
309+
Digest string `schema:"digest"`
310+
Title string `schema:"title"`
311+
}{}
312+
313+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
314+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
315+
return
316+
}
317+
318+
extractOpts := entities.ArtifactExtractOptions{
319+
Title: query.Title,
320+
Digest: query.Digest,
321+
}
322+
323+
name := utils.GetName(r)
324+
325+
imageEngine := abi.ImageEngine{Libpod: runtime}
326+
327+
err := imageEngine.ArtifactExtractTarStream(r.Context(), w, name, &extractOpts)
328+
if err != nil {
329+
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
330+
utils.ArtifactNotFound(w, name, err)
331+
return
332+
}
333+
utils.InternalServerError(w, err)
334+
return
335+
}
336+
}

pkg/api/handlers/swagger/errors.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ type containerNotFound struct {
2323
Body errorhandling.ErrorModel
2424
}
2525

26+
// No such artifact
27+
// swagger:response
28+
type artifactNotFound struct {
29+
// in:body
30+
Body errorhandling.ErrorModel
31+
}
32+
33+
// error in authentication
34+
// swagger:response
35+
type artifactBadAuth struct {
36+
// in:body
37+
Body errorhandling.ErrorModel
38+
}
39+
2640
// No such network
2741
// swagger:response
2842
type networkNotFound struct {

0 commit comments

Comments
 (0)