-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat: Add OCI Artifact support to the Podman REST API #26111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,336 @@ | ||
//go:build !remote | ||
|
||
package libpod | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/containers/image/v5/oci/layout" | ||
"github.com/containers/image/v5/types" | ||
"github.com/containers/podman/v5/libpod" | ||
"github.com/containers/podman/v5/pkg/api/handlers/utils" | ||
api "github.com/containers/podman/v5/pkg/api/types" | ||
"github.com/containers/podman/v5/pkg/auth" | ||
"github.com/containers/podman/v5/pkg/domain/entities" | ||
"github.com/containers/podman/v5/pkg/domain/infra/abi" | ||
domain_utils "github.com/containers/podman/v5/pkg/domain/utils" | ||
libartifact_types "github.com/containers/podman/v5/pkg/libartifact/types" | ||
"github.com/docker/distribution/registry/api/errcode" | ||
"github.com/gorilla/schema" | ||
) | ||
|
||
func InspectArtifact(w http.ResponseWriter, r *http.Request) { | ||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) | ||
|
||
name := utils.GetName(r) | ||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime} | ||
|
||
report, err := imageEngine.ArtifactInspect(r.Context(), name, entities.ArtifactInspectOptions{}) | ||
if err != nil { | ||
if errors.Is(err, libartifact_types.ErrArtifactNotExist) { | ||
utils.ArtifactNotFound(w, name, err) | ||
return | ||
} else { | ||
utils.InternalServerError(w, err) | ||
return | ||
} | ||
} | ||
|
||
utils.WriteResponse(w, http.StatusOK, report) | ||
} | ||
|
||
func ListArtifact(w http.ResponseWriter, r *http.Request) { | ||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) | ||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime} | ||
|
||
artifacts, err := imageEngine.ArtifactList(r.Context(), entities.ArtifactListOptions{}) | ||
if err != nil { | ||
utils.InternalServerError(w, err) | ||
return | ||
} | ||
|
||
utils.WriteResponse(w, http.StatusOK, artifacts) | ||
} | ||
|
||
func PullArtifact(w http.ResponseWriter, r *http.Request) { | ||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) | ||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) | ||
|
||
query := struct { | ||
Name string `schema:"name"` | ||
Retry uint `schema:"retry"` | ||
RetryDelay string `schema:"retryDelay"` | ||
TLSVerify types.OptionalBool `schema:"tlsVerify"` | ||
}{} | ||
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil { | ||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) | ||
return | ||
} | ||
|
||
if query.Name == "" { | ||
utils.Error(w, http.StatusBadRequest, errors.New("name parameter is required")) | ||
return | ||
} | ||
|
||
artifactsPullOptions := entities.ArtifactPullOptions{} | ||
|
||
// If TLS verification is explicitly specified (True or False) in the query, | ||
// set the InsecureSkipTLSVerify option accordingly. | ||
// If TLSVerify was not set in the query, OptionalBoolUndefined is used and | ||
// handled later based off the target registry configuration. | ||
switch query.TLSVerify { | ||
case types.OptionalBoolTrue: | ||
artifactsPullOptions.InsecureSkipTLSVerify = types.NewOptionalBool(false) | ||
case types.OptionalBoolFalse: | ||
artifactsPullOptions.InsecureSkipTLSVerify = types.NewOptionalBool(true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should there be a fall through and something like an error here? |
||
case types.OptionalBoolUndefined: | ||
// If the user doesn't define TLSVerify in the query, do nothing and pass | ||
// it to the backend code to handle. | ||
default: // Should never happen | ||
panic("Unexpected handling occurred for TLSVerify") | ||
} | ||
|
||
if _, found := r.URL.Query()["retry"]; found { | ||
artifactsPullOptions.MaxRetries = &query.Retry | ||
} | ||
|
||
if len(query.RetryDelay) != 0 { | ||
artifactsPullOptions.RetryDelay = query.RetryDelay | ||
} | ||
|
||
authConf, authfile, err := auth.GetCredentials(r) | ||
if err != nil { | ||
utils.Error(w, http.StatusBadRequest, err) | ||
return | ||
} | ||
defer auth.RemoveAuthfile(authfile) | ||
|
||
artifactsPullOptions.AuthFilePath = authfile | ||
if authConf != nil { | ||
artifactsPullOptions.Username = authConf.Username | ||
artifactsPullOptions.Password = authConf.Password | ||
artifactsPullOptions.IdentityToken = authConf.IdentityToken | ||
} | ||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime} | ||
|
||
artifacts, err := imageEngine.ArtifactPull(r.Context(), query.Name, artifactsPullOptions) | ||
if err != nil { | ||
var errcd errcode.ErrorCoder | ||
// Check to see if any of the wrapped errors is an errcode.ErrorCoder returned from the registry | ||
if errors.As(err, &errcd) { | ||
rc := errcd.ErrorCode().Descriptor().HTTPStatusCode | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would love // comments in here as to these conditions. maybe im on the only like that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I will add comments, it's not super clear. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WDYT about using named constants like Paying more attention to what is going on here, one thing to consider is that
Now, that puts us in a somewhat awkward position in that we either keep using an obsolete and unmaintained package, or break (an undocumented aspect of an) API. I don’t know what we are going to do here longer-term; one thing I’d recommend doing right now is checking for (Pedantically, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mtrmac Great call the on the named constants, much more readable. Regarding using I used the [1]
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My mistake, The quoted error seems to be when the user did not try to log in at all, it’s just the repo’s access that is refused (and the same error is returned by Quay when the repo outright does not exist[1]). So, that’s a different case, and currently needs to be determined by looking for the ([1] Given this ambiguity, Podman might want to be careful about promising to return specific error types for “not found” vs. “not authorized” from its API, because various registries intentionally conflate the two. I suppose doing the same thing that the “image pull” code does is, by definition, acceptable … but that code, AFAICS, doesn’t make (or at least doesn’t fulfill) such a promise.) |
||
// Check if the returned error is 401 StatusUnauthorized indicating the request was unauthorized | ||
if rc == http.StatusUnauthorized { | ||
utils.Error(w, http.StatusUnauthorized, errcd.ErrorCode()) | ||
return | ||
} | ||
// Check if the returned error is 404 StatusNotFound indicating the artifact was not found | ||
if rc == http.StatusNotFound { | ||
utils.Error(w, http.StatusNotFound, errcd.ErrorCode()) | ||
return | ||
} | ||
} | ||
utils.InternalServerError(w, err) | ||
return | ||
} | ||
|
||
utils.WriteResponse(w, http.StatusOK, artifacts) | ||
} | ||
|
||
func RemoveArtifact(w http.ResponseWriter, r *http.Request) { | ||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) | ||
imageEngine := abi.ImageEngine{Libpod: runtime} | ||
|
||
name := utils.GetName(r) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably worth a dedicated error for "name was not provided" if name == "" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that is needed here because name is provided on the path |
||
|
||
artifacts, err := imageEngine.ArtifactRm(r.Context(), name, entities.ArtifactRemoveOptions{}) | ||
if err != nil { | ||
if errors.Is(err, libartifact_types.ErrArtifactNotExist) { | ||
utils.ArtifactNotFound(w, name, err) | ||
return | ||
} | ||
utils.InternalServerError(w, err) | ||
return | ||
} | ||
|
||
utils.WriteResponse(w, http.StatusOK, artifacts) | ||
} | ||
|
||
func AddArtifact(w http.ResponseWriter, r *http.Request) { | ||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) | ||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) | ||
|
||
query := struct { | ||
Name string `schema:"name"` | ||
FileName string `schema:"fileName"` | ||
FileMIMEType string `schema:"fileMIMEType"` | ||
Annotations []string `schema:"annotations"` | ||
ArtifactMIMEType string `schema:"artifactMIMEType"` | ||
Append bool `schema:"append"` | ||
}{} | ||
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil { | ||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) | ||
return | ||
} | ||
|
||
if query.Name == "" || query.FileName == "" { | ||
utils.Error(w, http.StatusBadRequest, errors.New("name and file parameters are required")) | ||
return | ||
} | ||
|
||
annotations, err := domain_utils.ParseAnnotations(query.Annotations) | ||
if err != nil { | ||
utils.Error(w, http.StatusBadRequest, err) | ||
return | ||
} | ||
|
||
artifactAddOptions := &entities.ArtifactAddOptions{ | ||
Append: query.Append, | ||
Annotations: annotations, | ||
ArtifactType: query.ArtifactMIMEType, | ||
FileType: query.FileMIMEType, | ||
} | ||
|
||
artifactBlobs := []entities.ArtifactBlob{{ | ||
BlobReader: r.Body, | ||
FileName: query.FileName, | ||
}} | ||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime} | ||
|
||
artifacts, err := imageEngine.ArtifactAdd(r.Context(), query.Name, artifactBlobs, artifactAddOptions) | ||
if err != nil { | ||
if errors.Is(err, libartifact_types.ErrArtifactNotExist) { | ||
utils.ArtifactNotFound(w, query.Name, err) | ||
return | ||
} | ||
utils.InternalServerError(w, err) | ||
return | ||
} | ||
|
||
utils.WriteResponse(w, http.StatusCreated, artifacts) | ||
} | ||
|
||
func PushArtifact(w http.ResponseWriter, r *http.Request) { | ||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) | ||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) | ||
|
||
query := struct { | ||
Retry uint `schema:"retry"` | ||
RetryDelay string `schema:"retrydelay"` | ||
TLSVerify types.OptionalBool `schema:"tlsVerify"` | ||
}{} | ||
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil { | ||
utils.Error(w, http.StatusBadRequest, errors.New("name parameter is required")) | ||
return | ||
} | ||
|
||
name := utils.GetName(r) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could the name be part of the query structure? As in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm happy to change it, the reason I did it like these is just to mimic the image pull and push endpoints. This might be something we can discuss on the Change Request document? |
||
|
||
artifactsPushOptions := entities.ArtifactPushOptions{} | ||
|
||
// If TLS verification is explicitly specified (True or False) in the query, | ||
// set the SkipTLSVerify option accordingly. | ||
// If TLSVerify was not set in the query, OptionalBoolUndefined is used and | ||
// handled later based off the target registry configuration. | ||
switch query.TLSVerify { | ||
case types.OptionalBoolTrue: | ||
artifactsPushOptions.SkipTLSVerify = types.NewOptionalBool(false) | ||
case types.OptionalBoolFalse: | ||
artifactsPushOptions.SkipTLSVerify = types.NewOptionalBool(true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need a fallthrough? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe? query.TLSVerify can only be OptionalBoolTrue, OptionalBoolFalse or OptionalBoolUndefined. We handle the first two because we have to switch the logic and we pass OptionalBoolUndefined to be handled by the backend code as suggested by Paul: #26111 (comment) I looked into this some more and it's quite confusing where the default would be defined in the backend code. If you would like I can add a switch for OptionalBoolUndefined and have it set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason we must pass undefined in the backed is because registries.conf allows user to specify insecure option on a per registry basis, for example in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, undefined must map to undefined, as Paul says. Ideally, I’d like there to be a Compare #26111 (comment) . That’s probably out of scope of this PR, or at least the “centralized logic” part is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks folks, I added a default case that will fall back to enabling TLS if unexpected handling occurred. |
||
case types.OptionalBoolUndefined: | ||
// If the user doesn't define TLSVerify in the query, do nothing and pass | ||
// it to the backend code to handle. | ||
default: // Should never happen | ||
panic("Unexpected handling occurred for TLSVerify") | ||
} | ||
|
||
if _, found := r.URL.Query()["retry"]; found { | ||
artifactsPushOptions.Retry = &query.Retry | ||
} | ||
|
||
if len(query.RetryDelay) != 0 { | ||
artifactsPushOptions.RetryDelay = query.RetryDelay | ||
} | ||
|
||
authConf, authfile, err := auth.GetCredentials(r) | ||
if err != nil { | ||
utils.Error(w, http.StatusBadRequest, err) | ||
return | ||
} | ||
defer auth.RemoveAuthfile(authfile) | ||
|
||
if authConf != nil { | ||
artifactsPushOptions.Username = authConf.Username | ||
artifactsPushOptions.Password = authConf.Password | ||
} | ||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime} | ||
|
||
artifacts, err := imageEngine.ArtifactPush(r.Context(), name, artifactsPushOptions) | ||
if err != nil { | ||
var errcd errcode.ErrorCoder | ||
// Check to see if any of the wrapped errors is an errcode.ErrorCoder returned from the registry | ||
if errors.As(err, &errcd) { | ||
rc := errcd.ErrorCode().Descriptor().HTTPStatusCode | ||
// Check if the returned error is 401 indicating the request was unauthorized | ||
if rc == 401 { | ||
utils.Error(w, 401, errcd.ErrorCode()) | ||
return | ||
} | ||
} | ||
|
||
var notFoundErr layout.ImageNotFoundError | ||
if errors.As(err, ¬FoundErr) { | ||
utils.ArtifactNotFound(w, name, notFoundErr) | ||
return | ||
} | ||
|
||
utils.InternalServerError(w, err) | ||
return | ||
} | ||
|
||
utils.WriteResponse(w, http.StatusOK, artifacts) | ||
} | ||
|
||
func ExtractArtifact(w http.ResponseWriter, r *http.Request) { | ||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) | ||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) | ||
|
||
query := struct { | ||
Digest string `schema:"digest"` | ||
Title string `schema:"title"` | ||
}{} | ||
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil { | ||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) | ||
return | ||
} | ||
|
||
extractOpts := entities.ArtifactExtractOptions{ | ||
Title: query.Title, | ||
Digest: query.Digest, | ||
} | ||
|
||
name := utils.GetName(r) | ||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime} | ||
|
||
err := imageEngine.ArtifactExtractTarStream(r.Context(), w, name, &extractOpts) | ||
if err != nil { | ||
if errors.Is(err, libartifact_types.ErrArtifactNotExist) { | ||
utils.ArtifactNotFound(w, name, err) | ||
return | ||
} | ||
utils.InternalServerError(w, err) | ||
return | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.