44import subprocess
55from contextlib import contextmanager
66from pathlib import Path
7- from typing import Any , Set
7+ from typing import Any , Set , Literal
88
99import 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+
98170class 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
291325class 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