Skip to content

Commit 887f669

Browse files
committed
feat: Add OCI Artifact support to the Podman REST API
This patch adds a new endpoint to the REST API called "artifacts" with the following methods: - Add - Extract - Inspect - List - Pull - Push - Remove This API will be utilised by the Podman bindings to add OCI Artifact support to our remote clients. Jira: https://issues.redhat.com/browse/RUN-2711 Signed-off-by: Lewis Roy <[email protected]>
1 parent 27fdd7f commit 887f669

File tree

15 files changed

+1599
-78
lines changed

15 files changed

+1599
-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: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
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+
}
92+
93+
if _, found := r.URL.Query()["retry"]; found {
94+
artifactsPullOptions.MaxRetries = &query.Retry
95+
}
96+
97+
if len(query.RetryDelay) != 0 {
98+
artifactsPullOptions.RetryDelay = query.RetryDelay
99+
}
100+
101+
authConf, authfile, err := auth.GetCredentials(r)
102+
if err != nil {
103+
utils.Error(w, http.StatusBadRequest, err)
104+
return
105+
}
106+
defer auth.RemoveAuthfile(authfile)
107+
108+
artifactsPullOptions.AuthFilePath = authfile
109+
if authConf != nil {
110+
artifactsPullOptions.Username = authConf.Username
111+
artifactsPullOptions.Password = authConf.Password
112+
artifactsPullOptions.IdentityToken = authConf.IdentityToken
113+
}
114+
115+
imageEngine := abi.ImageEngine{Libpod: runtime}
116+
117+
artifacts, err := imageEngine.ArtifactPull(r.Context(), query.Name, artifactsPullOptions)
118+
if err != nil {
119+
var errcd errcode.ErrorCoder
120+
if errors.As(err, &errcd) {
121+
rc := errcd.ErrorCode().Descriptor().HTTPStatusCode
122+
if rc == 401 {
123+
utils.Error(w, 401, errcd.ErrorCode())
124+
return
125+
}
126+
if rc == 404 {
127+
utils.Error(w, 404, errcd.ErrorCode())
128+
return
129+
}
130+
}
131+
utils.InternalServerError(w, err)
132+
return
133+
}
134+
135+
utils.WriteResponse(w, http.StatusOK, artifacts)
136+
}
137+
138+
func RemoveArtifact(w http.ResponseWriter, r *http.Request) {
139+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
140+
imageEngine := abi.ImageEngine{Libpod: runtime}
141+
142+
name := utils.GetName(r)
143+
144+
artifacts, err := imageEngine.ArtifactRm(r.Context(), name, entities.ArtifactRemoveOptions{})
145+
if err != nil {
146+
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
147+
utils.ArtifactNotFound(w, name, err)
148+
return
149+
}
150+
utils.InternalServerError(w, err)
151+
return
152+
}
153+
154+
utils.WriteResponse(w, http.StatusOK, artifacts)
155+
}
156+
157+
func AddArtifact(w http.ResponseWriter, r *http.Request) {
158+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
159+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
160+
161+
query := struct {
162+
Name string `schema:"name"`
163+
FileName string `schema:"fileName"`
164+
FileMIMEType string `schema:"fileMIMEType"`
165+
Annotations []string `schema:"annotations"`
166+
ArtifactMIMEType string `schema:"artifactMIMEType"`
167+
Append bool `schema:"append"`
168+
}{
169+
Append: false,
170+
}
171+
172+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
173+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
174+
return
175+
}
176+
177+
if query.Name == "" || query.FileName == "" {
178+
utils.Error(w, http.StatusBadRequest, errors.New("name and file parameters are required"))
179+
return
180+
}
181+
182+
annotations, err := domain_utils.ParseAnnotations(query.Annotations)
183+
if err != nil {
184+
utils.Error(w, http.StatusBadRequest, err)
185+
return
186+
}
187+
188+
artifactAddOptions := &entities.ArtifactAddOptions{
189+
Append: query.Append,
190+
Annotations: annotations,
191+
ArtifactType: query.ArtifactMIMEType,
192+
FileType: query.FileMIMEType,
193+
}
194+
195+
artifactBlobs := []entities.ArtifactBlob{{
196+
BlobReader: r.Body,
197+
FileName: query.FileName,
198+
}}
199+
200+
imageEngine := abi.ImageEngine{Libpod: runtime}
201+
202+
artifacts, err := imageEngine.ArtifactAdd(r.Context(), query.Name, artifactBlobs, artifactAddOptions)
203+
if err != nil {
204+
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
205+
utils.ArtifactNotFound(w, query.Name, err)
206+
return
207+
}
208+
utils.InternalServerError(w, err)
209+
return
210+
}
211+
212+
utils.WriteResponse(w, http.StatusCreated, artifacts)
213+
}
214+
215+
func PushArtifact(w http.ResponseWriter, r *http.Request) {
216+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
217+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
218+
219+
query := struct {
220+
Retry uint `schema:"retry"`
221+
RetryDelay string `schema:"retrydelay"`
222+
TLSVerify types.OptionalBool `schema:"tlsVerify"`
223+
}{}
224+
225+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
226+
utils.Error(w, http.StatusBadRequest, errors.New("name parameter is required"))
227+
return
228+
}
229+
230+
name := utils.GetName(r)
231+
232+
artifactsPushOptions := entities.ArtifactPushOptions{}
233+
234+
// If TLS verification is explicitly specified (True or False) in the query,
235+
// set the SkipTLSVerify option accordingly.
236+
// If TLSVerify was not set in the query, OptionalBoolUndefined is used and
237+
// handled later based off the target registry configuration.
238+
switch query.TLSVerify {
239+
case types.OptionalBoolTrue:
240+
artifactsPushOptions.SkipTLSVerify = types.NewOptionalBool(false)
241+
case types.OptionalBoolFalse:
242+
artifactsPushOptions.SkipTLSVerify = types.NewOptionalBool(true)
243+
}
244+
245+
if _, found := r.URL.Query()["retry"]; found {
246+
artifactsPushOptions.Retry = &query.Retry
247+
}
248+
249+
if len(query.RetryDelay) != 0 {
250+
artifactsPushOptions.RetryDelay = query.RetryDelay
251+
}
252+
253+
authConf, authfile, err := auth.GetCredentials(r)
254+
if err != nil {
255+
utils.Error(w, http.StatusBadRequest, err)
256+
return
257+
}
258+
defer auth.RemoveAuthfile(authfile)
259+
260+
if authConf != nil {
261+
artifactsPushOptions.Username = authConf.Username
262+
artifactsPushOptions.Password = authConf.Password
263+
}
264+
265+
imageEngine := abi.ImageEngine{Libpod: runtime}
266+
267+
artifacts, err := imageEngine.ArtifactPush(r.Context(), name, artifactsPushOptions)
268+
if err != nil {
269+
var errcd errcode.ErrorCoder
270+
if errors.As(err, &errcd) {
271+
rc := errcd.ErrorCode().Descriptor().HTTPStatusCode
272+
if rc == 401 {
273+
utils.Error(w, 401, errcd.ErrorCode())
274+
return
275+
}
276+
}
277+
278+
var notFoundErr layout.ImageNotFoundError
279+
if errors.As(err, &notFoundErr) {
280+
utils.ArtifactNotFound(w, name, notFoundErr)
281+
return
282+
}
283+
284+
utils.InternalServerError(w, err)
285+
return
286+
}
287+
288+
utils.WriteResponse(w, http.StatusOK, artifacts)
289+
}
290+
291+
func ExtractArtifact(w http.ResponseWriter, r *http.Request) {
292+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
293+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
294+
295+
query := struct {
296+
Digest string `schema:"digest"`
297+
Title string `schema:"title"`
298+
}{}
299+
300+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
301+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
302+
return
303+
}
304+
305+
extractOpts := entities.ArtifactExtractOptions{
306+
Title: query.Title,
307+
Digest: query.Digest,
308+
}
309+
310+
name := utils.GetName(r)
311+
312+
imageEngine := abi.ImageEngine{Libpod: runtime}
313+
314+
err := imageEngine.ArtifactExtractTarStream(r.Context(), w, name, &extractOpts)
315+
if err != nil {
316+
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
317+
utils.ArtifactNotFound(w, name, err)
318+
return
319+
}
320+
utils.InternalServerError(w, err)
321+
return
322+
}
323+
}

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)