From 985f2a4c8cffc68f4299e94cf7f4e7186a13ba58 Mon Sep 17 00:00:00 2001 From: Linsho Kaku Date: Mon, 13 Nov 2023 15:26:39 +0900 Subject: [PATCH 1/5] Allow uploading when is not specified in the google artifact registry Signed-off-by: Linsho Kaku --- oras/defaults.py | 5 +++++ oras/oci.py | 8 ++++---- oras/provider.py | 11 ++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/oras/defaults.py b/oras/defaults.py index cccde2d..2992290 100644 --- a/oras/defaults.py +++ b/oras/defaults.py @@ -41,3 +41,8 @@ class registry: # what you get for a blank digest, so we don't need to save and recalculate blank_hash = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + +# what you get for a blank config digest, so we don't need to save and recalculate +blank_config_hash = ( + "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" +) diff --git a/oras/oci.py b/oras/oci.py index 8c37087..4265f7d 100644 --- a/oras/oci.py +++ b/oras/oci.py @@ -117,7 +117,7 @@ def NewLayer( def ManifestConfig( path: Optional[str] = None, media_type: Optional[str] = None -) -> Tuple[Dict[str, object], str]: +) -> Tuple[Dict[str, object], Optional[str]]: """ Write an empty config, if one is not provided @@ -128,11 +128,11 @@ def ManifestConfig( """ # Create an empty config if we don't have one if not path or not os.path.exists(path): - path = os.devnull + path = None conf = { "mediaType": media_type or oras.defaults.unknown_config_media_type, - "size": 0, - "digest": oras.defaults.blank_hash, + "size": 2, + "digest": oras.defaults.blank_config_hash, } else: diff --git a/oras/provider.py b/oras/provider.py index b03e197..0c2e138 100644 --- a/oras/provider.py +++ b/oras/provider.py @@ -7,6 +7,7 @@ import urllib from dataclasses import asdict, dataclass from http.cookiejar import DefaultCookiePolicy +from tempfile import TemporaryDirectory from typing import Callable, List, Optional, Tuple, Union import jsonschema @@ -747,7 +748,15 @@ def push(self, *args, **kwargs) -> requests.Response: # Config is just another layer blob! logger.debug(f"Preparing config {conf}") - response = self.upload_blob(config_file, container, conf) + if config_file is None: + with TemporaryDirectory() as tmp: + config_file = os.path.join(tmp, "config.json") + with open(config_file, "w") as f: + f.write("{}") + response = self.upload_blob(config_file, container, conf) + else: + response = self.upload_blob(config_file, container, conf) + self._check_200_response(response) # Final upload of the manifest From 74a829c30264ec695cc4847c6aab8c5a8455a80a Mon Sep 17 00:00:00 2001 From: Linsho Kaku Date: Mon, 13 Nov 2023 15:44:36 +0900 Subject: [PATCH 2/5] add change log Signed-off-by: Linsho Kaku --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfefbec..12ec7a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are: The versions coincide with releases on pip. Only major versions will be released as tags on Github. ## [0.0.x](https://github.com/oras-project/oras-py/tree/main) (0.0.x) + - To make it available for more OCI registries, the value of config used when `manifest_config` is not specified in `client.push()` has been changed from a pure empty string to `{}` (0.1.26) - refactor tests using fixtures and rework pre-commit configuration (0.1.25) - eliminate the additional subdirectory creation while pulling an image to a custom output directory (0.1.24) - updating the exclude string in the pyproject.toml file to match the [data type black expects](https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-format) From 932310cb36d191dc544dfe8c4b3045f97a2f2e37 Mon Sep 17 00:00:00 2001 From: Linsho Kaku Date: Mon, 13 Nov 2023 15:54:45 +0900 Subject: [PATCH 3/5] update versions Signed-off-by: Linsho Kaku --- oras/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oras/version.py b/oras/version.py index 7db5671..8432dbd 100644 --- a/oras/version.py +++ b/oras/version.py @@ -2,7 +2,7 @@ __copyright__ = "Copyright The ORAS Authors." __license__ = "Apache-2.0" -__version__ = "0.1.25" +__version__ = "0.1.26" AUTHOR = "Vanessa Sochat" EMAIL = "vsoch@users.noreply.github.com" NAME = "oras" From 483415579c9a25ec40447b55bf3c67825c73e26f Mon Sep 17 00:00:00 2001 From: Linsho Kaku Date: Wed, 15 Nov 2023 13:12:56 +0900 Subject: [PATCH 4/5] Refactoring of the process when config_file is None Signed-off-by: Linsho Kaku --- oras/provider.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/oras/provider.py b/oras/provider.py index 0c2e138..e28a898 100644 --- a/oras/provider.py +++ b/oras/provider.py @@ -7,7 +7,6 @@ import urllib from dataclasses import asdict, dataclass from http.cookiejar import DefaultCookiePolicy -from tempfile import TemporaryDirectory from typing import Callable, List, Optional, Tuple, Union import jsonschema @@ -749,13 +748,10 @@ def push(self, *args, **kwargs) -> requests.Response: # Config is just another layer blob! logger.debug(f"Preparing config {conf}") if config_file is None: - with TemporaryDirectory() as tmp: - config_file = os.path.join(tmp, "config.json") - with open(config_file, "w") as f: - f.write("{}") - response = self.upload_blob(config_file, container, conf) - else: - response = self.upload_blob(config_file, container, conf) + config_file = oras.utils.get_tmpfile(suffix=".json") + with open(config_file, "w") as f: + f.write("{}") + response = self.upload_blob(config_file, container, conf) self._check_200_response(response) From 6af1a04ffd20d8f24e93a429fad02e583a91caaa Mon Sep 17 00:00:00 2001 From: Linsho Kaku Date: Thu, 16 Nov 2023 16:03:31 +0900 Subject: [PATCH 5/5] Add cleanup process for temporary files created Signed-off-by: Linsho Kaku --- oras/provider.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/oras/provider.py b/oras/provider.py index e28a898..db0ac3a 100644 --- a/oras/provider.py +++ b/oras/provider.py @@ -5,9 +5,11 @@ import copy import os import urllib +from contextlib import contextmanager, nullcontext from dataclasses import asdict, dataclass from http.cookiejar import DefaultCookiePolicy -from typing import Callable, List, Optional, Tuple, Union +from tempfile import TemporaryDirectory +from typing import Callable, Generator, List, Optional, Tuple, Union import jsonschema import requests @@ -25,6 +27,14 @@ container_type = Union[str, oras.container.Container] +@contextmanager +def temporary_empty_config() -> Generator[str, None, None]: + with TemporaryDirectory() as tmpdir: + config_file = oras.utils.get_tmpfile(tmpdir=tmpdir, suffix=".json") + oras.utils.write_file(config_file, "{}") + yield config_file + + @dataclass class Subject: mediaType: str @@ -747,11 +757,10 @@ def push(self, *args, **kwargs) -> requests.Response: # Config is just another layer blob! logger.debug(f"Preparing config {conf}") - if config_file is None: - config_file = oras.utils.get_tmpfile(suffix=".json") - with open(config_file, "w") as f: - f.write("{}") - response = self.upload_blob(config_file, container, conf) + with temporary_empty_config() if config_file is None else nullcontext( + config_file + ) as config_file: + response = self.upload_blob(config_file, container, conf) self._check_200_response(response)