Skip to content

Commit 055c6a8

Browse files
committed
implement Strategy pattern for DockerBuilder and also support user-provide dockerfile for build
1 parent 437a1f8 commit 055c6a8

File tree

3 files changed

+129
-73
lines changed

3 files changed

+129
-73
lines changed

src/gaiaflow/cli/commands/minikube.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ def dockerize(
109109
image_name: str = typer.Option(
110110
DEFAULT_IMAGE_NAME, "--image-name", "-i", help=("Name of your image.")
111111
),
112+
dockerfile_path: Path = typer.Option(
113+
None, "--dockerfile-path", "-d", help=("Path to your custom "
114+
"Dockerfile")
115+
),
112116
):
113117
imports = load_imports()
114118
project_path = Path.cwd()
@@ -119,12 +123,17 @@ def dockerize(
119123
if not gaiaflow_path_exists:
120124
typer.echo("Please create a project with Gaiaflow before running this command.")
121125
return
126+
if dockerfile_path:
127+
docker_build_mode = "minikube-user"
128+
else:
129+
docker_build_mode = "minikube"
122130
imports.MinikubeManager.run(
123131
gaiaflow_path=gaiaflow_path,
124132
user_project_path=user_project_path,
125133
action=imports.ExtendedAction.DOCKERIZE,
126-
local=False,
134+
docker_build_mode=docker_build_mode,
127135
image_name=image_name,
136+
dockerfile_path=dockerfile_path,
128137
)
129138

130139

src/gaiaflow/cli/commands/mlops.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ def dockerize(
255255
image_name: str = typer.Option(
256256
DEFAULT_IMAGE_NAME, "--image-name", "-i", help=("Name of your image.")
257257
),
258+
dockerfile_path: Path = typer.Option(
259+
None, "--dockerfile-path", "-d", help=("Path to your custom "
260+
"Dockerfile")
261+
),
258262
):
259263
imports = load_imports()
260264
project_path = Path.cwd()
@@ -270,13 +274,16 @@ def dockerize(
270274
f"Gaiaflow project already exists at {gaiaflow_path}. Skipping "
271275
f"saving to the state"
272276
)
273-
277+
if dockerfile_path:
278+
docker_build_mode = "local-user"
279+
else:
280+
docker_build_mode = "local"
274281
typer.echo("Running dockerize")
275282
imports.MinikubeManager.run(
276283
gaiaflow_path=gaiaflow_path,
277284
user_project_path=user_project_path,
278285
action=imports.ExtendedAction.DOCKERIZE,
279-
local=True,
286+
docker_build_mode=docker_build_mode,
280287
image_name=image_name,
281288
)
282289

src/gaiaflow/managers/minikube_manager.py

Lines changed: 110 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import subprocess
55
from contextlib import contextmanager
66
from pathlib import Path
7-
from typing import Any, Set
7+
from typing import Any, Set, Literal
88

99
import yaml
1010

@@ -95,77 +95,110 @@ def run_cmd(self, args: list[str], **kwargs):
9595
return subprocess.run(full_cmd, **kwargs)
9696

9797

98+
class BaseDockerBuilder:
99+
"""Abstract docker builder with optional hooks."""
100+
101+
@classmethod
102+
def get_docker_builder(cls, mode: str, **kwargs):
103+
builder_cls = BUILDER_REGISTRY.get(mode)
104+
if not builder_cls:
105+
raise ValueError(f"Unknown Docker build mode: {mode}")
106+
return builder_cls(**kwargs)
107+
108+
def pre_build(self, image_name: str, dockerfile_path: Path, project_path: Path):
109+
"""Override this if you want a different or no pre_build"""
110+
log_info(f"Updating Dockerfile at {dockerfile_path}")
111+
DockerHelper._add_copy_statements_to_dockerfile(
112+
str(dockerfile_path), find_python_packages(project_path)
113+
)
114+
runner_src = Path(__file__).parent.parent.resolve() / "core" / "runner.py"
115+
runner_dest = project_path / "runner.py"
116+
return temporary_copy(runner_src, runner_dest)
117+
118+
def build(self, image_name: str, dockerfile_path: Path, project_path: Path):
119+
raise NotImplementedError
120+
121+
def post_build(self, image_name: str, dockerfile_path: Path, project_path: Path):
122+
pass
123+
124+
class LocalDockerBuilder(BaseDockerBuilder):
125+
def build(self, image_name: str, dockerfile_path: Path, project_path: Path):
126+
log_info(f"Building Docker image [{image_name}] locally")
127+
run(
128+
["docker", "build", "-t", image_name, "-f", str(dockerfile_path), str(project_path)],
129+
"Error building Docker image locally",
130+
)
131+
132+
class MinikubeDockerBuilder(BaseDockerBuilder):
133+
def __init__(self, minikube_helper: MinikubeHelper):
134+
self.minikube_helper = minikube_helper
135+
136+
def build(self, image_name: str, dockerfile_path: Path, project_path: Path):
137+
log_info(f"Building Docker image [{image_name}] in Minikube context")
138+
result = self.minikube_helper.run_cmd(
139+
["docker-env", "--shell", "bash"], stdout=subprocess.PIPE, check=True
140+
)
141+
env = DockerHelper._parse_minikube_env(result.stdout.decode())
142+
run(
143+
["docker", "build", "-t", image_name, "-f", str(dockerfile_path), str(project_path)],
144+
"Error building Docker image inside Minikube",
145+
env=env,
146+
)
147+
148+
class LocalUserCustomImageDockerBuilder(LocalDockerBuilder):
149+
def pre_build(self, image_name: str, dockerfile_path: Path, project_path: Path):
150+
pass
151+
def build(self, image_name: str, dockerfile_path: Path, project_path: Path):
152+
log_info("Building user provided dockerfile")
153+
super().build(image_name, dockerfile_path, project_path)
154+
155+
class MinikubeUserCustomImageDockerBuilder(MinikubeDockerBuilder):
156+
def pre_build(self, image_name: str, dockerfile_path: Path, project_path: Path):
157+
pass
158+
159+
def build(self, image_name: str, dockerfile_path: Path, project_path: Path):
160+
log_info("Building user provided dockerfile")
161+
super().build(image_name, dockerfile_path, project_path)
162+
163+
BUILDER_REGISTRY = {
164+
"local": LocalDockerBuilder,
165+
"minikube": MinikubeDockerBuilder,
166+
"local-user": LocalUserCustomImageDockerBuilder,
167+
"minikube-user": MinikubeUserCustomImageDockerBuilder
168+
}
169+
98170
class DockerHelper:
99171
def __init__(
100172
self,
101173
image_name: str,
102174
project_path: Path,
103-
local: bool,
104-
minikube_helper: MinikubeHelper,
175+
builder: BaseDockerBuilder
105176
):
106177
self.image_name = image_name
107178
self.project_path = project_path
108-
self.local = local
109-
self.minikube_helper = minikube_helper
179+
self.builder = builder
110180

111181
def build_image(self, dockerfile_path: Path):
112182
if not dockerfile_path.exists():
113183
log_error(f"Dockerfile not found at {dockerfile_path}")
114184
return
115185

116-
log_info(f"Updating Dockerfile at {dockerfile_path}")
117-
self._update_dockerfile(dockerfile_path)
118-
119-
runner_src = Path(__file__).parent.parent.resolve() / "core" / "runner.py"
120-
runner_dest = self.project_path / "runner.py"
186+
pre_build_ctx = self.builder.pre_build(
187+
self.image_name, dockerfile_path, self.project_path
188+
)
189+
if pre_build_ctx:
190+
with pre_build_ctx:
191+
self.builder.build(self.image_name, dockerfile_path, self.project_path)
192+
else:
193+
self.builder.build(self.image_name, dockerfile_path, self.project_path)
121194

122-
with temporary_copy(runner_src, runner_dest):
123-
if self.local:
124-
self._build_local(dockerfile_path)
125-
else:
126-
self._build_minikube(dockerfile_path)
195+
self.builder.post_build(self.image_name, dockerfile_path, self.project_path)
127196

128197
def _update_dockerfile(self, dockerfile_path: Path):
129198
DockerHelper._add_copy_statements_to_dockerfile(
130199
str(dockerfile_path), find_python_packages(self.project_path)
131200
)
132201

133-
def _build_local(self, dockerfile_path: Path):
134-
log_info(f"Building Docker image [{self.image_name}] locally")
135-
run(
136-
[
137-
"docker",
138-
"build",
139-
"-t",
140-
self.image_name,
141-
"-f",
142-
dockerfile_path,
143-
self.project_path,
144-
],
145-
"Error building Docker image locally",
146-
)
147-
set_permissions("/var/run/docker.sock", 0o666)
148-
149-
def _build_minikube(self, dockerfile_path: Path):
150-
log_info(f"Building Docker image [{self.image_name}] in Minikube context")
151-
result = self.minikube_helper.run_cmd(
152-
["docker-env", "--shell", "bash"], stdout=subprocess.PIPE, check=True
153-
)
154-
env = self._parse_minikube_env(result.stdout.decode())
155-
run(
156-
[
157-
"docker",
158-
"build",
159-
"-t",
160-
self.image_name,
161-
"-f",
162-
dockerfile_path,
163-
self.project_path,
164-
],
165-
"Error building Docker image inside Minikube",
166-
env=env,
167-
)
168-
169202
@staticmethod
170203
def _parse_minikube_env(output: str) -> dict:
171204
env = os.environ.copy()
@@ -225,17 +258,18 @@ def __init__(self, gaiaflow_path: Path, os_type: str):
225258
self.os_type = os_type
226259

227260
def create_inline(self):
228-
kube_config = Path.home() / ".kube" / "config"
229-
backup_config = kube_config.with_suffix(".backup")
261+
if self.os_type == "linux" or is_wsl():
262+
kube_config = Path.home() / ".kube" / "config"
263+
backup_config = kube_config.with_suffix(".backup")
230264

231-
self._backup_kube_config(kube_config, backup_config)
232-
self._patch_kube_config(kube_config)
233-
self._write_inline(kube_config)
265+
self._backup_kube_config(kube_config, backup_config)
266+
self._patch_kube_config(kube_config)
267+
self._write_inline(kube_config)
234268

235-
if (self.os_type == "windows" or is_wsl()) and backup_config.exists():
236-
shutil.copy(backup_config, kube_config)
237-
backup_config.unlink()
238-
log_info("Reverted kube config to original state.")
269+
if backup_config.exists():
270+
shutil.copy(backup_config, kube_config)
271+
backup_config.unlink()
272+
log_info("Reverted kube config to original state.")
239273

240274
def _backup_kube_config(self, kube_config: Path, backup_config: Path):
241275
if kube_config.exists():
@@ -289,33 +323,37 @@ def _write_inline(self, kube_config: Path):
289323

290324

291325
class MinikubeManager(BaseGaiaflowManager):
326+
allowed_kwargs = {"secret_name", "secret_data", "dockerfile_path"}
292327
def __init__(
293328
self,
294329
gaiaflow_path: Path,
295330
user_project_path: Path,
296331
action: Action,
297332
force_new: bool = False,
298333
prune: bool = False,
299-
local: bool = False,
334+
docker_build_mode: Literal["local", "minikube"] = "local",
300335
image_name: str = "",
301336
**kwargs,
302337
):
303-
# if kwargs:
304-
# raise TypeError(f"Unexpected keyword arguments: {list(kwargs.keys())}")
338+
if kwargs:
339+
for key in kwargs:
340+
if key not in self.allowed_kwargs:
341+
raise TypeError(f"Unexpected keyword argument: {key}")
342+
305343
self.minikube_profile = "airflow"
306344
# TODO: get the docker image name automatically
307345
# For CI, get the package name, version and create repository. See
308346
# in test-airflow-ci test_ecr_push.yml
309347
self.os_type = platform.system().lower()
310-
self.local = local
311348
self.image_name = image_name
312349

313350
self.minikube_helper = MinikubeHelper()
351+
builder = BaseDockerBuilder.get_docker_builder(docker_build_mode,
352+
minikube_helper=self.minikube_helper)
314353
self.docker_helper = DockerHelper(
315354
image_name=image_name,
316355
project_path=user_project_path,
317-
local=local,
318-
minikube_helper=self.minikube_helper,
356+
builder=builder,
319357
)
320358
self.kube_helper = KubeConfigHelper(
321359
gaiaflow_path=gaiaflow_path, os_type=self.os_type
@@ -338,7 +376,7 @@ def _get_valid_actions(self) -> Set[Action]:
338376

339377
@classmethod
340378
def run(cls, **kwargs):
341-
action = kwargs.get("action")
379+
action = kwargs.get("action", None)
342380
if action is None:
343381
raise ValueError("Missing required argument 'action'")
344382

@@ -349,7 +387,8 @@ def run(cls, **kwargs):
349387
BaseAction.STOP: manager.stop,
350388
BaseAction.RESTART: manager.restart,
351389
BaseAction.CLEANUP: manager.cleanup,
352-
ExtendedAction.DOCKERIZE: manager.build_docker_image,
390+
ExtendedAction.DOCKERIZE: lambda: manager.build_docker_image(
391+
kwargs["dockerfile_path"]),
353392
ExtendedAction.CREATE_CONFIG: manager.create_kube_config_inline,
354393
ExtendedAction.CREATE_SECRET: lambda: manager.create_secrets(
355394
kwargs["secret_name"], kwargs["secret_data"]
@@ -391,8 +430,9 @@ def stop(self):
391430
def create_kube_config_inline(self):
392431
self.kube_helper.create_inline()
393432

394-
def build_docker_image(self):
395-
dockerfile_path = self.gaiaflow_path / "_docker" / "user-package" / "Dockerfile"
433+
def build_docker_image(self, dockerfile_path: str):
434+
if not dockerfile_path:
435+
dockerfile_path = self.gaiaflow_path / "_docker" / "user-package" / "Dockerfile"
396436
self.docker_helper.build_image(dockerfile_path)
397437

398438
def create_secrets(self, secret_name: str, secret_data: dict[str, Any]):

0 commit comments

Comments
 (0)