Skip to content

Commit 16309c6

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 a1ac6c3 commit 16309c6

File tree

15 files changed

+1034
-67
lines changed

15 files changed

+1034
-67
lines changed

cmd/podman/artifact/add.go

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package artifact
22

33
import (
44
"fmt"
5+
"os"
6+
"path/filepath"
57

68
"github.com/containers/common/pkg/completion"
79
"github.com/containers/podman/v5/cmd/podman/common"
@@ -11,20 +13,18 @@ import (
1113
"github.com/spf13/cobra"
1214
)
1315

14-
var (
15-
addCmd = &cobra.Command{
16-
Use: "add [options] ARTIFACT PATH [...PATH]",
17-
Short: "Add an OCI artifact to the local store",
18-
Long: "Add an OCI artifact to the local store from the local filesystem",
19-
RunE: add,
20-
Args: cobra.MinimumNArgs(2),
21-
ValidArgsFunction: common.AutocompleteArtifactAdd,
22-
Example: `podman artifact add quay.io/myimage/myartifact:latest /tmp/foobar.txt
16+
var addCmd = &cobra.Command{
17+
Use: "add [options] ARTIFACT PATH [...PATH]",
18+
Short: "Add an OCI artifact to the local store",
19+
Long: "Add an OCI artifact to the local store from the local filesystem",
20+
RunE: add,
21+
Args: cobra.MinimumNArgs(2),
22+
ValidArgsFunction: common.AutocompleteArtifactAdd,
23+
Example: `podman artifact add quay.io/myimage/myartifact:latest /tmp/foobar.txt
2324
podman artifact add --file-type text/yaml quay.io/myimage/myartifact:latest /tmp/foobar.yaml
2425
podman artifact add --append quay.io/myimage/myartifact:latest /tmp/foobar.tar.gz`,
25-
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
26-
}
27-
)
26+
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
27+
}
2828

2929
type artifactAddOptions struct {
3030
ArtifactType string
@@ -33,9 +33,7 @@ type artifactAddOptions struct {
3333
FileType string
3434
}
3535

36-
var (
37-
addOpts artifactAddOptions
38-
)
36+
var addOpts artifactAddOptions
3937

4038
func init() {
4139
registry.Commands = append(registry.Commands, registry.CliCommand{
@@ -61,6 +59,8 @@ func init() {
6159
}
6260

6361
func add(cmd *cobra.Command, args []string) error {
62+
artifactName := args[0]
63+
blobs := args[1:]
6464
opts := new(entities.ArtifactAddOptions)
6565

6666
annots, err := utils.ParseAnnotations(addOpts.Annotations)
@@ -72,7 +72,23 @@ func add(cmd *cobra.Command, args []string) error {
7272
opts.Append = addOpts.Append
7373
opts.FileType = addOpts.FileType
7474

75-
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), args[0], args[1:], opts)
75+
artifactBlobs := make([]entities.ArtifactBlob, 0, len(blobs))
76+
77+
for _, blobPath := range blobs {
78+
b, err := os.Open(blobPath)
79+
if err != nil {
80+
return fmt.Errorf("error opening path %s: %w", blobPath, err)
81+
}
82+
defer b.Close()
83+
84+
artifactBlob := entities.ArtifactBlob{
85+
Blob: b,
86+
Filename: filepath.Base(blobPath),
87+
}
88+
artifactBlobs = append(artifactBlobs, artifactBlob)
89+
}
90+
91+
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), artifactName, artifactBlobs, opts)
7692
if err != nil {
7793
return err
7894
}

pkg/api/handlers/compat/containers_archive.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ package compat
44

55
import (
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"net/http"
910
"os"
1011
"strings"
1112

12-
"errors"
13-
1413
"github.com/containers/podman/v5/libpod"
1514
"github.com/containers/podman/v5/libpod/define"
1615
"github.com/containers/podman/v5/pkg/api/handlers/utils"

pkg/api/handlers/libpod/artifacts.go

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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/types"
11+
libartifact_types "github.com/containers/podman/v5/pkg/libartifact/types"
12+
13+
"github.com/containers/podman/v5/libpod"
14+
"github.com/containers/podman/v5/pkg/api/handlers/utils"
15+
16+
domain_utils "github.com/containers/podman/v5/pkg/domain/utils"
17+
18+
api "github.com/containers/podman/v5/pkg/api/types"
19+
"github.com/containers/podman/v5/pkg/auth"
20+
"github.com/containers/podman/v5/pkg/domain/entities"
21+
"github.com/containers/podman/v5/pkg/domain/infra/abi"
22+
"github.com/gorilla/schema"
23+
)
24+
25+
func InspectArtifact(w http.ResponseWriter, r *http.Request) {
26+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
27+
name := utils.GetName(r)
28+
imageEngine := abi.ImageEngine{Libpod: runtime}
29+
report, err := imageEngine.ArtifactInspect(r.Context(), name, entities.ArtifactInspectOptions{})
30+
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
31+
utils.ArtifactNotFound(w, name, err)
32+
return
33+
}
34+
if err != nil {
35+
utils.InternalServerError(w, err)
36+
return
37+
}
38+
utils.WriteResponse(w, http.StatusOK, report)
39+
}
40+
41+
func ListArtifact(w http.ResponseWriter, r *http.Request) {
42+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
43+
imageEngine := abi.ImageEngine{Libpod: runtime}
44+
artifacts, err := imageEngine.ArtifactList(r.Context(), entities.ArtifactListOptions{})
45+
if err != nil {
46+
utils.InternalServerError(w, err)
47+
return
48+
}
49+
utils.WriteResponse(w, http.StatusOK, artifacts)
50+
}
51+
52+
func PullArtifact(w http.ResponseWriter, r *http.Request) {
53+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
54+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
55+
query := struct {
56+
Name string `schema:"name"`
57+
Retry uint `schema:"retry"`
58+
RetryDelay string `schema:"retryDelay"`
59+
TLSVerify bool `schema:"tlsVerify"`
60+
}{
61+
TLSVerify: true,
62+
}
63+
64+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
65+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
66+
return
67+
}
68+
69+
if len(query.Name) == 0 {
70+
utils.InternalServerError(w, errors.New("name parameter cannot be empty"))
71+
return
72+
}
73+
74+
artifactsPullOptions := entities.ArtifactPullOptions{}
75+
76+
if _, found := r.URL.Query()["tlsVerify"]; found {
77+
artifactsPullOptions.InsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
78+
}
79+
80+
if _, found := r.URL.Query()["retry"]; found {
81+
artifactsPullOptions.MaxRetries = &query.Retry
82+
}
83+
84+
if len(query.RetryDelay) != 0 {
85+
artifactsPullOptions.RetryDelay = query.RetryDelay
86+
}
87+
88+
authConf, authfile, err := auth.GetCredentials(r)
89+
if err != nil {
90+
utils.Error(w, http.StatusBadRequest, err)
91+
return
92+
}
93+
defer auth.RemoveAuthfile(authfile)
94+
95+
artifactsPullOptions.AuthFilePath = authfile
96+
if authConf != nil {
97+
artifactsPullOptions.Username = authConf.Username
98+
artifactsPullOptions.Password = authConf.Password
99+
artifactsPullOptions.IdentityToken = authConf.IdentityToken
100+
}
101+
102+
imageEngine := abi.ImageEngine{Libpod: runtime}
103+
artifacts, err := imageEngine.ArtifactPull(r.Context(), query.Name, artifactsPullOptions)
104+
if err != nil {
105+
utils.InternalServerError(w, err)
106+
return
107+
}
108+
utils.WriteResponse(w, http.StatusOK, artifacts)
109+
}
110+
111+
func RemoveArtifact(w http.ResponseWriter, r *http.Request) {
112+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
113+
imageEngine := abi.ImageEngine{Libpod: runtime}
114+
115+
name := utils.GetName(r)
116+
117+
artifacts, err := imageEngine.ArtifactRm(r.Context(), name, entities.ArtifactRemoveOptions{})
118+
119+
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
120+
utils.ArtifactNotFound(w, name, err)
121+
return
122+
}
123+
if err != nil {
124+
utils.InternalServerError(w, err)
125+
return
126+
}
127+
utils.WriteResponse(w, http.StatusOK, artifacts)
128+
}
129+
130+
func AddArtifact(w http.ResponseWriter, r *http.Request) {
131+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
132+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
133+
query := struct {
134+
Name string `schema:"name"`
135+
File string `schema:"file"`
136+
Annotations []string `schema:"annotations"`
137+
Type string `schema:"type"`
138+
Append bool `schema:"append"`
139+
}{
140+
Append: false,
141+
}
142+
143+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
144+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
145+
return
146+
}
147+
148+
if query.Name == "" || len(query.File) == 0 {
149+
utils.Error(w, http.StatusBadRequest, errors.New("name and file parameters are required"))
150+
return
151+
}
152+
153+
annotations, err := domain_utils.ParseAnnotations(query.Annotations)
154+
if err != nil {
155+
utils.Error(w, http.StatusBadRequest, errors.New("error parsing annotations"))
156+
return
157+
}
158+
159+
// FIX: Should we verify r.body isn't empty here? It's hard as it's streaming
160+
161+
artifactAddOptions := &entities.ArtifactAddOptions{
162+
Append: query.Append,
163+
Annotations: annotations,
164+
ArtifactType: query.Type,
165+
}
166+
167+
artifactBlobs := []entities.ArtifactBlob{{ // FIX: Should this be a pointer?
168+
Blob: r.Body,
169+
Filename: query.File,
170+
}}
171+
172+
imageEngine := abi.ImageEngine{Libpod: runtime}
173+
artifacts, err := imageEngine.ArtifactAdd(r.Context(), query.Name, artifactBlobs, artifactAddOptions)
174+
if err != nil {
175+
utils.InternalServerError(w, err)
176+
return
177+
}
178+
utils.WriteResponse(w, http.StatusCreated, artifacts)
179+
}
180+
181+
func PushArtifact(w http.ResponseWriter, r *http.Request) {
182+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
183+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
184+
query := struct {
185+
Retry uint `schema:"retry"`
186+
RetryDelay string `schema:"retrydelay"`
187+
TLSVerify bool `schema:"tlsVerify"`
188+
}{
189+
TLSVerify: true,
190+
}
191+
192+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
193+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
194+
return
195+
}
196+
197+
name := utils.GetName(r)
198+
199+
artifactsPushOptions := entities.ArtifactPushOptions{}
200+
201+
if _, found := r.URL.Query()["tlsVerify"]; found {
202+
artifactsPushOptions.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
203+
}
204+
205+
if _, found := r.URL.Query()["retry"]; found {
206+
artifactsPushOptions.Retry = &query.Retry
207+
}
208+
209+
if len(query.RetryDelay) != 0 {
210+
artifactsPushOptions.RetryDelay = query.RetryDelay
211+
}
212+
213+
authConf, authfile, err := auth.GetCredentials(r)
214+
if err != nil {
215+
utils.Error(w, http.StatusBadRequest, err)
216+
return
217+
}
218+
defer auth.RemoveAuthfile(authfile)
219+
220+
if authConf != nil {
221+
artifactsPushOptions.Username = authConf.Username
222+
artifactsPushOptions.Password = authConf.Password
223+
}
224+
225+
imageEngine := abi.ImageEngine{Libpod: runtime}
226+
227+
// FIX: push currently just returns an empty struct, should we return the digest?
228+
artifacts, err := imageEngine.ArtifactPush(r.Context(), name, artifactsPushOptions)
229+
if err != nil {
230+
utils.InternalServerError(w, err)
231+
return
232+
}
233+
utils.WriteResponse(w, http.StatusNoContent, artifacts)
234+
}
235+
236+
func ExtractArtifact(w http.ResponseWriter, r *http.Request) {
237+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
238+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
239+
240+
query := struct {
241+
Digest string `schema:"digest"`
242+
Title string `schema:"title"`
243+
}{}
244+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
245+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
246+
return
247+
}
248+
249+
extractOpts := entities.ArtifactExtractOptions{
250+
Title: query.Title,
251+
Digest: query.Digest,
252+
}
253+
254+
name := utils.GetName(r)
255+
256+
imageEngine := abi.ImageEngine{Libpod: runtime}
257+
258+
blobs, err := imageEngine.ArtifactExtractTarStream(r.Context(), name, &extractOpts)
259+
if err != nil {
260+
utils.InternalServerError(w, err)
261+
return
262+
}
263+
264+
utils.WriteResponse(w, http.StatusOK, blobs)
265+
}

pkg/api/handlers/swagger/errors.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ 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+
2633
// No such network
2734
// swagger:response
2835
type networkNotFound struct {

0 commit comments

Comments
 (0)