From 7b353467e6fa4854e77b7a6b0a13f9f35e95b64c Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Tue, 25 Apr 2023 17:05:53 -0700 Subject: [PATCH 01/16] Fixed tests Signed-off-by: Kevin Su --- flytekit/__init__.py | 1 + flytekit/image_spec/image_spec.py | 9 ++++++++- .../flytekit-envd/flytekitplugins/envd/image_builder.py | 5 +++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/flytekit/__init__.py b/flytekit/__init__.py index 7e279badf8..f081381ee6 100644 --- a/flytekit/__init__.py +++ b/flytekit/__init__.py @@ -225,6 +225,7 @@ from flytekit.core.workflow import WorkflowFailurePolicy, reference_workflow, workflow from flytekit.deck import Deck from flytekit.extras import pytorch, sklearn, tensorflow +from flytekit.image_spec import ImageSpec from flytekit.loggers import logger from flytekit.models.common import Annotations, AuthRole, Labels from flytekit.models.core.execution import WorkflowExecutionPhase diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index 72c815c574..16d63316c6 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -10,6 +10,7 @@ import click import docker +import requests from dataclasses_json import dataclass_json from docker.errors import APIError, ImageNotFound @@ -56,8 +57,14 @@ def exist(self) -> bool: """ Check if the image exists in the registry. """ - client = docker.from_env() + response = requests.get( + f"https://hub.docker.com/v2/repositories/{self.registry}/{self.name}/tags/{calculate_hash_from_image_spec(self)}" + ) + if response.status_code == 200: + return True + try: + client = docker.from_env() if self.registry: client.images.get_registry_data(self.image_name()) else: diff --git a/plugins/flytekit-envd/flytekitplugins/envd/image_builder.py b/plugins/flytekit-envd/flytekitplugins/envd/image_builder.py index 22e46c787f..ff44104f78 100644 --- a/plugins/flytekit-envd/flytekitplugins/envd/image_builder.py +++ b/plugins/flytekit-envd/flytekitplugins/envd/image_builder.py @@ -38,8 +38,9 @@ def create_envd_config(image_spec: ImageSpec) -> str: base_image = DefaultImages.default_image() if image_spec.base_image is None else image_spec.base_image packages = [] if image_spec.packages is None else image_spec.packages apt_packages = [] if image_spec.apt_packages is None else image_spec.apt_packages - env = {} if image_spec.env is None else image_spec.env - env.update({"PYTHONPATH": "/root"}) + env = {"PYTHONPATH": "/root"} + if image_spec.env: + env.update(image_spec.env) envd_config = f"""# syntax=v1 From 4be80a4f59b78c7105fe1208e03a43e47919e8ee Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Tue, 25 Apr 2023 17:15:14 -0700 Subject: [PATCH 02/16] test Signed-off-by: Kevin Su --- flytekit/image_spec/image_spec.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index 16d63316c6..3ac15e3480 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -79,6 +79,10 @@ def exist(self) -> bool: return False except ImageNotFound: return False + except Exception as e: + click.secho(f"Unknown error: {e}", fg="red") + # Skip building the image if there is an unknown error. + return True def __hash__(self): return hash(self.to_json()) From bcf2beb1e532a942c63e759d3b1c2ec32399ac00 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Tue, 25 Apr 2023 17:32:33 -0700 Subject: [PATCH 03/16] test Signed-off-by: Kevin Su --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d44b36091d..46be07b643 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ extras_require = {} -__version__ = "0.0.0+develop" +__version__ = "1.7.0" setup( name="flytekit", From 8efcbc777283c909d31ab4cd4dde1c41dc3a759d Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Tue, 25 Apr 2023 17:40:42 -0700 Subject: [PATCH 04/16] test Signed-off-by: Kevin Su --- flytekit/image_spec/image_spec.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index 3ac15e3480..c500af8c8c 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -57,12 +57,6 @@ def exist(self) -> bool: """ Check if the image exists in the registry. """ - response = requests.get( - f"https://hub.docker.com/v2/repositories/{self.registry}/{self.name}/tags/{calculate_hash_from_image_spec(self)}" - ) - if response.status_code == 200: - return True - try: client = docker.from_env() if self.registry: @@ -70,18 +64,11 @@ def exist(self) -> bool: else: client.images.get(self.image_name()) return True - except APIError as e: - if e.response.status_code == 404: - return False - if e.response.status_code == 403: - click.secho("Permission denied. Please login you docker registry first.", fg="red") - raise e - return False except ImageNotFound: return False except Exception as e: - click.secho(f"Unknown error: {e}", fg="red") - # Skip building the image if there is an unknown error. + click.secho(f"Failed to check if the image exists with error : {e}", fg="red") + click.secho(f"Flytekit assumes that the image already exists.", fg="blue") return True def __hash__(self): From 698efa892c4eb4dbe754ddb15fe7cbca4eb5ac24 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 07:47:46 -0700 Subject: [PATCH 05/16] lint Signed-off-by: Kevin Su --- flytekit/image_spec/image_spec.py | 5 ++--- tests/flytekit/unit/core/test_python_function_task.py | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index c500af8c8c..39c60431f2 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -10,9 +10,8 @@ import click import docker -import requests from dataclasses_json import dataclass_json -from docker.errors import APIError, ImageNotFound +from docker.errors import ImageNotFound @dataclass_json @@ -68,7 +67,7 @@ def exist(self) -> bool: return False except Exception as e: click.secho(f"Failed to check if the image exists with error : {e}", fg="red") - click.secho(f"Flytekit assumes that the image already exists.", fg="blue") + click.secho("Flytekit assumes that the image already exists.", fg="blue") return True def __hash__(self): diff --git a/tests/flytekit/unit/core/test_python_function_task.py b/tests/flytekit/unit/core/test_python_function_task.py index be2c3c1827..3499459a1f 100644 --- a/tests/flytekit/unit/core/test_python_function_task.py +++ b/tests/flytekit/unit/core/test_python_function_task.py @@ -78,9 +78,6 @@ def build_image(self, img): == "flytekit:0N8X-XowtpEkDYWDlb8Abg.." ) - with pytest.raises(Exception): - get_registerable_container_image(ImageSpec(builder="test", python_version="3.7", registry="hello"), cfg) - def test_get_registerable_container_image_no_images(): cfg = ImageConfig() From e575f3193f320cae52e1dd7b0d92ca2714f15ad6 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 10:36:48 -0700 Subject: [PATCH 06/16] wip Signed-off-by: Kevin Su --- flytekit/configuration/__init__.py | 3 +++ flytekit/image_spec/image_spec.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/flytekit/configuration/__init__.py b/flytekit/configuration/__init__.py index 1e424b3c57..eba35cfb25 100644 --- a/flytekit/configuration/__init__.py +++ b/flytekit/configuration/__init__.py @@ -795,6 +795,7 @@ def new_builder(self) -> Builder: flytekit_virtualenv_root=self.flytekit_virtualenv_root, python_interpreter=self.python_interpreter, fast_serialization_settings=self.fast_serialization_settings, + source_root=self.source_root, ) def should_fast_serialize(self) -> bool: @@ -845,6 +846,7 @@ class Builder(object): flytekit_virtualenv_root: Optional[str] = None python_interpreter: Optional[str] = None fast_serialization_settings: Optional[FastSerializationSettings] = None + source_root: Optional[str] = None def with_fast_serialization_settings(self, fss: fast_serialization_settings) -> SerializationSettings.Builder: self.fast_serialization_settings = fss @@ -861,4 +863,5 @@ def build(self) -> SerializationSettings: flytekit_virtualenv_root=self.flytekit_virtualenv_root, python_interpreter=self.python_interpreter, fast_serialization_settings=self.fast_serialization_settings, + source_root=self.source_root, ) diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index 39c60431f2..598b421bb4 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -11,7 +11,7 @@ import click import docker from dataclasses_json import dataclass_json -from docker.errors import ImageNotFound +from docker.errors import APIError, ImageNotFound @dataclass_json @@ -63,6 +63,9 @@ def exist(self) -> bool: else: client.images.get(self.image_name()) return True + except APIError as e: + if e.response.status_code == 404: + return False except ImageNotFound: return False except Exception as e: From a60ae916bdfb98a25fe7b0625ae63681fa48cba9 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 11:27:59 -0700 Subject: [PATCH 07/16] test Signed-off-by: Kevin Su --- flytekit/image_spec/image_spec.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index 598b421bb4..cbfd23f0eb 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -4,12 +4,14 @@ import sys import typing from abc import abstractmethod +from copy import copy from dataclasses import dataclass from functools import lru_cache from typing import List, Optional import click import docker +import requests from dataclasses_json import dataclass_json from docker.errors import APIError, ImageNotFound @@ -69,6 +71,12 @@ def exist(self) -> bool: except ImageNotFound: return False except Exception as e: + # if docker engine is not running locally + response = requests.get( + f"https://hub.docker.com/v2/repositories/{self.registry}/{self.name}/tags/{calculate_hash_from_image_spec(self)}" + ) + if response.status_code == 200: + return True click.secho(f"Failed to check if the image exists with error : {e}", fg="red") click.secho("Flytekit assumes that the image already exists.", fg="blue") return True @@ -116,10 +124,10 @@ def calculate_hash_from_image_spec(image_spec: ImageSpec): """ Calculate the hash from the image spec. """ + spec = copy(image_spec) + spec.source_root = hash_directory(image_spec.source_root) if image_spec.source_root else b"" image_spec_bytes = bytes(image_spec.to_json(), "utf-8") - source_root_bytes = hash_directory(image_spec.source_root) if image_spec.source_root else b"" - h = hashlib.md5(image_spec_bytes + source_root_bytes) - tag = base64.urlsafe_b64encode(h.digest()).decode("ascii") + tag = base64.urlsafe_b64encode(hashlib.md5(image_spec_bytes).digest()).decode("ascii") # replace "=" with "." to make it a valid tag return tag.replace("=", ".") From 5589a4b9231e8e42d40736623edad504543828f9 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 11:38:57 -0700 Subject: [PATCH 08/16] test Signed-off-by: Kevin Su --- flytekit/core/python_auto_container.py | 6 +++++- flytekit/image_spec/image_spec.py | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/flytekit/core/python_auto_container.py b/flytekit/core/python_auto_container.py index ea72c19660..29e074b80b 100644 --- a/flytekit/core/python_auto_container.py +++ b/flytekit/core/python_auto_container.py @@ -181,7 +181,11 @@ def _get_container(self, settings: SerializationSettings) -> _task_model.Contain for elem in (settings.env, self.environment): if elem: env.update(elem) - if isinstance(self.container_image, ImageSpec): + if ( + settings.fast_serialization_settings + and settings.fast_serialization_settings.enabled + and isinstance(self.container_image, ImageSpec) + ): self.container_image.source_root = settings.source_root return _get_container_definition( image=get_registerable_container_image(self.container_image, settings.image_config), diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index cbfd23f0eb..8090d6b71a 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -71,12 +71,15 @@ def exist(self) -> bool: except ImageNotFound: return False except Exception as e: + tag = calculate_hash_from_image_spec(self) # if docker engine is not running locally - response = requests.get( - f"https://hub.docker.com/v2/repositories/{self.registry}/{self.name}/tags/{calculate_hash_from_image_spec(self)}" - ) + response = requests.get(f"https://hub.docker.com/v2/repositories/{self.registry}/{self.name}/tags/{tag}") if response.status_code == 200: return True + response = requests.get(f"https://ghcr.io/v2/{self.registry}/{self.name}/manifests/{tag}") + if response.status_code == 200: + return True + click.secho(f"Failed to check if the image exists with error : {e}", fg="red") click.secho("Flytekit assumes that the image already exists.", fg="blue") return True From d1004895242035853afe45a37f48b9b79e50ff5c Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 11:39:37 -0700 Subject: [PATCH 09/16] test Signed-off-by: Kevin Su --- flytekit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flytekit/__init__.py b/flytekit/__init__.py index f081381ee6..ebce91a833 100644 --- a/flytekit/__init__.py +++ b/flytekit/__init__.py @@ -241,7 +241,7 @@ StructuredDatasetType, ) -__version__ = "0.0.0+develop" +__version__ = "1.7.0" def current_context() -> ExecutionParameters: From 7707dee04bc65ade61b267d8f8308076b42d2af1 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 11:43:27 -0700 Subject: [PATCH 10/16] test Signed-off-by: Kevin Su --- flytekit/core/python_auto_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flytekit/core/python_auto_container.py b/flytekit/core/python_auto_container.py index 29e074b80b..589ad8ebdc 100644 --- a/flytekit/core/python_auto_container.py +++ b/flytekit/core/python_auto_container.py @@ -183,7 +183,7 @@ def _get_container(self, settings: SerializationSettings) -> _task_model.Contain env.update(elem) if ( settings.fast_serialization_settings - and settings.fast_serialization_settings.enabled + and not settings.fast_serialization_settings.enabled and isinstance(self.container_image, ImageSpec) ): self.container_image.source_root = settings.source_root From 8b79aafe9df53e2a3b122b6d48e6796a96a8c5d1 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 11:48:34 -0700 Subject: [PATCH 11/16] test Signed-off-by: Kevin Su --- flytekit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flytekit/__init__.py b/flytekit/__init__.py index ebce91a833..f081381ee6 100644 --- a/flytekit/__init__.py +++ b/flytekit/__init__.py @@ -241,7 +241,7 @@ StructuredDatasetType, ) -__version__ = "1.7.0" +__version__ = "0.0.0+develop" def current_context() -> ExecutionParameters: From 77d0b42b6665d48e4b307a65bc590d73bf1608a5 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 11:57:03 -0700 Subject: [PATCH 12/16] test Signed-off-by: Kevin Su --- flytekit/core/python_auto_container.py | 9 +++------ flytekit/image_spec/image_spec.py | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/flytekit/core/python_auto_container.py b/flytekit/core/python_auto_container.py index 589ad8ebdc..fa844b8ae4 100644 --- a/flytekit/core/python_auto_container.py +++ b/flytekit/core/python_auto_container.py @@ -181,12 +181,9 @@ def _get_container(self, settings: SerializationSettings) -> _task_model.Contain for elem in (settings.env, self.environment): if elem: env.update(elem) - if ( - settings.fast_serialization_settings - and not settings.fast_serialization_settings.enabled - and isinstance(self.container_image, ImageSpec) - ): - self.container_image.source_root = settings.source_root + if settings.fast_serialization_settings is None or not settings.fast_serialization_settings.enabled: + if isinstance(self.container_image, ImageSpec): + self.container_image.source_root = settings.source_root return _get_container_definition( image=get_registerable_container_image(self.container_image, settings.image_config), command=[], diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index 8090d6b71a..39bd02659a 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -128,6 +128,7 @@ def calculate_hash_from_image_spec(image_spec: ImageSpec): Calculate the hash from the image spec. """ spec = copy(image_spec) + print(image_spec.source_root) spec.source_root = hash_directory(image_spec.source_root) if image_spec.source_root else b"" image_spec_bytes = bytes(image_spec.to_json(), "utf-8") tag = base64.urlsafe_b64encode(hashlib.md5(image_spec_bytes).digest()).decode("ascii") From 3ffb159c3c9113453b3f7ac7bbf6ff0baf98703b Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 12:17:43 -0700 Subject: [PATCH 13/16] nit Signed-off-by: Kevin Su --- flytekit/image_spec/image_spec.py | 1 - setup.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index 39bd02659a..8090d6b71a 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -128,7 +128,6 @@ def calculate_hash_from_image_spec(image_spec: ImageSpec): Calculate the hash from the image spec. """ spec = copy(image_spec) - print(image_spec.source_root) spec.source_root = hash_directory(image_spec.source_root) if image_spec.source_root else b"" image_spec_bytes = bytes(image_spec.to_json(), "utf-8") tag = base64.urlsafe_b64encode(hashlib.md5(image_spec_bytes).digest()).decode("ascii") diff --git a/setup.py b/setup.py index 46be07b643..d44b36091d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ extras_require = {} -__version__ = "1.7.0" +__version__ = "0.0.0+develop" setup( name="flytekit", From e4edef63881723406dfcca3bf7b719e246ba0061 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 26 Apr 2023 16:13:12 -0700 Subject: [PATCH 14/16] add lru cache Signed-off-by: Kevin Su --- flytekit/image_spec/image_spec.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index 8090d6b71a..546f76edfb 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -54,6 +54,7 @@ def image_name(self) -> str: container_image = f"{self.registry}/{container_image}" return container_image + @lru_cache def exist(self) -> bool: """ Check if the image exists in the registry. @@ -122,7 +123,7 @@ def build(cls, image_spec: ImageSpec): click.secho(f"Image {image_spec.image_name()} found. Skip building.", fg="blue") -@lru_cache(maxsize=None) +@lru_cache def calculate_hash_from_image_spec(image_spec: ImageSpec): """ Calculate the hash from the image spec. From d4700967f0ca17280657d4db3ac90db8e5e1be88 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Mon, 1 May 2023 14:50:50 -0700 Subject: [PATCH 15/16] update Signed-off-by: Kevin Su --- flytekit/image_spec/image_spec.py | 17 +++++++++++------ .../unit/core/image_spec/test_image_spec.py | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index 546f76edfb..7c4dd4b269 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -15,6 +15,8 @@ from dataclasses_json import dataclass_json from docker.errors import APIError, ImageNotFound +DOCKER_HUB = "docker.io" + @dataclass_json @dataclass @@ -74,12 +76,14 @@ def exist(self) -> bool: except Exception as e: tag = calculate_hash_from_image_spec(self) # if docker engine is not running locally - response = requests.get(f"https://hub.docker.com/v2/repositories/{self.registry}/{self.name}/tags/{tag}") - if response.status_code == 200: - return True - response = requests.get(f"https://ghcr.io/v2/{self.registry}/{self.name}/manifests/{tag}") - if response.status_code == 200: - return True + container_registry = DOCKER_HUB + if "/" in self.registry: + container_registry = self.registry.split("/")[0] + if container_registry == DOCKER_HUB: + url = "https://hub.docker.com/v2/repositories/{self.registry}/{self.name}/tags/{tag}" + response = requests.get(url) + if response.status_code == 200: + return True click.secho(f"Failed to check if the image exists with error : {e}", fg="red") click.secho("Flytekit assumes that the image already exists.", fg="blue") @@ -128,6 +132,7 @@ def calculate_hash_from_image_spec(image_spec: ImageSpec): """ Calculate the hash from the image spec. """ + # copy the image spec to avoid modifying the original image spec. otherwise, the hash will be different. spec = copy(image_spec) spec.source_root = hash_directory(image_spec.source_root) if image_spec.source_root else b"" image_spec_bytes = bytes(image_spec.to_json(), "utf-8") diff --git a/tests/flytekit/unit/core/image_spec/test_image_spec.py b/tests/flytekit/unit/core/image_spec/test_image_spec.py index d2d4975364..76c9dfccb9 100644 --- a/tests/flytekit/unit/core/image_spec/test_image_spec.py +++ b/tests/flytekit/unit/core/image_spec/test_image_spec.py @@ -31,6 +31,7 @@ def build_image(self, img): ImageBuildEngine._REGISTRY["dummy"].build_image(image_spec) assert "dummy" in ImageBuildEngine._REGISTRY assert calculate_hash_from_image_spec(image_spec) == "yZ8jICcDTLoDArmNHbWNwg.." + assert image_spec.exist() is False with pytest.raises(Exception): image_spec.builder = "flyte" From f56a4773934a864e7d63c1246cb9d46eee48389c Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Mon, 1 May 2023 14:57:44 -0700 Subject: [PATCH 16/16] lint Signed-off-by: Kevin Su --- flytekit/image_spec/image_spec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flytekit/image_spec/image_spec.py b/flytekit/image_spec/image_spec.py index e7c0c58ad3..e35fd5c597 100644 --- a/flytekit/image_spec/image_spec.py +++ b/flytekit/image_spec/image_spec.py @@ -89,7 +89,7 @@ def exist(self) -> bool: if "/" in self.registry: container_registry = self.registry.split("/")[0] if container_registry == DOCKER_HUB: - url = "https://hub.docker.com/v2/repositories/{self.registry}/{self.name}/tags/{tag}" + url = f"https://hub.docker.com/v2/repositories/{self.registry}/{self.name}/tags/{tag}" response = requests.get(url) if response.status_code == 200: return True