Skip to content

Commit 4bd6048

Browse files
aaronsteersoctavia-squidington-iiidevin-ai-integration[bot]
authored
feat: new CLI: airbyte-cdk image build (#504)
Co-authored-by: octavia-squidington-iii <[email protected]> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 5a1644d commit 4bd6048

File tree

5 files changed

+623
-9
lines changed

5 files changed

+623
-9
lines changed

airbyte_cdk/cli/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1-
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""The `airbyte-cdk.cli` module provides command-line interfaces for the Airbyte CDK.
3+
4+
As of now, it includes the following CLI entry points:
5+
6+
- `airbyte-cdk`: Commands for working with connectors.
7+
- `source-declarative-manifest`: Directly invoke the declarative manifests connector.
8+
9+
"""

airbyte_cdk/cli/airbyte_cdk/_image.py

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,93 @@
11
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2-
"""Docker image commands.
2+
"""Airbyte CDK 'image' commands.
33
4-
Coming soon.
5-
6-
This module is planned to provide a command line interface (CLI) for building
7-
Docker images for Airbyte CDK connectors.
4+
The `airbyte-cdk image build` command provides a simple way to work with Airbyte
5+
connector images.
86
"""
97

10-
import click
8+
import sys
9+
from pathlib import Path
10+
11+
import rich_click as click
12+
13+
from airbyte_cdk.cli.airbyte_cdk._util import resolve_connector_name_and_directory
14+
from airbyte_cdk.models.connector_metadata import MetadataFile
15+
from airbyte_cdk.utils.docker import (
16+
ConnectorImageBuildError,
17+
build_connector_image,
18+
verify_docker_installation,
19+
)
1120

1221

1322
@click.group(
1423
name="image",
1524
help=__doc__.replace("\n", "\n\n"), # Render docstring as help text (markdown)
1625
)
1726
def image_cli_group() -> None:
18-
"""Docker image commands."""
19-
pass
27+
"""Commands for working with connector Docker images."""
28+
29+
30+
@image_cli_group.command()
31+
@click.option(
32+
"--connector-name",
33+
type=str,
34+
help="Name of the connector to test. Ignored if --connector-directory is provided.",
35+
)
36+
@click.option(
37+
"--connector-directory",
38+
type=click.Path(exists=True, file_okay=False, path_type=Path),
39+
help="Path to the connector directory.",
40+
)
41+
@click.option("--tag", default="dev", help="Tag to apply to the built image (default: dev)")
42+
@click.option("--no-verify", is_flag=True, help="Skip verification of the built image")
43+
def build(
44+
connector_name: str | None = None,
45+
connector_directory: Path | None = None,
46+
*,
47+
tag: str = "dev",
48+
no_verify: bool = False,
49+
) -> None:
50+
"""Build a connector Docker image.
51+
52+
This command builds a Docker image for a connector, using either
53+
the connector's Dockerfile or a base image specified in the metadata.
54+
The image is built for both AMD64 and ARM64 architectures.
55+
"""
56+
if not verify_docker_installation():
57+
click.echo(
58+
"Docker is not installed or not running. Please install Docker and try again.", err=True
59+
)
60+
sys.exit(1)
61+
62+
connector_name, connector_directory = resolve_connector_name_and_directory(
63+
connector_name=connector_name,
64+
connector_directory=connector_directory,
65+
)
66+
67+
metadata_file_path: Path = connector_directory / "metadata.yaml"
68+
try:
69+
metadata = MetadataFile.from_file(metadata_file_path)
70+
except (FileNotFoundError, ValueError) as e:
71+
click.echo(
72+
f"Error loading metadata file '{metadata_file_path}': {e!s}",
73+
err=True,
74+
)
75+
sys.exit(1)
76+
click.echo(f"Building Image for Connector: {metadata.data.dockerRepository}:{tag}")
77+
try:
78+
build_connector_image(
79+
connector_directory=connector_directory,
80+
connector_name=connector_name,
81+
metadata=metadata,
82+
tag=tag,
83+
no_verify=no_verify,
84+
)
85+
except ConnectorImageBuildError as e:
86+
click.echo(
87+
f"Error building connector image: {e!s}",
88+
err=True,
89+
)
90+
sys.exit(1)
2091

2192

2293
__all__ = [
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Models to represent the structure of a `metadata.yaml` file."""
2+
3+
from __future__ import annotations
4+
5+
from enum import Enum
6+
from pathlib import Path
7+
8+
import yaml
9+
from pydantic import BaseModel, Field
10+
11+
12+
class ConnectorLanguage(str, Enum):
13+
"""Connector implementation language."""
14+
15+
PYTHON = "python"
16+
JAVA = "java"
17+
LOW_CODE = "low-code"
18+
MANIFEST_ONLY = "manifest-only"
19+
UNKNOWN = "unknown"
20+
21+
22+
class ConnectorBuildOptions(BaseModel):
23+
"""Connector build options from metadata.yaml."""
24+
25+
model_config = {"extra": "allow"}
26+
27+
baseImage: str | None = Field(
28+
None,
29+
description="Base image to use for building the connector",
30+
)
31+
path: str | None = Field(
32+
None,
33+
description="Path to the connector code within the repository",
34+
)
35+
36+
37+
class ConnectorMetadata(BaseModel):
38+
"""Connector metadata from metadata.yaml."""
39+
40+
model_config = {"extra": "allow"}
41+
42+
dockerRepository: str = Field(..., description="Docker repository for the connector image")
43+
dockerImageTag: str = Field(..., description="Docker image tag for the connector")
44+
45+
tags: list[str] = Field(
46+
default=[],
47+
description="List of tags for the connector",
48+
)
49+
50+
@property
51+
def language(self) -> ConnectorLanguage:
52+
"""Get the connector language."""
53+
for tag in self.tags:
54+
if tag.startswith("language:"):
55+
language = tag.split(":", 1)[1]
56+
if language == "python":
57+
return ConnectorLanguage.PYTHON
58+
elif language == "java":
59+
return ConnectorLanguage.JAVA
60+
elif language == "low-code":
61+
return ConnectorLanguage.LOW_CODE
62+
elif language == "manifest-only":
63+
return ConnectorLanguage.MANIFEST_ONLY
64+
65+
return ConnectorLanguage.UNKNOWN
66+
67+
connectorBuildOptions: ConnectorBuildOptions | None = Field(
68+
None, description="Options for building the connector"
69+
)
70+
71+
72+
class MetadataFile(BaseModel):
73+
"""Represents the structure of a metadata.yaml file."""
74+
75+
model_config = {"extra": "allow"}
76+
77+
data: ConnectorMetadata = Field(..., description="Connector metadata")
78+
79+
@classmethod
80+
def from_file(
81+
cls,
82+
file_path: Path,
83+
) -> MetadataFile:
84+
"""Load metadata from a YAML file."""
85+
if not file_path.exists():
86+
raise FileNotFoundError(f"Metadata file not found: {file_path!s}")
87+
88+
metadata_content = file_path.read_text()
89+
metadata_dict = yaml.safe_load(metadata_content)
90+
91+
if not metadata_dict or "data" not in metadata_dict:
92+
raise ValueError(
93+
"Invalid metadata format: missing 'data' field in YAML file '{file_path!s}'"
94+
)
95+
96+
metadata_file = MetadataFile.model_validate(metadata_dict)
97+
return metadata_file

0 commit comments

Comments
 (0)