mirror of
https://github.com/quay/quay.git
synced 2026-01-26 06:21:37 +03:00
* storage: Disable pushes on registry (PROJQUAY-6870)
The current read-only option for Quay is not sometimes feasible, since it requires an insert of the service key and other manual config changes. For instance, if you want to just recalculate quota on the registry, but would like to allow all registry operations (including UI) without the possibility of pushes until recalculation is done, setting the whole registry `read-only` cannot be done since it makes the database read only as well.
This PR introduces a new flag called `DISABLE_PUSHES` which allows all registry operations to continue (changing tags, repo editing, robot account creation/deletion, user creation etc.) but will disable pushes of new images to the registry (i.e. backend storage will not change). If a registry already contains the image and a new tag is simply being added, that operation should succeed.
The following message would appear in the logs:
~~~
gunicorn-registry stdout | 2024-03-13 20:19:49,414 [369] [DEBUG] [endpoints.v2] sending response: b'{"errors":[{"code":"METHOD NOT ALLOWED","detail":{},"message":"Pushes to the registry are currently disabled. Please contact the administrator for more information."}]}\n'
gunicorn-registry stdout | 2024-03-13 20:19:49,414 [369] [INFO] [gunicorn.access] 172.17.0.1 - - [13/Mar/2024:20:19:49 +0000] "PUT /v2/ibazulic/mariadb/manifests/sha256:c4694ba424e0259694a5117bbb510d67340051f0bdb7f9fa8033941a2d66e53e HTTP/1.1" 405 169 "-" "skopeo/1.9.3"
nginx stdout | 172.17.0.1 (-) - - [13/Mar/2024:20:19:49 +0000] "PUT /v2/ibazulic/mariadb/manifests/sha256:c4694ba424e0259694a5117bbb510d67340051f0bdb7f9fa8033941a2d66e53e HTTP/1.1" 405 169 "-" "skopeo/1.9.3" (0.002 3813 0.002)
~~~
The flag defaults to `False` (pushes enabled), unless set otherwise.
* Removed constraint on storage replication when pushes are disabled
* Rebase
* Fix isort sorting
* Fix isort sorting #2
* Removed constraint on storage replication when pushes are disabled
* Rebase
* Remove constraint on storage replication worker
* Fix linting on config.py
227 lines
7.2 KiB
Python
227 lines
7.2 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 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)
|