Skip to content

Commit cf0e51e

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 a3e65c7 commit cf0e51e

File tree

15 files changed

+1609
-78
lines changed

15 files changed

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

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)