66
77from __future__ import annotations
88
9- import argparse
109import json
1110import logging
1211import os
1514import sys
1615import tempfile
1716from pathlib import Path
18- from typing import Any , Dict , List , Optional , Tuple
17+ from typing import Dict , List , Optional , Tuple
1918
2019import yaml
2120
22- from airbyte_cdk .cli .build .models import ConnectorLanguage , ConnectorMetadata , MetadataFile
21+ from airbyte_cdk .cli .build .models import ConnectorMetadata , MetadataFile
2322
2423logger = logging .getLogger ("airbyte-cdk.cli.build" )
2524
@@ -34,23 +33,6 @@ def set_up_logging(verbose: bool = False) -> None:
3433 )
3534
3635
37- def parse_args (args : List [str ]) -> argparse .Namespace :
38- """Parse command line arguments for the build command."""
39- parser = argparse .ArgumentParser (
40- description = "Build connector Docker images using the host Docker daemon"
41- )
42- parser .add_argument ("connector_dir" , type = str , help = "Path to the connector directory" )
43- parser .add_argument (
44- "--tag" , type = str , default = "dev" , help = "Tag to apply to the built image (default: dev)"
45- )
46- parser .add_argument (
47- "--no-verify" , action = "store_true" , help = "Skip verification of the built image"
48- )
49- parser .add_argument ("--verbose" , "-v" , action = "store_true" , help = "Enable verbose logging" )
50-
51- return parser .parse_args (args )
52-
53-
5436def read_metadata (connector_dir : Path ) -> ConnectorMetadata :
5537 """Read and parse connector metadata from metadata.yaml.
5638
@@ -78,30 +60,7 @@ def read_metadata(connector_dir: Path) -> ConnectorMetadata:
7860 return metadata_file .data
7961
8062
81- def infer_connector_language (metadata : ConnectorMetadata , connector_dir : Path ) -> ConnectorLanguage :
82- """Infer the connector language from metadata and the file structure.
83-
84- Args:
85- metadata: The connector metadata.
86- connector_dir: Path to the connector directory.
87-
88- Returns:
89- The inferred connector language.
90- """
91- if metadata .language is not None :
92- return metadata .language
93-
94- if (connector_dir / "setup.py" ).exists () or (connector_dir / "pyproject.toml" ).exists ():
95- return ConnectorLanguage .PYTHON
96-
97- if (connector_dir / "build.gradle" ).exists ():
98- return ConnectorLanguage .JAVA
99-
100- if any ((connector_dir / f ).exists () for f in ["manifest.yaml" , "spec.yaml" , "spec.json" ]):
101- return ConnectorLanguage .LOW_CODE
10263
103- logger .warning ("Could not determine connector language, using UNKNOWN." )
104- return ConnectorLanguage .UNKNOWN
10564
10665
10766def run_docker_command (cmd : List [str ], check : bool = True ) -> Tuple [int , str , str ]:
@@ -259,8 +218,17 @@ def build_from_base_image(
259218 f"Building Docker image from base image { base_image } : { full_image_name } for platforms { platforms } "
260219 )
261220
262- with tempfile .TemporaryDirectory () as temp_dir :
263- temp_dir_path = Path (temp_dir )
221+ docker_dir = connector_dir / "build" / "docker"
222+ docker_dir .mkdir (parents = True , exist_ok = True )
223+
224+ dockerfile_path = docker_dir / "Dockerfile"
225+ dockerignore_path = docker_dir / ".dockerignore"
226+
227+ os .environ ["DOCKER_BUILDKIT" ] = "1"
228+
229+ try :
230+ main_file = get_main_file_name (connector_dir )
231+ logger .info (f"Using main file: { main_file } " )
264232
265233 dockerfile_content = f"""
266234FROM { base_image }
@@ -271,18 +239,26 @@ def build_from_base_image(
271239
272240RUN pip install .
273241
274- ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/{ get_main_file_name ( connector_dir ) } "
275- ENTRYPOINT ["python", "/airbyte/integration_code/{ get_main_file_name ( connector_dir ) } "]
242+ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/{ main_file } "
243+ ENTRYPOINT ["python", "/airbyte/integration_code/{ main_file } "]
276244"""
277245
278- dockerfile_path = temp_dir_path / "Dockerfile"
279- dockerfile_path .write_text (dockerfile_content )
246+ dockerignore_content = """
247+ **/__pycache__
248+ **/.pytest_cache
249+ **/.venv
250+ **/.coverage
251+ **/venv
252+ **/.idea
253+ **/.vscode
254+ **/.DS_Store
255+ **/node_modules
256+ **/.git
257+ build/docker
258+ """
280259
281- for item in connector_dir .iterdir ():
282- if item .is_dir ():
283- shutil .copytree (item , temp_dir_path / item .name )
284- else :
285- shutil .copy2 (item , temp_dir_path / item .name )
260+ dockerfile_path .write_text (dockerfile_content )
261+ dockerignore_path .write_text (dockerignore_content )
286262
287263 build_cmd = [
288264 "docker" ,
@@ -296,8 +272,12 @@ def build_from_base_image(
296272 f"io.airbyte.version={ metadata .dockerImageTag } " ,
297273 "--label" ,
298274 f"io.airbyte.name={ metadata .dockerRepository } " ,
275+ "--file" ,
276+ str (dockerfile_path ),
277+ "--ignorefile" ,
278+ str (dockerignore_path ),
299279 "--load" , # Load the image into the local Docker daemon
300- str (temp_dir_path ),
280+ str (connector_dir ),
301281 ]
302282
303283 try :
@@ -307,6 +287,26 @@ def build_from_base_image(
307287 except subprocess .CalledProcessError as e :
308288 logger .error (f"Failed to build image: { e } " )
309289 raise
290+ finally :
291+ if dockerfile_path .exists ():
292+ try :
293+ dockerfile_path .unlink ()
294+ logger .debug (f"Cleaned up temporary file: { dockerfile_path } " )
295+ except Exception as e :
296+ logger .warning (f"Failed to clean up temporary file { dockerfile_path } : { e } " )
297+
298+ if dockerignore_path .exists ():
299+ try :
300+ dockerignore_path .unlink ()
301+ logger .debug (f"Cleaned up temporary file: { dockerignore_path } " )
302+ except Exception as e :
303+ logger .warning (f"Failed to clean up temporary file { dockerignore_path } : { e } " )
304+
305+ try :
306+ docker_dir .rmdir ()
307+ logger .debug (f"Removed empty directory: { docker_dir } " )
308+ except Exception :
309+ pass
310310
311311
312312def verify_image (image_name : str ) -> bool :
@@ -341,26 +341,28 @@ def verify_image(image_name: str) -> bool:
341341 return True
342342
343343
344- def run_command (args : List [ str ] ) -> int :
344+ def run_command (connector_dir : Path , tag : str , no_verify : bool , verbose : bool ) -> int :
345345 """Run the build command with the given arguments.
346346
347347 Args:
348- args: Command line arguments.
348+ connector_dir: Path to the connector directory.
349+ tag: Tag to apply to the built image.
350+ no_verify: Whether to skip verification of the built image.
351+ verbose: Whether to enable verbose logging.
349352
350353 Returns:
351354 Exit code (0 for success, non-zero for failure).
352355 """
353356 try :
354- parsed_args = parse_args (args )
355- set_up_logging (parsed_args .verbose )
357+ set_up_logging (verbose )
356358
357359 if not verify_docker_installation ():
358360 logger .error (
359361 "Docker is not installed or not running. Please install Docker and try again."
360362 )
361363 return 1
362364
363- connector_dir = Path ( parsed_args . connector_dir ) .absolute ()
365+ connector_dir = connector_dir .absolute ()
364366
365367 if not connector_dir .exists ():
366368 logger .error (f"Connector directory not found: { connector_dir } " )
@@ -374,23 +376,20 @@ def run_command(args: List[str]) -> int:
374376 logger .error (f"Error reading connector metadata: { e } " )
375377 return 1
376378
377- language = infer_connector_language (metadata , connector_dir )
378- logger .info (f"Detected connector language: { language } " )
379-
380379 try :
381380 platforms = "linux/amd64,linux/arm64"
382381 logger .info (f"Building for platforms: { platforms } " )
383382
384383 if metadata .connectorBuildOptions and metadata .connectorBuildOptions .baseImage :
385384 image_name = build_from_base_image (
386- connector_dir , metadata , parsed_args . tag , platforms
385+ connector_dir , metadata , tag , platforms
387386 )
388387 else :
389388 image_name = build_from_dockerfile (
390- connector_dir , metadata , parsed_args . tag , platforms
389+ connector_dir , metadata , tag , platforms
391390 )
392391
393- if not parsed_args . no_verify :
392+ if not no_verify :
394393 if verify_image (image_name ):
395394 logger .info (f"Build completed successfully: { image_name } " )
396395 return 0
@@ -407,7 +406,7 @@ def run_command(args: List[str]) -> int:
407406
408407 except Exception as e :
409408 logger .error (f"Unexpected error: { e } " )
410- if parsed_args and parsed_args . verbose :
409+ if verbose :
411410 import traceback
412411
413412 logger .error (traceback .format_exc ())
@@ -416,7 +415,22 @@ def run_command(args: List[str]) -> int:
416415
417416def run () -> None :
418417 """Entry point for the airbyte-cdk build command."""
419- sys .exit (run_command (sys .argv [1 :]))
418+ import argparse
419+
420+ parser = argparse .ArgumentParser (description = "Build connector Docker images" )
421+ parser .add_argument ("connector_dir" , type = str , help = "Path to the connector directory" )
422+ parser .add_argument ("--tag" , type = str , default = "dev" , help = "Tag to apply to the built image (default: dev)" )
423+ parser .add_argument ("--no-verify" , action = "store_true" , help = "Skip verification of the built image" )
424+ parser .add_argument ("--verbose" , "-v" , action = "store_true" , help = "Enable verbose logging" )
425+
426+ args = parser .parse_args (sys .argv [1 :])
427+
428+ sys .exit (run_command (
429+ connector_dir = Path (args .connector_dir ),
430+ tag = args .tag ,
431+ no_verify = args .no_verify ,
432+ verbose = args .verbose
433+ ))
420434
421435
422436if __name__ == "__main__" :
0 commit comments