mirror of
https://github.com/quay/quay.git
synced 2026-01-26 06:21:37 +03:00
Expose tag immutability through the existing tag REST API endpoint.
This adds:
- immutable field to PUT /api/v1/repository/{repo}/tag/{tag}
- TagImmutable 409 exception for blocked operations
- immutable field in tag list responses
- Exception handling for DELETE and PUT on immutable tags
Write permission required to lock, admin required to unlock.
Signed-off-by: Brady Pratt <bpratt@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
234 lines
7.4 KiB
Python
234 lines
7.4 KiB
Python
import bitmath
|
|
|
|
|
|
class V2RegistryException(Exception):
|
|
def __init__(
|
|
self,
|
|
error_code_str,
|
|
message,
|
|
detail,
|
|
http_status_code=400,
|
|
repository=None,
|
|
scopes=None,
|
|
is_read_only=False,
|
|
):
|
|
super(V2RegistryException, self).__init__(message)
|
|
self.http_status_code = http_status_code
|
|
self.repository = repository
|
|
self.scopes = scopes
|
|
self.is_read_only = is_read_only
|
|
|
|
self._error_code_str = error_code_str
|
|
self._detail = detail
|
|
|
|
def as_dict(self):
|
|
error_dict = {
|
|
"code": self._error_code_str,
|
|
"message": str(self),
|
|
"detail": self._detail if self._detail is not None else {},
|
|
}
|
|
|
|
if self.is_read_only:
|
|
error_dict["is_readonly"] = True
|
|
|
|
return error_dict
|
|
|
|
|
|
class BlobUnknown(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(BlobUnknown, self).__init__("BLOB_UNKNOWN", "blob unknown to registry", detail, 404)
|
|
|
|
|
|
class BlobUploadInvalid(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(BlobUploadInvalid, self).__init__(
|
|
"BLOB_UPLOAD_INVALID", "blob upload invalid", detail
|
|
)
|
|
|
|
|
|
class BlobUploadUnknown(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(BlobUploadUnknown, self).__init__(
|
|
"BLOB_UPLOAD_UNKNOWN", "blob upload unknown to registry", detail, 404
|
|
)
|
|
|
|
|
|
class DigestInvalid(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(DigestInvalid, self).__init__(
|
|
"DIGEST_INVALID", "provided digest did not match uploaded content", detail
|
|
)
|
|
|
|
|
|
class ManifestBlobUnknown(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(ManifestBlobUnknown, self).__init__(
|
|
"MANIFEST_BLOB_UNKNOWN", "manifest blob unknown to registry", detail
|
|
)
|
|
|
|
|
|
class ManifestInvalid(V2RegistryException):
|
|
def __init__(self, detail=None, http_status_code=400):
|
|
super(ManifestInvalid, self).__init__(
|
|
"MANIFEST_INVALID", "manifest invalid", detail, http_status_code
|
|
)
|
|
|
|
|
|
class ManifestUnknown(V2RegistryException):
|
|
def __init__(self, message=None, detail=None):
|
|
super(ManifestUnknown, self).__init__(
|
|
"MANIFEST_UNKNOWN", message or "manifest unknown", detail, 404
|
|
)
|
|
|
|
|
|
class TagExpired(V2RegistryException):
|
|
def __init__(self, message=None, detail=None):
|
|
super(TagExpired, self).__init__("TAG_EXPIRED", message or "Tag has expired", detail, 404)
|
|
|
|
|
|
class ManifestUnverified(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(ManifestUnverified, self).__init__(
|
|
"MANIFEST_UNVERIFIED", "manifest failed signature verification", detail
|
|
)
|
|
|
|
|
|
class NameInvalid(V2RegistryException):
|
|
def __init__(self, detail=None, message=None):
|
|
super(NameInvalid, self).__init__(
|
|
"NAME_INVALID", message or "invalid repository name", detail
|
|
)
|
|
|
|
|
|
class NameUnknown(V2RegistryException):
|
|
def __init__(self, message, detail=None):
|
|
super(NameUnknown, self).__init__(
|
|
"NAME_UNKNOWN", message or "repository name not known to registry", detail, 404
|
|
)
|
|
|
|
|
|
class SizeInvalid(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(SizeInvalid, self).__init__(
|
|
"SIZE_INVALID", "provided length did not match content length", detail
|
|
)
|
|
|
|
|
|
class TagAlreadyExists(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(TagAlreadyExists, self).__init__(
|
|
"TAG_ALREADY_EXISTS", "tag was already pushed", detail, 409
|
|
)
|
|
|
|
|
|
class TagImmutable(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(TagImmutable, self).__init__(
|
|
"TAG_IMMUTABLE", "tag is immutable and cannot be overwritten", detail, 409
|
|
)
|
|
|
|
|
|
class TagInvalid(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
super(TagInvalid, self).__init__("TAG_INVALID", "manifest tag did not match URI", detail)
|
|
|
|
|
|
class TooManyTagsRequested(V2RegistryException):
|
|
def __init__(self, message, detail=None):
|
|
super(TooManyTagsRequested, self).__init__(
|
|
"TOO_MANY_TAGS_REQUESTED", message or "too many tags requested", detail, 413
|
|
)
|
|
|
|
|
|
class LayerTooLarge(V2RegistryException):
|
|
def __init__(self, uploaded=None, max_allowed=None):
|
|
detail = {}
|
|
message = "Uploaded blob is larger than allowed by this registry"
|
|
|
|
if uploaded is not None and max_allowed is not None:
|
|
detail = {
|
|
"reason": "%s is greater than maximum allowed size %s" % (uploaded, max_allowed),
|
|
"max_allowed": max_allowed,
|
|
"uploaded": uploaded,
|
|
}
|
|
|
|
up_str = bitmath.Byte(uploaded).best_prefix().format("{value:.2f} {unit}")
|
|
max_str = bitmath.Byte(max_allowed).best_prefix().format("{value:.2f} {unit}")
|
|
message = "Uploaded blob of %s is larger than %s allowed by this registry" % (
|
|
up_str,
|
|
max_str,
|
|
)
|
|
|
|
|
|
class QuotaExceeded(V2RegistryException):
|
|
def __init__(self):
|
|
super(QuotaExceeded, self).__init__(
|
|
"DENIED", "Quota has been exceeded on namespace", {}, 403
|
|
)
|
|
|
|
|
|
class Unauthorized(V2RegistryException):
|
|
def __init__(self, detail=None, repository=None, scopes=None):
|
|
super(Unauthorized, self).__init__(
|
|
"UNAUTHORIZED",
|
|
"access to the requested resource is not authorized",
|
|
detail,
|
|
401,
|
|
repository=repository,
|
|
scopes=scopes,
|
|
)
|
|
|
|
|
|
class Unsupported(V2RegistryException):
|
|
def __init__(self, detail=None, message=None):
|
|
super(Unsupported, self).__init__(
|
|
"UNSUPPORTED", message or "The operation is unsupported.", detail, 405
|
|
)
|
|
|
|
|
|
class InvalidLogin(V2RegistryException):
|
|
def __init__(self, message=None):
|
|
super(InvalidLogin, self).__init__(
|
|
"UNAUTHORIZED", message or "Specified credentials are invalid", {}, 401
|
|
)
|
|
|
|
|
|
class InvalidRequest(V2RegistryException):
|
|
def __init__(self, message=None):
|
|
super(InvalidRequest, self).__init__(
|
|
"INVALID_REQUEST", message or "Invalid request", {}, 400
|
|
)
|
|
|
|
|
|
class NamespaceDisabled(V2RegistryException):
|
|
def __init__(self, message=None):
|
|
message = message or "This namespace is disabled. Please contact your system administrator."
|
|
super(NamespaceDisabled, self).__init__("DENIED", message, {}, 405)
|
|
|
|
|
|
class BlobDownloadGeoBlocked(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
message = (
|
|
"The region from which you are pulling has been geo-ip blocked. "
|
|
+ "Please contact the namespace owner."
|
|
)
|
|
super(BlobDownloadGeoBlocked, self).__init__("DENIED", message, detail, 403)
|
|
|
|
|
|
class ReadOnlyMode(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
message = (
|
|
"System is currently read-only. Pulls will succeed but all write operations "
|
|
+ "are currently suspended."
|
|
)
|
|
super(ReadOnlyMode, self).__init__("DENIED", message, detail, 405, is_read_only=True)
|
|
|
|
|
|
class PushesDisabled(V2RegistryException):
|
|
def __init__(self, detail=None):
|
|
message = (
|
|
"Pushes to the registry are currently disabled. Please contact"
|
|
+ " the administrator for more information."
|
|
)
|
|
super(PushesDisabled, self).__init__("METHOD NOT ALLOWED", message, {}, 405)
|