Skip to content

Commit 9b1803c

Browse files
author
Yalin Li
authored
[ACR] Support set/get non-oci manifest (Azure#29552)
1 parent 8bc6dd0 commit 9b1803c

25 files changed

+1185
-368
lines changed

.vscode/cspell.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,10 @@
610610
{
611611
"filename": "sdk/containerregistry/azure-containerregistry/**",
612612
"words": [
613-
"udpated"
613+
"udpated",
614+
"cncf",
615+
"oras",
616+
"rootfs"
614617
]
615618
},
616619
{

sdk/containerregistry/azure-containerregistry/CHANGELOG.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# Release History
22

3-
## 1.1.0b4 (Unreleased)
3+
## 1.1.0b4 (2023-04-25)
44

55
### Features Added
6+
- Added an optional kwarg `media_type` in `set_manifest()` to enable uploading image manifests of any type.
67

78
### Breaking Changes
8-
9-
### Bugs Fixed
9+
- Renamed `upload_manifest()` to `set_manifest()`, and changed to consume manifest in `JSON` instead of `OCIManifest` type.
10+
- Renamed `download_manifest()` to `get_manifest()`, and changed it's return type from `DownloadManifestResult` to `GetManifestResult`.
1011

1112
### Other Changes
12-
- Changed the default audience to "https://containerregistry.azure.net" which works for all clouds.
13+
- Changed the default audience to `"https://containerregistry.azure.net"` which works for all clouds.
1314

1415
## 1.1.0b3 (2023-04-04)
1516

@@ -65,7 +66,7 @@
6566
### Features Added
6667

6768
- Updated the supported rest api version to be the stable "2021-07-01".
68-
- Removed the property `teleport_enabled` in `RepositoryProperties`.
69+
- Removed the property `teleport_enabled` in `RepositoryProperties`.
6970

7071
## 1.0.0b6 (2021-09-08)
7172

sdk/containerregistry/azure-containerregistry/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/containerregistry/azure-containerregistry",
5-
"Tag": "python/containerregistry/azure-containerregistry_fbbcf05c71"
5+
"Tag": "python/containerregistry/azure-containerregistry_7f3bea3b69"
66
}

sdk/containerregistry/azure-containerregistry/azure/containerregistry/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
ArtifactManifestProperties,
1818
RepositoryProperties,
1919
ArtifactTagProperties,
20-
DownloadManifestResult,
20+
GetManifestResult,
2121
)
2222
from ._download_stream import DownloadBlobStream
2323
from ._version import VERSION
@@ -33,6 +33,6 @@
3333
"RepositoryProperties",
3434
"ArtifactTagOrder",
3535
"ArtifactTagProperties",
36-
"DownloadManifestResult",
36+
"GetManifestResult",
3737
"DownloadBlobStream",
3838
]

sdk/containerregistry/azure-containerregistry/azure/containerregistry/_container_registry_client.py

Lines changed: 70 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
# pylint: disable=too-many-lines
77
import functools
88
import hashlib
9+
import json
910
from io import BytesIO
10-
from typing import Any, Dict, IO, Optional, overload, Union, cast, Tuple
11+
from typing import Any, Dict, IO, Optional, overload, Union, cast, Tuple, MutableMapping
1112

1213
from azure.core.credentials import TokenCredential
1314
from azure.core.exceptions import (
@@ -22,28 +23,30 @@
2223
from azure.core.tracing.decorator import distributed_trace
2324

2425
from ._base_client import ContainerRegistryBaseClient
25-
from ._generated.models import AcrErrors, OCIManifest, ManifestWrapper
26+
from ._generated.models import AcrErrors
2627
from ._download_stream import DownloadBlobStream
2728
from ._helpers import (
2829
_compute_digest,
2930
_is_tag,
3031
_parse_next_link,
31-
_serialize_manifest,
3232
_validate_digest,
33-
OCI_MANIFEST_MEDIA_TYPE,
3433
SUPPORTED_API_VERSIONS,
34+
OCI_IMAGE_MANIFEST,
35+
SUPPORTED_MANIFEST_MEDIA_TYPES,
3536
DEFAULT_AUDIENCE,
3637
DEFAULT_CHUNK_SIZE,
3738
)
3839
from ._models import (
3940
RepositoryProperties,
4041
ArtifactTagProperties,
4142
ArtifactManifestProperties,
42-
DownloadManifestResult,
43+
GetManifestResult,
4344
)
4445

45-
def _return_response_and_deserialized(pipeline_response, deserialized, _):
46-
return pipeline_response, deserialized
46+
JSON = MutableMapping[str, Any]
47+
48+
def _return_response(pipeline_response, _, __):
49+
return pipeline_response
4750

4851
def _return_response_and_headers(pipeline_response, _, response_headers):
4952
return pipeline_response, response_headers
@@ -859,24 +862,35 @@ def update_repository_properties(
859862
)
860863

861864
@distributed_trace
862-
def upload_manifest(
863-
self, repository: str, manifest: Union[OCIManifest, IO], *, tag: Optional[str] = None, **kwargs
865+
def set_manifest(
866+
self,
867+
repository: str,
868+
manifest: Union[JSON, IO[bytes]],
869+
*,
870+
tag: Optional[str] = None,
871+
media_type: str = OCI_IMAGE_MANIFEST,
872+
**kwargs
864873
) -> str:
865-
"""Upload a manifest for an OCI artifact.
874+
"""Set a manifest for an artifact.
866875
867-
:param str repository: Name of the repository.
868-
:param manifest: The manifest to upload. Note: This must be a seekable stream.
869-
:type manifest: ~azure.containerregistry.models.OCIManifest or IO
876+
:param str repository: Name of the repository
877+
:param manifest: The manifest to set. It can be a JSON formatted dict or seekable stream.
878+
:type manifest: dict or IO
870879
:keyword tag: Tag of the manifest.
871880
:paramtype tag: str or None
872-
:returns: The digest of the uploaded manifest, calculated by the registry.
881+
:keyword media_type: The media type of the manifest. If not specified, this value will be set to
882+
a default value of "application/vnd.oci.image.manifest.v1+json". Note: the current known media types are:
883+
"application/vnd.oci.image.manifest.v1+json", and "application/vnd.docker.distribution.manifest.v2+json".
884+
:paramtype media_type: str
885+
:returns: The digest of the set manifest, calculated by the registry.
873886
:rtype: str
874887
:raises ValueError: If the parameter repository or manifest is None,
875-
or the digest in the response does not match the digest of the uploaded manifest.
888+
or the digest in the response does not match the digest of the set manifest.
876889
"""
877890
try:
878-
if isinstance(manifest, OCIManifest):
879-
data = _serialize_manifest(manifest)
891+
data: IO[bytes]
892+
if isinstance(manifest, MutableMapping):
893+
data = BytesIO(json.dumps(manifest).encode())
880894
else:
881895
data = manifest
882896
tag_or_digest = tag
@@ -887,20 +901,55 @@ def upload_manifest(
887901
name=repository,
888902
reference=tag_or_digest,
889903
payload=data,
890-
content_type=OCI_MANIFEST_MEDIA_TYPE,
891-
headers={"Accept": OCI_MANIFEST_MEDIA_TYPE},
904+
content_type=media_type,
892905
cls=_return_response_headers,
893906
**kwargs
894907
)
895908
digest = response_headers['Docker-Content-Digest']
896909
if not _validate_digest(data, digest):
897-
raise ValueError("The digest in the response does not match the digest of the uploaded manifest.")
910+
raise ValueError("The server-computed digest does not match the client-computed digest.")
898911
except Exception as e:
899912
if repository is None or manifest is None:
900913
raise ValueError("The parameter repository and manifest cannot be None.") from e
901914
raise
902915
return digest
903916

917+
@distributed_trace
918+
def get_manifest(self, repository: str, tag_or_digest: str, **kwargs) -> GetManifestResult:
919+
"""Get the manifest for an artifact.
920+
921+
:param str repository: Name of the repository.
922+
:param str tag_or_digest: The tag or digest of the manifest to get.
923+
When digest is provided, will use this digest to compare with the one calculated by the response payload.
924+
When tag is provided, will use the digest in response headers to compare.
925+
:returns: GetManifestResult
926+
:rtype: ~azure.containerregistry.GetManifestResult
927+
:raises ValueError: If the requested digest does not match the digest of the received manifest.
928+
"""
929+
response = cast(
930+
PipelineResponse,
931+
self._client.container_registry.get_manifest(
932+
name=repository,
933+
reference=tag_or_digest,
934+
accept=SUPPORTED_MANIFEST_MEDIA_TYPES,
935+
cls=_return_response,
936+
**kwargs
937+
)
938+
)
939+
media_type = response.http_response.headers['Content-Type']
940+
manifest_bytes = response.http_response.read()
941+
manifest_json = response.http_response.json()
942+
if tag_or_digest.startswith("sha256:"):
943+
digest = tag_or_digest
944+
if not _validate_digest(manifest_bytes, digest):
945+
raise ValueError("The requested digest does not match the digest of the received manifest.")
946+
else:
947+
digest = response.http_response.headers['Docker-Content-Digest']
948+
if not _validate_digest(manifest_bytes, digest):
949+
raise ValueError("The server-computed digest does not match the client-computed digest.")
950+
951+
return GetManifestResult(digest=digest, manifest=manifest_json, media_type=media_type)
952+
904953
@distributed_trace
905954
def upload_blob(self, repository: str, data: IO[bytes], **kwargs) -> Tuple[str, int]:
906955
"""Upload an artifact blob.
@@ -949,40 +998,7 @@ def _upload_blob_chunk(self, location: str, data: IO[bytes], **kwargs) -> Tuple[
949998
hasher.update(buffer)
950999
buffer = data.read(DEFAULT_CHUNK_SIZE)
9511000
blob_size += len(buffer)
952-
return "sha256:" + hasher.hexdigest(), location, blob_size
953-
954-
@distributed_trace
955-
def download_manifest(self, repository: str, tag_or_digest: str, **kwargs) -> DownloadManifestResult:
956-
"""Download the manifest for an OCI artifact.
957-
958-
:param str repository: Name of the repository.
959-
:param str tag_or_digest: The tag or digest of the manifest to download.
960-
When digest is provided, will use this digest to compare with the one calculated by the response payload.
961-
When tag is provided, will use the digest in response headers to compare.
962-
:returns: DownloadManifestResult
963-
:rtype: ~azure.containerregistry.DownloadManifestResult
964-
:raises ValueError: If the requested digest does not match the digest of the received manifest.
965-
"""
966-
response, manifest_wrapper = cast(
967-
Tuple[PipelineResponse, ManifestWrapper],
968-
self._client.container_registry.get_manifest(
969-
name=repository,
970-
reference=tag_or_digest,
971-
headers={"Accept": OCI_MANIFEST_MEDIA_TYPE},
972-
cls=_return_response_and_deserialized,
973-
**kwargs
974-
)
975-
)
976-
manifest = OCIManifest.deserialize(cast(ManifestWrapper, manifest_wrapper).serialize())
977-
manifest_stream = _serialize_manifest(manifest)
978-
if tag_or_digest.startswith("sha256:"):
979-
digest = tag_or_digest
980-
else:
981-
digest = response.http_response.headers['Docker-Content-Digest']
982-
if not _validate_digest(manifest_stream, digest):
983-
raise ValueError("The requested digest does not match the digest of the received manifest.")
984-
985-
return DownloadManifestResult(digest=digest, data=manifest_stream, manifest=manifest)
1001+
return f"sha256:{hasher.hexdigest()}", location, blob_size
9861002

9871003
@distributed_trace
9881004
def download_blob(self, repository: str, digest: str, **kwargs) -> DownloadBlobStream:

sdk/containerregistry/azure-containerregistry/azure/containerregistry/_generated/aio/operations/_operations.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,21 @@ async def check_docker_v2_support(self, **kwargs: Any) -> None: # pylint: disab
124124
return cls(pipeline_response, None, {})
125125

126126
@distributed_trace_async
127-
async def get_manifest(self, name: str, reference: str, **kwargs: Any) -> _models.ManifestWrapper:
127+
async def get_manifest(
128+
self, name: str, reference: str, *, accept: Optional[str] = None, **kwargs: Any
129+
) -> AsyncIterator[bytes]:
128130
"""Get the manifest identified by ``name`` and ``reference`` where ``reference`` can be a tag or
129131
digest.
130132
131133
:param name: Name of the image (including the namespace). Required.
132134
:type name: str
133135
:param reference: A tag or a digest, pointing to a specific image. Required.
134136
:type reference: str
135-
:return: ManifestWrapper
136-
:rtype: ~container_registry.models.ManifestWrapper
137+
:keyword accept: Accept header string delimited by comma. For example,
138+
application/vnd.docker.distribution.manifest.v2+json. Default value is None.
139+
:paramtype accept: str
140+
:return: Async iterator of the response bytes
141+
:rtype: AsyncIterator[bytes]
137142
:raises ~azure.core.exceptions.HttpResponseError:
138143
"""
139144
error_map = {
@@ -147,11 +152,12 @@ async def get_manifest(self, name: str, reference: str, **kwargs: Any) -> _model
147152
_headers = kwargs.pop("headers", {}) or {}
148153
_params = kwargs.pop("params", {}) or {}
149154

150-
cls: ClsType[_models.ManifestWrapper] = kwargs.pop("cls", None)
155+
cls: ClsType[AsyncIterator[bytes]] = kwargs.pop("cls", None)
151156

152157
request = build_container_registry_get_manifest_request(
153158
name=name,
154159
reference=reference,
160+
accept=accept,
155161
headers=_headers,
156162
params=_params,
157163
)
@@ -160,7 +166,7 @@ async def get_manifest(self, name: str, reference: str, **kwargs: Any) -> _model
160166
}
161167
request.url = self._client.format_url(request.url, **path_format_arguments)
162168

163-
_stream = False
169+
_stream = True
164170
pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access
165171
request, stream=_stream, **kwargs
166172
)
@@ -172,12 +178,12 @@ async def get_manifest(self, name: str, reference: str, **kwargs: Any) -> _model
172178
error = self._deserialize.failsafe_deserialize(_models.AcrErrors, pipeline_response)
173179
raise HttpResponseError(response=response, model=error)
174180

175-
deserialized = self._deserialize("ManifestWrapper", pipeline_response)
181+
deserialized = response.iter_bytes()
176182

177183
if cls:
178-
return cls(pipeline_response, deserialized, {})
184+
return cls(pipeline_response, deserialized, {}) # type: ignore
179185

180-
return deserialized
186+
return deserialized # type: ignore
181187

182188
@distributed_trace_async
183189
async def create_manifest(self, name: str, reference: str, payload: IO, **kwargs: Any) -> Any:

sdk/containerregistry/azure-containerregistry/azure/containerregistry/_generated/models/_models.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,50 +1081,49 @@ def __init__(
10811081
self.annotations = annotations
10821082

10831083

1084-
class OCIManifest(_serialization.Model):
1084+
class OCIManifest(Manifest):
10851085
"""Returns the requested OCI Manifest file.
10861086
1087+
:ivar schema_version: Schema version.
1088+
:vartype schema_version: int
10871089
:ivar config: V2 image config descriptor.
10881090
:vartype config: ~container_registry.models.Descriptor
10891091
:ivar layers: List of V2 image layer information.
10901092
:vartype layers: list[~container_registry.models.Descriptor]
10911093
:ivar annotations: Additional information provided through arbitrary metadata.
10921094
:vartype annotations: ~container_registry.models.Annotations
1093-
:ivar schema_version: Schema version.
1094-
:vartype schema_version: int
10951095
"""
10961096

10971097
_attribute_map = {
1098+
"schema_version": {"key": "schemaVersion", "type": "int"},
10981099
"config": {"key": "config", "type": "Descriptor"},
10991100
"layers": {"key": "layers", "type": "[Descriptor]"},
11001101
"annotations": {"key": "annotations", "type": "Annotations"},
1101-
"schema_version": {"key": "schemaVersion", "type": "int"},
11021102
}
11031103

11041104
def __init__(
11051105
self,
11061106
*,
1107+
schema_version: Optional[int] = None,
11071108
config: Optional["_models.Descriptor"] = None,
11081109
layers: Optional[List["_models.Descriptor"]] = None,
11091110
annotations: Optional["_models.Annotations"] = None,
1110-
schema_version: Optional[int] = None,
11111111
**kwargs: Any
11121112
) -> None:
11131113
"""
1114+
:keyword schema_version: Schema version.
1115+
:paramtype schema_version: int
11141116
:keyword config: V2 image config descriptor.
11151117
:paramtype config: ~container_registry.models.Descriptor
11161118
:keyword layers: List of V2 image layer information.
11171119
:paramtype layers: list[~container_registry.models.Descriptor]
11181120
:keyword annotations: Additional information provided through arbitrary metadata.
11191121
:paramtype annotations: ~container_registry.models.Annotations
1120-
:keyword schema_version: Schema version.
1121-
:paramtype schema_version: int
11221122
"""
1123-
super().__init__(**kwargs)
1123+
super().__init__(schema_version=schema_version, **kwargs)
11241124
self.config = config
11251125
self.layers = layers
11261126
self.annotations = annotations
1127-
self.schema_version = schema_version
11281127

11291128

11301129
class Paths108HwamOauth2ExchangePostRequestbodyContentApplicationXWwwFormUrlencodedSchema(_serialization.Model):

0 commit comments

Comments
 (0)