1
0
mirror of synced 2025-12-19 10:00:34 -05:00

chore: delete Airbyte CI Base Images Package (#64135)

This commit is contained in:
David Gold
2025-07-31 14:37:50 -07:00
committed by GitHub
parent 93c12fa3e0
commit 6042a063b8
46 changed files with 105 additions and 5976 deletions

View File

@@ -34,7 +34,6 @@ jobs:
# This list is duplicated in `pipelines/airbyte_ci/test/__init__.py`
internal_poetry_packages:
- airbyte-ci/connectors/pipelines/**
- airbyte-ci/connectors/base_images/**
- airbyte-ci/connectors/connectors_insights/**
- airbyte-ci/connectors/connector_ops/**
- airbyte-ci/connectors/connectors_qa/**

View File

@@ -9,7 +9,6 @@ The installation instructions for the `airbyte-ci` CLI tool cal be found here
| Directory | Description |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| [`base_images`](connectors/base_images) | A set of tools to build and publish Airbyte base connector images. |
| [`ci_credentials`](connectors/ci_credentials) | A CLI tool to fetch connector secrets from GCP Secrets Manager. |
| [`connector_ops`](connectors/connector_ops) | A python package with utils reused in internal packages. |
| [`connectors_qa`](connectors/connectors_qa/) | A tool to verify connectors have sounds assets and metadata. |

View File

@@ -1,158 +0,0 @@
# airbyte-connectors-base-images
This python package contains the base images used by Airbyte connectors.
It is intended to be used as a python library.
Our connector build pipeline ([`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md#L1)) uses this library to build the connector images.
Our base images are declared in code, using the [Dagger Python SDK](https://dagger-io.readthedocs.io/en/sdk-python-v0.6.4/).
- [Python base image code declaration](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/base_images/base_images/python/bases.py)
- [Java base image code declaration](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/base_images/base_images/java/bases.py)
## Where are the Dockerfiles?
Our base images are not declared using Dockerfiles.
They are declared in code using the [Dagger Python SDK](https://dagger-io.readthedocs.io/en/sdk-python-v0.6.4/).
We prefer this approach because it allows us to interact with base images container as code: we can use python to declare the base images and use the full power of the language to build and test them.
However, we do artificially generate Dockerfiles for debugging and documentation purposes.
### Example for `airbyte/python-connector-base`:
```dockerfile
FROM docker.io/python:3.11.13-slim-bookworm@sha256:139020233cc412efe4c8135b0efe1c7569dc8b28ddd88bddb109b764f8977e30
RUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime
RUN adduser --uid 1000 --system --group --no-create-home airbyte
RUN mkdir --mode 755 /custom_cache
RUN mkdir --mode 755 /airbyte
RUN chown airbyte:airbyte /airbyte
ENV PIP_CACHE_DIR=/custom_cache/pip
RUN pip install --upgrade pip==24.0 setuptools==70.0.0
ENV POETRY_VIRTUALENVS_CREATE=false
ENV POETRY_VIRTUALENVS_IN_PROJECT=false
ENV POETRY_NO_INTERACTION=1
RUN pip install poetry==1.8.4
RUN sh -c apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get clean
RUN sh -c apt-get install -y socat=1.7.4.4-2
RUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+deb12u1
RUN mkdir -p 755 /usr/share/nltk_data
```
### Example for `airbyte/java-connector-base`:
```dockerfile
```
## Base images
### `airbyte/python-connector-base`
| Version | Published | Docker Image Address | Changelog |
|---------|-----------|--------------|-----------|
| 4.0.2 | ✅| | Bump to Python 3.11.13 to patch security vulnerabilities |
| 4.0.1-rc.1 | ✅| docker.io/airbyte/python-connector-base:4.0.1-rc.1@sha256:5d1d8216467b29cf8a65d42e9c3316cdb5e62529ec3bcb14cfa993f5221316ed | Bump to Python 3.11.13 to patch security vulnerabilities |
| 4.0.0 | ✅| docker.io/airbyte/python-connector-base:4.0.0@sha256:d9894b6895923b379f3006fa251147806919c62b7d9021b5cd125bb67d7bbe22 | Python 3.11.11 |
| 4.0.0-rc.1 | ✅| docker.io/airbyte/python-connector-base:4.0.0-rc.1@sha256:0f3b3baab5ee7a040dd9f01b436afabf023b86616e21215ef7467e0501b463ea | |
| 3.0.2 | ✅| docker.io/airbyte/python-connector-base:3.0.2@sha256:73697fbe1c0e2ebb8ed58e2268484bb4bfb2cb56b653808e1680cbc50bafef75 | |
| 3.0.1-rc.1 | ✅| docker.io/airbyte/python-connector-base:3.0.1-rc.1@sha256:5b5cbe613691cc61d643a347ee4ba21d6358252f274ebb040ef820c7b9f4a5a7 | |
| 3.0.0 | ✅| docker.io/airbyte/python-connector-base:3.0.0@sha256:1a0845ff2b30eafa793c6eee4e8f4283c2e52e1bbd44eed6cb9e9abd5d34d844 | Create airbyte user |
| 3.0.0-rc.1 | ✅| docker.io/airbyte/python-connector-base:3.0.0-rc.1@sha256:ee046486af9ad90b1b248afe5e92846b51375a21463dff1cd377c4f06abb55b5 | Update Python 3.10.4 image + create airbyte user |
| 2.0.0 | ✅| docker.io/airbyte/python-connector-base:2.0.0@sha256:c44839ba84406116e8ba68722a0f30e8f6e7056c726f447681bb9e9ece8bd916 | Use Python 3.10 |
| 1.2.3 | ✅| docker.io/airbyte/python-connector-base:1.2.3@sha256:a8abfdc75f8e22931657a1ae15069e7b925e74bb7b5ef36371a85e4caeae5696 | Use latest root image version and update system packages |
| 1.2.2 | ✅| docker.io/airbyte/python-connector-base:1.2.2@sha256:57703de3b4c4204bd68a7b13c9300f8e03c0189bffddaffc796f1da25d2dbea0 | Fix Python 3.9.19 image digest |
| 1.2.2-rc.1 | ✅| docker.io/airbyte/python-connector-base:1.2.2-rc.1@sha256:a8abfdc75f8e22931657a1ae15069e7b925e74bb7b5ef36371a85e4caeae5696 | Create an airbyte user and use it |
| 1.2.1 | ✅| docker.io/airbyte/python-connector-base:1.2.1@sha256:4a4255e2bccab71fa5912487e42d9755cdecffae77273fed8be01a081cd6e795 | Upgrade to Python 3.9.19 + update pip and setuptools |
| 1.2.0 | ✅| docker.io/airbyte/python-connector-base:1.2.0@sha256:c22a9d97464b69d6ef01898edf3f8612dc11614f05a84984451dde195f337db9 | Add CDK system dependencies: nltk data, tesseract, poppler. |
| 1.2.0-rc.1 | ✅| docker.io/airbyte/python-connector-base:1.2.0-rc.1@sha256:f6467768b75fb09125f6e6b892b6b48c98d9fe085125f3ff4adc722afb1e5b30 | |
| 1.1.0 | ✅| docker.io/airbyte/python-connector-base:1.1.0@sha256:bd98f6505c6764b1b5f99d3aedc23dfc9e9af631a62533f60eb32b1d3dbab20c | Install socat |
| 1.0.0 | ✅| docker.io/airbyte/python-connector-base:1.0.0@sha256:dd17e347fbda94f7c3abff539be298a65af2d7fc27a307d89297df1081a45c27 | Initial release: based on Python 3.9.18, on slim-bookworm system, with pip==23.2.1 and poetry==1.6.1 |
### `airbyte/java-connector-base`
| Version | Published | Docker Image Address | Changelog |
|---------|-----------|--------------|-----------|
| 2.1.0-test-1 | ✅| docker.io/airbyte/java-connector-base:2.1.0-test-1@sha256:68ae2f8041efa3df55395013a192e35480f7a8129c9c11ec8924f8a39d55ffe6 | |
| 2.1.0-test | ✅| docker.io/airbyte/java-connector-base:2.1.0-test@sha256:14c7ea45c1f83e5a16f02fef54d3b7d855882eb050360c10ce2dcececfdad373 | |
| 2.0.3 | ✅| docker.io/airbyte/java-connector-base:2.0.3@sha256:119b8506bca069bbc8357a275936c7e2b0994e6947b81f1bf8d6ce9e16db7d47 | |
| 2.0.2 | ✅| docker.io/airbyte/java-connector-base:2.0.2@sha256:f8e47304842a2c4d75ac223cf4b3c4117aa1c5c9207149369d296616815fe5b0 | |
| 2.0.1 | ✅| docker.io/airbyte/java-connector-base:2.0.1@sha256:ec89bd1a89e825514dd2fc8730ba299a3ae1544580a078df0e35c5202c2085b3 | Bump Amazon Coretto image version for compatibility with Apple M4 architecture. |
| 2.0.0 | ✅| docker.io/airbyte/java-connector-base:2.0.0@sha256:5a1a21c75c5e1282606de9fa539ba136520abe2fbd013058e988bb0297a9f454 | ~Release non root base image~ |
| 2.0.0-rc.2 | ✅| docker.io/airbyte/java-connector-base:2.0.0-rc.2@sha256:e5543b3de4c38e9ef45dba886bad5ee319b0d7bfe921f310c788f1d4466e25eb | Fine tune permissions and reproduce platform java base implementation |
| 2.0.0-rc.1 | ✅| docker.io/airbyte/java-connector-base:2.0.0-rc.1@sha256:484b929684b9e4f60d06cde171ee0b8238802cb434403293fcede81c1e73c537 | Make the java base image non root |
| 1.0.0 | ✅| docker.io/airbyte/java-connector-base:1.0.0@sha256:be86e5684e1e6d9280512d3d8071b47153698fe08ad990949c8eeff02803201a | Create a base image for our java connectors based on Amazon Corretto. |
| 1.0.0-rc.4 | ✅| docker.io/airbyte/java-connector-base:1.0.0-rc.4@sha256:be86e5684e1e6d9280512d3d8071b47153698fe08ad990949c8eeff02803201a | Bundle yum calls in a single RUN |
| 1.0.0-rc.3 | ✅| docker.io/airbyte/java-connector-base:1.0.0-rc.3@sha256:be86e5684e1e6d9280512d3d8071b47153698fe08ad990949c8eeff02803201a | |
| 1.0.0-rc.2 | ✅| docker.io/airbyte/java-connector-base:1.0.0-rc.2@sha256:fca66e81b4d2e4869a03b57b1b34beb048e74f5d08deb2046c3bb9919e7e2273 | Set entrypoint to base.sh |
| 1.0.0-rc.1 | ✅| docker.io/airbyte/java-connector-base:1.0.0-rc.1@sha256:886a7ce7eccfe3c8fb303511d0e46b83b7edb4f28e3705818c090185ba511fe7 | Create a base image for our java connectors. |
## How to release a new base image version (example for Python)
### Requirements
* [Docker](https://docs.docker.com/get-docker/)
* [Poetry](https://python-poetry.org/docs/#installation)
* Dockerhub logins
### Steps
1. `poetry install`
2. Open `base_images/python/bases.py`.
3. Make changes to the `AirbytePythonConnectorBaseImage`, you're likely going to change the `get_container` method to change the base image.
4. Implement the `container` property which must return a `dagger.Container` object.
5. **Recommended**: Add new sanity checks to `run_sanity_check` to confirm that the new version is working as expected.
6. Cut a new base image version by running `poetry run generate-release`. You'll need your DockerHub credentials.
It will:
- Prompt you to pick which base image you'd like to publish.
- Prompt you for a major/minor/patch/pre-release version bump.
- Prompt you for a changelog message.
- Run the sanity checks on the new version.
- Optional: Publish the new version to DockerHub.
- Regenerate the docs and the registry json file.
7. Commit and push your changes.
8. Create a PR and ask for a review from the Connector Operations team.
**Please note that if you don't publish your image while cutting the new version you can publish it later with `poetry run publish <repository> <version>`.**
No connector will use the new base image version until its metadata is updated to use it.
If you're not fully confident with the new base image version please:
- please publish it as a pre-release version
- try out the new version on a couple of connectors
- cut a new version with a major/minor/patch bump and publish it
- This steps can happen in different PRs.
## Running tests locally
```bash
poetry run pytest
# Static typing checks
poetry run mypy base_images --check-untyped-defs
```
## CHANGELOG
### 1.4.0
- Declare a base image for our java connectors.
### 1.3.1
- Update the crane image address. The previous address was deleted by the maintainer.
### 1.2.0
- Improve new version prompt to pick bump type with optional pre-release version.
### 1.1.0
- Add a cache ttl for base image listing to avoid DockerHub rate limiting.
### 1.0.4
- Upgrade Dagger to `0.13.3`
### 1.0.2
- Improved support for images with non-semantic-versioned tags.
### 1.0.1
- Bumped dependencies ([#42581](https://github.com/airbytehq/airbyte/pull/42581))

View File

@@ -1,7 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from rich.console import Console
console = Console()

View File

@@ -1,121 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
"""This module declares common (abstract) classes and methods used by all base images."""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import final
import dagger
import semver
from .published_image import PublishedImage
class AirbyteConnectorBaseImage(ABC):
"""An abstract class that represents an Airbyte base image.
Please do not declare any Dagger with_exec instruction in this class as in the abstract class context we have no guarantee about the underlying system used in the base image.
"""
USER: str = "airbyte"
USER_ID: int = 1000
CACHE_DIR_PATH: str = "/custom_cache"
AIRBYTE_DIR_PATH: str = "/airbyte"
@final
def __init__(self, dagger_client: dagger.Client, version: semver.VersionInfo):
"""Initializes the Airbyte base image.
Args:
dagger_client (dagger.Client): The dagger client used to build the base image.
version (semver.VersionInfo): The version of the base image.
"""
self.dagger_client = dagger_client
self.version = version
# INSTANCE PROPERTIES:
@property
def name_with_tag(self) -> str:
"""Returns the full name of the Airbyte base image, with its tag.
Returns:
str: The full name of the Airbyte base image, with its tag.
"""
return f"{self.repository}:{self.version}"
# Child classes should define their root image if the image is indeed managed by base_images.
@property
def root_image(self) -> PublishedImage:
"""Returns the base image used to build the Airbyte base image.
Raises:
NotImplementedError: Raised if a subclass does not define a 'root_image' attribute.
Returns:
PublishedImage: The base image used to build the Airbyte base image.
"""
raise NotImplementedError("Subclasses must define a 'root_image' attribute.")
# MANDATORY SUBCLASSES ATTRIBUTES / PROPERTIES:
@property
@abstractmethod
def repository(self) -> str:
"""This is the name of the repository where the image will be hosted.
e.g: airbyte/python-connector-base
Raises:
NotImplementedError: Raised if a subclass does not define an 'repository' attribute.
Returns:
str: The repository name where the image will be hosted.
"""
raise NotImplementedError("Subclasses must define an 'repository' attribute.")
# MANDATORY SUBCLASSES METHODS:
@abstractmethod
def get_container(self, platform: dagger.Platform) -> dagger.Container:
"""Returns the container of the Airbyte connector base image."""
raise NotImplementedError("Subclasses must define a 'get_container' method.")
@abstractmethod
async def run_sanity_checks(self, platform: dagger.Platform):
"""Runs sanity checks on the base image container.
This method is called before image publication.
Args:
base_image_version (AirbyteConnectorBaseImage): The base image version on which the sanity checks should run.
Raises:
SanityCheckError: Raised if a sanity check fails.
"""
raise NotImplementedError("Subclasses must define a 'run_sanity_checks' method.")
# INSTANCE METHODS:
@final
def get_base_container(self, platform: dagger.Platform) -> dagger.Container:
"""Returns a container using the base image.
This container is used to build the Airbyte base image.
We create the user 'airbyte' with the UID 1000 and GID 1000.
Returns:
dagger.Container: The container using the base python image.
"""
return (
self.dagger_client.container(platform=platform)
.from_(self.root_image.address)
# Set the timezone to UTC
.with_exec(["ln", "-snf", "/usr/share/zoneinfo/Etc/UTC", "/etc/localtime"])
# Create the user 'airbyte' with the UID 1000 and GID 1000
.with_exec(["adduser", "--uid", str(self.USER_ID), "--system", "--group", "--no-create-home", self.USER])
# Create the cache airbyte directories and set the right permissions
.with_exec(["mkdir", "--mode", "755", self.CACHE_DIR_PATH])
.with_exec(["mkdir", "--mode", "755", self.AIRBYTE_DIR_PATH])
# Change the owner of the airbyte directory to the user 'airbyte'
.with_exec(["chown", f"{self.USER}:{self.USER}", self.AIRBYTE_DIR_PATH])
)

View File

@@ -1,198 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import argparse
import sys
from typing import Callable, Type
import anyio
import dagger
import inquirer # type: ignore
import semver
from jinja2 import Environment, FileSystemLoader
from base_images import bases, console, consts, errors, hacks, publish, utils, version_registry
async def _generate_docs(dagger_client: dagger.Client):
"""This function will generate the README.md file from the templates/README.md.j2 template.
It will first load all the registries to render the template with up to date information.
"""
docker_credentials = utils.docker.get_credentials()
env = Environment(loader=FileSystemLoader("base_images/templates"))
template = env.get_template("README.md.j2")
rendered_template = template.render({"registries": await version_registry.get_all_registries(dagger_client, docker_credentials)})
with open("README.md", "w") as readme:
readme.write(rendered_template)
console.log("README.md generated successfully.")
async def _generate_release(dagger_client: dagger.Client):
"""This function will cut a new version on top of the previous one. It will prompt the user for release details: version bump, changelog entry.
The user can optionally publish the new version to our remote registry.
If the version is not published its changelog entry is still persisted.
It can later be published by running the publish command.
In the future we might only allow publishing new pre-release versions from this flow.
"""
docker_credentials = utils.docker.get_credentials()
select_base_image_class_answers = inquirer.prompt(
[
inquirer.List(
"BaseImageClass",
message="Which base image would you like to release a new version for?",
choices=[(BaseImageClass.repository, BaseImageClass) for BaseImageClass in version_registry.MANAGED_BASE_IMAGES],
)
]
)
BaseImageClass = select_base_image_class_answers["BaseImageClass"]
registry = await version_registry.VersionRegistry.load(BaseImageClass, dagger_client, docker_credentials)
latest_entry = registry.latest_entry
# If theres in no latest entry, it means we have no version yet: the registry is empty
# New version will be cut on top of 0.0.0 so this one will actually never be published
seed_version = semver.VersionInfo.parse("0.0.0")
if latest_entry is None:
latest_version = seed_version
else:
latest_version = latest_entry.version
if latest_version != seed_version and not latest_entry.published: # type: ignore
console.log(
f"The latest version of {BaseImageClass.repository} ({latest_version}) has not been published yet. Please publish it first before cutting a new version."
)
sys.exit(1)
new_version_answers = inquirer.prompt(
[
inquirer.List(
"new_version",
message=f"Which kind of new version would you like to cut? (latest version is {latest_version}))",
choices=[
("prerelease", latest_version.bump_prerelease()),
("finalize", latest_version.finalize_version()),
("patch", latest_version.bump_patch()),
("patch-prerelease", latest_version.bump_patch().bump_prerelease()),
("minor", latest_version.bump_minor()),
("minor-prerelease", latest_version.bump_minor().bump_prerelease()),
("major", latest_version.bump_major()),
("major-prerelease", latest_version.bump_major().bump_prerelease()),
],
),
inquirer.Text("changelog_entry", message="What should the changelog entry be?", validate=lambda _, entry: len(entry) > 0),
inquirer.Confirm("publish_now", message="Would you like to publish it to our remote registry now?"),
]
)
new_version, changelog_entry, publish_now = (
new_version_answers["new_version"],
new_version_answers["changelog_entry"],
new_version_answers["publish_now"],
)
base_image_version = BaseImageClass(dagger_client, new_version)
try:
await publish.run_sanity_checks(base_image_version)
console.log("Sanity checks passed.")
except errors.SanityCheckError as e:
console.log(f"Sanity checks failed: {e}")
console.log("Aborting.")
sys.exit(1)
dockerfile_example = hacks.get_container_dockerfile(base_image_version.get_container(consts.PLATFORMS_WE_PUBLISH_FOR[0]))
# Add this step we can create a changelog entry: sanity checks passed, image built successfully and sanity checks passed.
changelog_entry = version_registry.ChangelogEntry(new_version, changelog_entry, dockerfile_example)
if publish_now:
published_docker_image = await publish.publish_to_remote_registry(base_image_version)
console.log(f"Published {published_docker_image.address} successfully.")
else:
published_docker_image = None
console.log(
f"Skipping publication. You can publish it later by running `poetry run publish {base_image_version.repository} {new_version}`."
)
new_registry_entry = version_registry.VersionRegistryEntry(published_docker_image, changelog_entry, new_version)
registry.add_entry(new_registry_entry)
console.log(f"Added {new_version} to the registry.")
await _generate_docs(dagger_client)
console.log("Generated docs successfully.")
async def _publish(
dagger_client: dagger.Client, BaseImageClassToPublish: Type[bases.AirbyteConnectorBaseImage], version: semver.VersionInfo
):
"""This function will publish a specific version of a base image to our remote registry.
Users are prompted for confirmation before overwriting an existing version.
If the version does not exist in the registry, the flow is aborted and user is suggested to cut a new version first.
"""
docker_credentials = utils.docker.get_credentials()
registry = await version_registry.VersionRegistry.load(BaseImageClassToPublish, dagger_client, docker_credentials)
registry_entry = registry.get_entry_for_version(version)
if not registry_entry:
console.log(f"No entry found for version {version} in the registry. Please cut a new version first: `poetry run generate-release`")
sys.exit(1)
if registry_entry.published:
force_answers = inquirer.prompt(
[
inquirer.Confirm(
"force", message="This version has already been published to our remote registry. Would you like to overwrite it?"
),
]
)
if not force_answers["force"]:
console.log("Not overwriting the already exiting image.")
sys.exit(0)
base_image_version = BaseImageClassToPublish(dagger_client, version)
published_docker_image = await publish.publish_to_remote_registry(base_image_version)
console.log(f"Published {published_docker_image.address} successfully.")
await _generate_docs(dagger_client)
console.log("Generated docs successfully.")
async def execute_async_command(command_fn: Callable, *args, **kwargs):
"""This is a helper function that will execute a command function in an async context, required by the use of Dagger."""
# NOTE: Dagger logs using Rich now, and two rich apps don't play well with each other.
# Logging into a file makes the CLI experience tolerable.
async with dagger.Connection(dagger.Config(log_output=open("dagger.log", "w"))) as dagger_client:
await command_fn(dagger_client, *args, **kwargs)
def generate_docs():
"""This command will generate the README.md file from the templates/README.md.j2 template.
It will first load all the registries to render the template with up to date information.
"""
anyio.run(execute_async_command, _generate_docs)
def generate_release():
"""This command will cut a new version on top of the previous one. It will prompt the user for release details: version bump, changelog entry.
The user can optionally publish the new version to our remote registry.
If the version is not published its changelog entry is still persisted.
It can later be published by running the publish command.
In the future we might only allow publishing new pre-release versions from this flow.
"""
anyio.run(execute_async_command, _generate_release)
def publish_existing_version():
"""This command is intended to be used when:
- We have a changelog entry for a new version but it's not published yet (for future publish on merge flows).
- We have a good reason to overwrite an existing version in the remote registry.
"""
parser = argparse.ArgumentParser(description="Publish a specific version of a base image to our remote registry.")
parser.add_argument("repository", help="The base image repository name")
parser.add_argument("version", help="The version to publish")
args = parser.parse_args()
version = semver.VersionInfo.parse(args.version)
BaseImageClassToPublish = None
for BaseImageClass in version_registry.MANAGED_BASE_IMAGES:
if BaseImageClass.repository == args.repository:
BaseImageClassToPublish = BaseImageClass
if BaseImageClassToPublish is None:
console.log(f"Unknown base image name: {args.repository}")
sys.exit(1)
anyio.run(execute_async_command, _publish, BaseImageClassToPublish, version)

View File

@@ -1,11 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
"""This module declares constants used by the base_images module."""
import dagger
REMOTE_REGISTRY = "docker.io"
PLATFORMS_WE_PUBLISH_FOR = (dagger.Platform("linux/amd64"), dagger.Platform("linux/arm64"))

View File

@@ -1,16 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
"""This module contains the exceptions used by the base_images module."""
from typing import Union
import dagger
class SanityCheckError(Exception):
"""Raised when a sanity check fails."""
def __init__(self, error: Union[str, dagger.ExecError], *args: object) -> None:
super().__init__(error, *args)

View File

@@ -1,39 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import dagger
# If we perform addition dagger operations on the container, we need to make sure that a mapping exists for the new field name.
DAGGER_FIELD_NAME_TO_DOCKERFILE_INSTRUCTION = {
"from": lambda field: f'FROM {field.args.get("address")}',
"withExec": lambda field: f'RUN {" ".join(field.args.get("args"))}',
"withEnvVariable": lambda field: f'ENV {field.args.get("name")}={field.args.get("value")}',
"withLabel": lambda field: f'LABEL {field.args.get("name")}={field.args.get("value")}',
}
def get_container_dockerfile(container) -> str:
"""Returns the Dockerfile of the base image container.
Disclaimer: THIS IS HIGHLY EXPERIMENTAL, HACKY AND BRITTLE.
TODO: CONFIRM WITH THE DAGGER TEAM WHAT CAN GO WRONG HERE.
Returns:
str: The Dockerfile of the base image container.
"""
lineage = [
field
for field in list(container._ctx.selections)
if isinstance(field, dagger.client._core.Field) and field.type_name == "Container"
]
dockerfile = []
for field in lineage:
if field.name in DAGGER_FIELD_NAME_TO_DOCKERFILE_INSTRUCTION:
try:
dockerfile.append(DAGGER_FIELD_NAME_TO_DOCKERFILE_INSTRUCTION[field.name](field))
except KeyError:
raise KeyError(
f"Unknown field name: {field.name}, please add it to the DAGGER_FIELD_NAME_TO_DOCKERFILE_INSTRUCTION mapping."
)
return "\n".join(dockerfile)

View File

@@ -1,3 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#

View File

@@ -1,129 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from __future__ import annotations
from typing import Callable, Final
import dagger
from base_images import bases, published_image
from base_images import sanity_checks as base_sanity_checks
from base_images.root_images import AMAZON_CORRETTO_21_AL_2023
from base_images.utils.dagger import sh_dash_c
class AirbyteJavaConnectorBaseImage(bases.AirbyteConnectorBaseImage):
root_image: Final[published_image.PublishedImage] = AMAZON_CORRETTO_21_AL_2023
repository: Final[str] = "airbyte/java-connector-base"
DD_AGENT_JAR_URL: Final[str] = "https://dtdg.co/latest-java-tracer"
BASE_SCRIPT_URL = "https://raw.githubusercontent.com/airbytehq/airbyte/6d8a3a2bc4f4ca79f10164447a90fdce5c9ad6f9/airbyte-integrations/bases/base/base.sh"
JAVA_BASE_SCRIPT_URL: Final[str] = (
"https://raw.githubusercontent.com/airbytehq/airbyte/6d8a3a2bc4f4ca79f10164447a90fdce5c9ad6f9/airbyte-integrations/bases/base-java/javabase.sh"
)
def get_container(self, platform: dagger.Platform) -> dagger.Container:
"""Returns the container used to build the base image for java connectors
We currently use the Amazon coretto image as a base.
We install some packages required to build java connectors.
We also download the datadog java agent jar and the javabase.sh script.
We set some env variables used by the javabase.sh script.
Args:
platform (dagger.Platform): The platform this container should be built for.
Returns:
dagger.Container: The container used to build the base image.
"""
return (
self.dagger_client.container(platform=platform)
.from_(self.root_image.address)
# Bundle RUN commands together to reduce the number of layers.
.with_exec(
sh_dash_c(
[
# Shadow-utils is required to add a user with a specific UID and GID.
# tar is equired to untar java connector binary distributions.
# openssl is required because we need to ssh and scp sometimes.
# findutils is required for xargs, which is shipped as part of findutils.
f"yum install -y shadow-utils tar openssl findutils",
# Update first, but in the same .with_exec step as the package installation.
# Otherwise, we risk caching stale package URLs.
"yum update -y --security",
# Remove any dangly bits.
"yum clean all",
# Remove the yum cache to reduce the image size.
"rm -rf /var/cache/yum",
# Create the group 'airbyte' with the GID 1000
f"groupadd --gid {self.USER_ID} {self.USER}",
# Create the user 'airbyte' with the UID 1000
f"useradd --uid {self.USER_ID} --gid {self.USER} --shell /bin/bash --create-home {self.USER}",
# Create mount point for secrets and configs
"mkdir /secrets",
"mkdir /config",
# Create the cache airbyte directories and set the right permissions
f"mkdir --mode 755 {self.AIRBYTE_DIR_PATH}",
f"mkdir --mode 755 {self.CACHE_DIR_PATH}",
# Change the owner of the airbyte directory to the user 'airbyte'
f"chown -R {self.USER}:{self.USER} {self.AIRBYTE_DIR_PATH}",
f"chown -R {self.USER}:{self.USER} {self.CACHE_DIR_PATH}",
f"chown -R {self.USER}:{self.USER} /secrets",
f"chown -R {self.USER}:{self.USER} /config",
f"chown -R {self.USER}:{self.USER} /usr/share/pki/ca-trust-source",
f"chown -R {self.USER}:{self.USER} /etc/pki/ca-trust",
f"chown -R {self.USER}:{self.USER} /tmp",
]
)
)
.with_workdir(self.AIRBYTE_DIR_PATH)
# Copy the datadog java agent jar from the internet.
.with_file("dd-java-agent.jar", self.dagger_client.http(self.DD_AGENT_JAR_URL), owner=self.USER)
# Copy base.sh from the git repo.
.with_file("base.sh", self.dagger_client.http(self.BASE_SCRIPT_URL), owner=self.USER)
# Copy javabase.sh from the git repo.
.with_file("javabase.sh", self.dagger_client.http(self.JAVA_BASE_SCRIPT_URL), owner=self.USER)
# Set a bunch of env variables used by base.sh.
.with_env_variable("AIRBYTE_SPEC_CMD", "/airbyte/javabase.sh --spec")
.with_env_variable("AIRBYTE_CHECK_CMD", "/airbyte/javabase.sh --check")
.with_env_variable("AIRBYTE_DISCOVER_CMD", "/airbyte/javabase.sh --discover")
.with_env_variable("AIRBYTE_READ_CMD", "/airbyte/javabase.sh --read")
.with_env_variable("AIRBYTE_WRITE_CMD", "/airbyte/javabase.sh --write")
.with_env_variable("AIRBYTE_ENTRYPOINT", "/airbyte/base.sh")
.with_entrypoint(["/airbyte/base.sh"])
.with_user(self.USER)
)
async def run_sanity_checks(self, platform: dagger.Platform):
"""Runs sanity checks on the base image container.
This method is called before image publication.
Consider it like a pre-flight check before take-off to the remote registry.
Args:
platform (dagger.Platform): The platform on which the sanity checks should run.
"""
container = await self.get_container(platform)
for expected_rw_dir in [
self.AIRBYTE_DIR_PATH,
self.CACHE_DIR_PATH,
"/tmp",
"/secrets",
"/config",
"/usr/share/pki/ca-trust-source",
"/etc/pki/ca-trust",
]:
await base_sanity_checks.check_user_can_write_dir(container, self.USER, expected_rw_dir)
await base_sanity_checks.check_user_can_read_dir(container, self.USER, expected_rw_dir)
await base_sanity_checks.check_user_uid_guid(container, self.USER, self.USER_ID, self.USER_ID)
await base_sanity_checks.check_file_exists(container, "/airbyte/dd-java-agent.jar")
await base_sanity_checks.check_file_exists(container, "/airbyte/base.sh")
await base_sanity_checks.check_file_exists(container, "/airbyte/javabase.sh")
await base_sanity_checks.check_env_var_with_printenv(container, "AIRBYTE_SPEC_CMD", "/airbyte/javabase.sh --spec")
await base_sanity_checks.check_env_var_with_printenv(container, "AIRBYTE_CHECK_CMD", "/airbyte/javabase.sh --check")
await base_sanity_checks.check_env_var_with_printenv(container, "AIRBYTE_DISCOVER_CMD", "/airbyte/javabase.sh --discover")
await base_sanity_checks.check_env_var_with_printenv(container, "AIRBYTE_READ_CMD", "/airbyte/javabase.sh --read")
await base_sanity_checks.check_env_var_with_printenv(container, "AIRBYTE_WRITE_CMD", "/airbyte/javabase.sh --write")
await base_sanity_checks.check_env_var_with_printenv(container, "AIRBYTE_ENTRYPOINT", "/airbyte/base.sh")
await base_sanity_checks.check_a_command_is_available_using_version_option(container, "tar")
await base_sanity_checks.check_a_command_is_available_using_version_option(container, "openssl", "version")

View File

@@ -1,35 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import dagger
from base_images import bases, consts, published_image
async def run_sanity_checks(base_image_version: bases.AirbyteConnectorBaseImage):
for platform in consts.PLATFORMS_WE_PUBLISH_FOR:
await base_image_version.run_sanity_checks(platform)
async def publish_to_remote_registry(base_image_version: bases.AirbyteConnectorBaseImage) -> published_image.PublishedImage:
"""Publishes a base image to the remote registry.
Args:
base_image_version (common.AirbyteConnectorBaseImage): The base image to publish.
Returns:
models.PublishedImage: The published image as a PublishedImage instance.
"""
address = f"{consts.REMOTE_REGISTRY}/{base_image_version.repository}:{base_image_version.version}"
variants_to_publish = []
for platform in consts.PLATFORMS_WE_PUBLISH_FOR:
await base_image_version.run_sanity_checks(platform)
variants_to_publish.append(base_image_version.get_container(platform))
# Publish with forced compression to ensure backward compatibility with older versions of docker
published_address = await variants_to_publish[0].publish(
address, platform_variants=variants_to_publish[1:], forced_compression=dagger.ImageLayerCompression.Gzip
)
return published_image.PublishedImage.from_address(published_address)

View File

@@ -1,47 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from __future__ import annotations
from dataclasses import dataclass
import semver
@dataclass
class PublishedImage:
registry: str
repository: str
tag: str
sha: str
@property
def address(self) -> str:
return f"{self.registry}/{self.repository}:{self.tag}@sha256:{self.sha}"
@classmethod
def from_address(cls, address: str) -> PublishedImage:
"""Creates a PublishedImage instance from a docker image address.
A docker image address is a string of the form:
registry/repository:tag@sha256:sha
Args:
address (str): _description_
Returns:
PublishedImage: _description_
"""
parts = address.split("/")
registry = parts.pop(0)
without_registry = "/".join(parts)
repository, tag, sha = without_registry.replace("@sha256", "").split(":")
return cls(registry, repository, tag, sha)
@property
def name_with_tag(self) -> str:
return f"{self.repository}:{self.tag}"
@property
def version(self) -> semver.VersionInfo:
return semver.VersionInfo.parse(self.tag)

View File

@@ -1,3 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#

View File

@@ -1,139 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from __future__ import annotations
from typing import Callable, Final
import dagger
from base_images import bases, published_image
from base_images import sanity_checks as base_sanity_checks
from base_images.python import sanity_checks as python_sanity_checks
from base_images.root_images import PYTHON_3_11_13
class AirbyteManifestOnlyConnectorBaseImage(bases.AirbyteConnectorBaseImage):
"""ManifestOnly base image class, only used to fetch the registry."""
repository: Final[str] = "airbyte/source-declarative-manifest"
class AirbytePythonConnectorBaseImage(bases.AirbyteConnectorBaseImage):
root_image: Final[published_image.PublishedImage] = PYTHON_3_11_13
repository: Final[str] = "airbyte/python-connector-base"
pip_cache_name: Final[str] = "pip_cache"
nltk_data_path: Final[str] = "/usr/share/nltk_data"
ntlk_data = {
"tokenizers": {"https://github.com/nltk/nltk_data/raw/5db857e6f7df11eabb5e5665836db9ec8df07e28/packages/tokenizers/punkt.zip"},
"taggers": {
"https://github.com/nltk/nltk_data/raw/5db857e6f7df11eabb5e5665836db9ec8df07e28/packages/taggers/averaged_perceptron_tagger.zip"
},
}
@property
def pip_cache_path(self) -> str:
return f"{self.CACHE_DIR_PATH}/pip"
def install_cdk_system_dependencies(self) -> Callable:
def get_nltk_data_dir() -> dagger.Directory:
"""Returns a dagger directory containing the nltk data.
Returns:
dagger.Directory: A dagger directory containing the nltk data.
"""
data_container = self.dagger_client.container().from_("bash:latest")
for nltk_data_subfolder, nltk_data_urls in self.ntlk_data.items():
full_nltk_data_path = f"{self.nltk_data_path}/{nltk_data_subfolder}"
for nltk_data_url in nltk_data_urls:
zip_file = self.dagger_client.http(nltk_data_url)
data_container = (
data_container.with_file("/tmp/data.zip", zip_file)
.with_exec(["mkdir", "-p", full_nltk_data_path])
.with_exec(["unzip", "-o", "/tmp/data.zip", "-d", full_nltk_data_path])
.with_exec(["rm", "/tmp/data.zip"])
)
return data_container.directory(self.nltk_data_path)
def with_tesseract_and_poppler(container: dagger.Container) -> dagger.Container:
"""
Installs Tesseract-OCR and Poppler-utils in the base image.
These tools are necessary for OCR (Optical Character Recognition) processes and working with PDFs, respectively.
"""
container = container.with_exec(
["sh", "-c", "apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+deb12u1"]
)
return container
def with_file_based_connector_dependencies(container: dagger.Container) -> dagger.Container:
"""
Installs the dependencies for file-based connectors. This includes:
- tesseract-ocr
- poppler-utils
- nltk data
"""
container = with_tesseract_and_poppler(container)
container = container.with_exec(["mkdir", "-p", "755", self.nltk_data_path]).with_directory(
self.nltk_data_path, get_nltk_data_dir()
)
return container
return with_file_based_connector_dependencies
def get_container(self, platform: dagger.Platform) -> dagger.Container:
"""Returns the container used to build the base image.
We currently use the python:3.9.18-slim-bookworm image as a base.
We set the container system timezone to UTC.
We then upgrade pip and install poetry.
Args:
platform (dagger.Platform): The platform this container should be built for.
Returns:
dagger.Container: The container used to build the base image.
"""
pip_cache_volume: dagger.CacheVolume = self.dagger_client.cache_volume(AirbytePythonConnectorBaseImage.pip_cache_name)
return (
self.get_base_container(platform)
.with_mounted_cache(self.pip_cache_path, pip_cache_volume, owner=self.USER)
.with_env_variable("PIP_CACHE_DIR", self.pip_cache_path)
# Upgrade pip to the expected version
.with_exec(["pip", "install", "--upgrade", "pip==24.0", "setuptools==70.0.0"])
# Declare poetry specific environment variables
.with_env_variable("POETRY_VIRTUALENVS_CREATE", "false")
.with_env_variable("POETRY_VIRTUALENVS_IN_PROJECT", "false")
.with_env_variable("POETRY_NO_INTERACTION", "1")
.with_exec(["pip", "install", "poetry==1.8.4"])
.with_exec(["sh", "-c", "apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get clean"])
.with_exec(["sh", "-c", "apt-get install -y socat=1.7.4.4-2"])
# Install CDK system dependencies
.with_(self.install_cdk_system_dependencies())
)
async def run_sanity_checks(self, platform: dagger.Platform):
"""Runs sanity checks on the base image container.
This method is called before image publication.
Consider it like a pre-flight check before take-off to the remote registry.
Args:
platform (dagger.Platform): The platform on which the sanity checks should run.
"""
container = self.get_container(platform)
await base_sanity_checks.check_timezone_is_utc(container)
await base_sanity_checks.check_a_command_is_available_using_version_option(container, "bash")
await python_sanity_checks.check_python_version(container, "3.11.13")
await python_sanity_checks.check_pip_version(container, "24.0")
await base_sanity_checks.check_user_exists(container, self.USER, expected_uid=self.USER_ID, expected_gid=self.USER_ID)
await base_sanity_checks.check_user_can_read_dir(container, self.USER, self.AIRBYTE_DIR_PATH)
await base_sanity_checks.check_user_can_read_dir(container, self.USER, self.nltk_data_path)
await base_sanity_checks.check_user_can_read_dir(container, self.USER, self.CACHE_DIR_PATH)
await base_sanity_checks.check_user_can_write_dir(container, self.USER, self.AIRBYTE_DIR_PATH)
await python_sanity_checks.check_poetry_version(container, "1.8.4")
await python_sanity_checks.check_python_image_has_expected_env_vars(container)
await base_sanity_checks.check_a_command_is_available_using_version_option(container, "socat", "-V")
await base_sanity_checks.check_socat_version(container, "1.7.4.4")
await python_sanity_checks.check_cdk_system_dependencies(container)

View File

@@ -1,145 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import dagger
from base_images import errors
from base_images import sanity_checks as base_sanity_checks
async def check_python_version(container: dagger.Container, expected_python_version: str):
"""Checks that the python version is the expected one.
Args:
container (dagger.Container): The container on which the sanity checks should run.
expected_python_version (str): The expected python version.
Raises:
errors.SanityCheckError: Raised if the python --version command could not be executed or if the outputted version is not the expected one.
"""
try:
python_version_output: str = await container.with_exec(["python", "--version"]).stdout()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
if python_version_output != f"Python {expected_python_version}\n":
raise errors.SanityCheckError(f"unexpected python version: {python_version_output}")
async def check_pip_version(container: dagger.Container, expected_pip_version: str):
"""Checks that the pip version is the expected one.
Args:
container (dagger.Container): The container on which the sanity checks should run.
expected_pip_version (str): The expected pip version.
Raises:
errors.SanityCheckError: Raised if the pip --version command could not be executed or if the outputted version is not the expected one.
"""
try:
pip_version_output: str = await container.with_exec(["pip", "--version"]).stdout()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
if not pip_version_output.startswith(f"pip {expected_pip_version}"):
raise errors.SanityCheckError(f"unexpected pip version: {pip_version_output}")
async def check_poetry_version(container: dagger.Container, expected_poetry_version: str):
"""Checks that the poetry version is the expected one.
Args:
container (dagger.Container): The container on which the sanity checks should run.
expected_poetry_version (str): The expected poetry version.
Raises:
errors.SanityCheckError: Raised if the poetry --version command could not be executed or if the outputted version is not the expected one.
"""
try:
poetry_version_output: str = await container.with_exec(["poetry", "--version"]).stdout()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
if not poetry_version_output.startswith(f"Poetry (version {expected_poetry_version}"):
raise errors.SanityCheckError(f"unexpected poetry version: {poetry_version_output}")
async def check_python_image_has_expected_env_vars(python_image_container: dagger.Container):
"""Check a python container has the set of env var we always expect on python images.
Args:
python_image_container (dagger.Container): The container on which the sanity checks should run.
"""
expected_env_vars = {
"PYTHON_VERSION",
"HOME",
"PATH",
"LANG",
"GPG_KEY",
}
# It's not suboptimal to call printenv multiple times because the printenv output is cached.
for expected_env_var in expected_env_vars:
await base_sanity_checks.check_env_var_with_printenv(python_image_container, expected_env_var)
async def check_nltk_data(python_image_container: dagger.Container):
"""Install nltk and check that the required data is available.
As of today the required data is:
- taggers/averaged_perceptron_tagger
- tokenizers/punkt
Args:
python_image_container (dagger.Container): The container on which the sanity checks should run.
Raises:
errors.SanityCheckError: Raised if the nltk data is not available.
"""
with_nltk = await python_image_container.with_exec(["pip", "install", "nltk==3.8.1"])
try:
await with_nltk.with_exec(
["python", "-c", 'import nltk;nltk.data.find("taggers/averaged_perceptron_tagger");nltk.data.find("tokenizers/punkt")']
)
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
async def check_tesseract_version(python_image_container: dagger.Container, tesseract_version: str):
"""Check that the tesseract version is the expected one.
Args:
python_image_container (dagger.Container): The container on which the sanity checks should run.
tesseract_version (str): The expected tesseract version.
Raises:
errors.SanityCheckError: Raised if the tesseract --version command could not be executed or if the outputted version is not the expected one.
"""
try:
tesseract_version_output = await python_image_container.with_exec(["tesseract", "--version"]).stdout()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
if not tesseract_version_output.startswith(f"tesseract {tesseract_version}"):
raise errors.SanityCheckError(f"unexpected tesseract version: {tesseract_version_output}")
async def check_poppler_utils_version(python_image_container: dagger.Container, poppler_version: str):
"""Check that the poppler version is the expected one.
The poppler version can be checked by running a pdftotext -v command.
Args:
python_image_container (dagger.Container): The container on which the sanity checks should run.
poppler_version (str): The expected poppler version.
Raises:
errors.SanityCheckError: Raised if the pdftotext -v command could not be executed or if the outputted version is not the expected one.
"""
try:
pdf_to_text_version_output = await python_image_container.with_exec(["pdftotext", "-v"]).stderr()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
if f"pdftotext version {poppler_version}" not in pdf_to_text_version_output:
raise errors.SanityCheckError(f"unexpected poppler version: {pdf_to_text_version_output}")
async def check_cdk_system_dependencies(python_image_container: dagger.Container):
await check_nltk_data(python_image_container)
await check_tesseract_version(python_image_container, "5.3.0")
await check_poppler_utils_version(python_image_container, "22.12.0")

View File

@@ -1,48 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from .published_image import PublishedImage
PYTHON_3_9_18 = PublishedImage(
registry="docker.io",
repository="python",
tag="3.9.18-slim-bookworm",
sha="44b7f161ed03f85e96d423b9916cdc8cb0509fb970fd643bdbc9896d49e1cad0",
)
PYTHON_3_9_19 = PublishedImage(
registry="docker.io",
repository="python",
tag="3.9.19-slim-bookworm",
sha="e6941b744e8eb9df6cf6baf323d2b9ad1dfe17d118f5efee14634aff4d47c76f",
)
PYTHON_3_10_14 = PublishedImage(
registry="docker.io",
repository="python",
tag="3.10.14-slim-bookworm",
sha="2407c61b1a18067393fecd8a22cf6fceede893b6aaca817bf9fbfe65e33614a3",
)
PYTHON_3_11_11 = PublishedImage(
registry="docker.io",
repository="python",
tag="3.11.11-slim-bookworm",
sha="6ed5bff4d7d377e2a27d9285553b8c21cfccc4f00881de1b24c9bc8d90016e82",
)
PYTHON_3_11_13 = PublishedImage(
registry="docker.io",
repository="python",
tag="3.11.13-slim-bookworm",
sha="139020233cc412efe4c8135b0efe1c7569dc8b28ddd88bddb109b764f8977e30",
)
AMAZON_CORRETTO_21_AL_2023 = PublishedImage(
registry="docker.io",
repository="amazoncorretto",
tag="21-al2023",
sha="c90f38f8a5c4494cb773a984dc9fa9a727b3e6c2f2ee2cba27c834a6e101af0d",
)

View File

@@ -1,202 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import re
from typing import Optional
import dagger
from base_images import errors
async def check_env_var_with_printenv(
container: dagger.Container, expected_env_var_name: str, expected_env_var_value: Optional[str] = None
):
"""This checks if an environment variable is correctly defined by calling the printenv command in a container.
Args:
container (dagger.Container): The container on which the sanity checks should run.
expected_env_var_name (str): The name of the environment variable to check.
expected_env_var_value (Optional[str], optional): The expected value of the environment variable. Defaults to None.
Raises:
errors.SanityCheckError: Raised if the environment variable is not defined or if it has an unexpected value.
"""
try:
printenv_output = await container.with_exec(["printenv"]).stdout()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
env_vars = {line.split("=")[0]: line.split("=")[1] for line in printenv_output.splitlines()}
if expected_env_var_name not in env_vars:
raise errors.SanityCheckError(f"the {expected_env_var_name} environment variable is not defined.")
if expected_env_var_value is not None and env_vars[expected_env_var_name] != expected_env_var_value:
raise errors.SanityCheckError(
f"the {expected_env_var_name} environment variable is defined but has an unexpected value: {env_vars[expected_env_var_name]}."
)
async def check_timezone_is_utc(container: dagger.Container):
"""Check that the system timezone is UTC.
Args:
container (dagger.Container): The container on which the sanity checks should run.
Raises:
errors.SanityCheckError: Raised if the date command could not be executed or if the outputted timezone is not UTC.
"""
try:
tz_output: str = await container.with_exec(["date"]).stdout()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
if "UTC" not in tz_output:
raise errors.SanityCheckError(f"unexpected timezone: {tz_output}")
async def check_a_command_is_available_using_version_option(container: dagger.Container, command: str, version_option: str = "--version"):
"""Checks that a command is available in the container by calling it with the --version option.
Args:
container (dagger.Container): The container on which the sanity checks should run.
command (str): The command to check.
Raises:
errors.SanityCheckError: Raised if the command could not be executed or if the outputted version is not the expected one.
"""
try:
command_version_output: str = await container.with_exec([command, version_option]).stdout()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
if command_version_output == "":
raise errors.SanityCheckError(f"unexpected {command} version: {command_version_output}")
async def check_socat_version(container: dagger.Container, expected_socat_version: str):
"""Checks that the socat version is the expected one.
Args:
container (dagger.Container): The container on which the sanity checks should run.
expected_socat_version (str): The expected socat version.
Raises:
errors.SanityCheckError: Raised if the socat --version command could not be executed or if the outputted version is not the expected one.
"""
try:
socat_version_output: str = await container.with_exec(["socat", "-V"]).stdout()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
socat_version_line = None
for line in socat_version_output.splitlines():
if line.startswith("socat version"):
socat_version_line = line
break
if socat_version_line is None:
raise errors.SanityCheckError(f"Could not parse the socat version from the output: {socat_version_output}")
version_pattern = r"version (\d+\.\d+\.\d+\.\d+)"
match = re.search(version_pattern, socat_version_line)
if match:
version_number = match.group(1)
if version_number != expected_socat_version:
raise errors.SanityCheckError(f"unexpected socat version: {version_number}")
else:
raise errors.SanityCheckError(f"Could not find the socat version in the version output: {socat_version_line}")
async def check_user_exists(container: dagger.Container, user: str, expected_uid: int, expected_gid: int):
"""Check that a user exists in the container, can be impersonated and has the expected user id and group id.
Args:
container (dagger.Container): The container on which the sanity checks should run.
user (str): The user to impersonate.
expected_uid (int): The expected user id.
expected_gid (int): The expected group id.
Raises:
errors.SanityCheckError: Raised if the id command could not be executed or if the user does not exist.
"""
container = container.with_user(user)
try:
whoami_output = (await container.with_exec(["whoami"]).stdout()).strip()
except dagger.ExecError as e:
raise errors.SanityCheckError(e)
if whoami_output != user:
raise errors.SanityCheckError(f"The user {user} does not exist in the container.")
user_id = (await container.with_exec(["id", "-u"]).stdout()).strip()
if int(user_id) != expected_uid:
raise errors.SanityCheckError(f"Unexpected user id: {user_id}")
group_id = (await container.with_exec(["id", "-g"]).stdout()).strip()
if int(group_id) != expected_gid:
raise errors.SanityCheckError(f"Unexpected group id: {group_id}")
async def check_user_can_read_dir(container: dagger.Container, user: str, dir_path: str):
"""Check that the given user has read permissions on files in a given directory.
Args:
container (dagger.Container): The container on which the sanity checks should run.
user (str): The user to impersonate.
dir_path (str): The directory path to check.
Raises:
errors.SanityCheckError: Raised if the given user could not read a file created in the given directory.
"""
try:
await container.with_exec(["touch", f"{dir_path}/foo.txt"]).with_user(user).with_exec(["cat", f"{dir_path}/foo.txt"])
except dagger.ExecError:
raise errors.SanityCheckError(f"{dir_path} is not readable by {user}.")
async def check_user_can_write_dir(container: dagger.Container, user: str, dir_path: str):
"""Check that the given user has write permissions on files in a given directory.
Args:
container (dagger.Container): The container on which the sanity checks should run.
user (str): The user to impersonate.
dir_path (str): The directory path to check.
Raises:
errors.SanityCheckError: Raised if the user could write a file in the given directory.
"""
try:
await container.with_user(user).with_exec(["touch", f"{dir_path}/foo.txt"])
except dagger.ExecError:
raise errors.SanityCheckError(f"{dir_path} is not writable by the {user}.")
async def check_file_exists(container: dagger.Container, file_path: str):
"""Check that a file exists in the container.
Args:
container (dagger.Container): The container on which the sanity checks should run.
file_path (str): The file path to check.
Raises:
errors.SanityCheckError: Raised if the file does not exist.
"""
try:
await container.with_exec(["test", "-f", file_path])
except dagger.ExecError:
raise errors.SanityCheckError(f"{file_path} does not exist.")
async def check_user_uid_guid(container: dagger.Container, user: str, expected_uid: int, expected_gid: int):
"""Check that the given user has the expected user id and group id.
Args:
container (dagger.Container): The container on which the sanity checks should run.
user (str): The user to impersonate.
expected_uid (int): The expected user id.
expected_gid (int): The expected group id.
Raises:
errors.SanityCheckError: Raised if the user does not have the expected user id or group id.
"""
try:
user_id = (await container.with_user(user).with_exec(["id", "-u"]).stdout()).strip()
if int(user_id) != expected_uid:
raise errors.SanityCheckError(f"Unexpected user id: {user_id}")
group_id = (await container.with_user(user).with_exec(["id", "-g"]).stdout()).strip()
if int(group_id) != expected_gid:
raise errors.SanityCheckError(f"Unexpected group id: {group_id}")
except dagger.ExecError as e:
raise errors.SanityCheckError(e)

View File

@@ -1,103 +0,0 @@
# airbyte-connectors-base-images
This python package contains the base images used by Airbyte connectors.
It is intended to be used as a python library.
Our connector build pipeline ([`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md#L1)) uses this library to build the connector images.
Our base images are declared in code, using the [Dagger Python SDK](https://dagger-io.readthedocs.io/en/sdk-python-v0.6.4/).
- [Python base image code declaration](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/base_images/base_images/python/bases.py)
- [Java base image code declaration](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/base_images/base_images/java/bases.py)
## Where are the Dockerfiles?
Our base images are not declared using Dockerfiles.
They are declared in code using the [Dagger Python SDK](https://dagger-io.readthedocs.io/en/sdk-python-v0.6.4/).
We prefer this approach because it allows us to interact with base images container as code: we can use python to declare the base images and use the full power of the language to build and test them.
However, we do artificially generate Dockerfiles for debugging and documentation purposes.
{% for registry in registries %}
{% if registry.entries %}
### Example for `{{ registry.ConnectorBaseImageClass.repository }}`:
```dockerfile
{{ registry.entries[0].changelog_entry.dockerfile_example }}
```
{% endif %}
{% endfor %}
## Base images
{% for registry in registries %}
### `{{ registry.ConnectorBaseImageClass.repository }}`
| Version | Published | Docker Image Address | Changelog |
|---------|-----------|--------------|-----------|
{%- for entry in registry.entries %}
| {{ entry.version }} | {{ "✅" if entry.published else "❌" }}| {{ entry.published_docker_image.address }} | {{ entry.changelog_entry.changelog_entry }} |
{%- endfor %}
{% endfor %}
## How to release a new base image version (example for Python)
### Requirements
* [Docker](https://docs.docker.com/get-docker/)
* [Poetry](https://python-poetry.org/docs/#installation)
* Dockerhub logins
### Steps
1. `poetry install`
2. Open `base_images/python/bases.py`.
3. Make changes to the `AirbytePythonConnectorBaseImage`, you're likely going to change the `get_container` method to change the base image.
4. Implement the `container` property which must return a `dagger.Container` object.
5. **Recommended**: Add new sanity checks to `run_sanity_check` to confirm that the new version is working as expected.
6. Cut a new base image version by running `poetry run generate-release`. You'll need your DockerHub credentials.
It will:
- Prompt you to pick which base image you'd like to publish.
- Prompt you for a major/minor/patch/pre-release version bump.
- Prompt you for a changelog message.
- Run the sanity checks on the new version.
- Optional: Publish the new version to DockerHub.
- Regenerate the docs and the registry json file.
7. Commit and push your changes.
8. Create a PR and ask for a review from the Connector Operations team.
**Please note that if you don't publish your image while cutting the new version you can publish it later with `poetry run publish <repository> <version>`.**
No connector will use the new base image version until its metadata is updated to use it.
If you're not fully confident with the new base image version please:
- please publish it as a pre-release version
- try out the new version on a couple of connectors
- cut a new version with a major/minor/patch bump and publish it
- This steps can happen in different PRs.
## Running tests locally
```bash
poetry run pytest
# Static typing checks
poetry run mypy base_images --check-untyped-defs
```
## CHANGELOG
### 1.4.0
- Declare a base image for our java connectors.
### 1.3.1
- Update the crane image address. The previous address was deleted by the maintainer.
### 1.2.0
- Improve new version prompt to pick bump type with optional pre-release version.
### 1.1.0
- Add a cache ttl for base image listing to avoid DockerHub rate limiting.
### 1.0.4
- Upgrade Dagger to `0.13.3`
### 1.0.2
- Improved support for images with non-semantic-versioned tags.
### 1.0.1
- Bumped dependencies ([#42581](https://github.com/airbytehq/airbyte/pull/42581))

View File

@@ -1,3 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#

View File

@@ -1,6 +0,0 @@
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
def sh_dash_c(lines: list[str]) -> list[str]:
"""Wrap sequence of commands in shell for safe usage of dagger Container's with_exec method."""
return ["sh", "-c", " && ".join(["set -o xtrace"] + lines)]

View File

@@ -1,94 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import getpass
import os
import time
import uuid
from typing import List, Tuple
import dagger
from base_images import console, published_image
def get_credentials() -> Tuple[str, str]:
"""This function will prompt the user for docker credentials.
If the user has set the DOCKER_HUB_USERNAME and DOCKER_HUB_PASSWORD environment variables, it will use those instead.
Returns:
Tuple[str, str]: (username, password)
"""
if os.environ.get("DOCKER_HUB_USERNAME") and os.environ.get("DOCKER_HUB_PASSWORD"):
console.log("Using docker credentials from environment variables.")
return os.environ["DOCKER_HUB_USERNAME"], os.environ["DOCKER_HUB_PASSWORD"]
else:
console.log("Please enter your docker credentials.")
console.log("You can set them as environment variables to avoid being prompted again: DOCKER_HUB_USERNAME, DOCKER_HUB_PASSWORD")
# Not using inquirer here because of the sensitive nature of the information
docker_username = input("Dockerhub username: ")
docker_password = getpass.getpass("Dockerhub Password: ")
return docker_username, docker_password
class CraneClient:
CRANE_IMAGE_ADDRESS = "gcr.io/go-containerregistry/crane/debug:c195f151efe3369874c72662cd69ad43ee485128@sha256:94f61956845714bea3b788445454ae4827f49a90dcd9dac28255c4cccb6220ad"
def __init__(self, dagger_client: dagger.Client, docker_credentials: Tuple[str, str], cache_ttl_seconds: int = 0):
self.docker_hub_username_secret = dagger_client.set_secret("DOCKER_HUB_USERNAME", docker_credentials[0])
self.docker_hub_username_password = dagger_client.set_secret("DOCKER_HUB_PASSWORD", docker_credentials[1])
if cache_ttl_seconds == 0:
cache_buster = str(uuid.uuid4())
else:
cache_buster = str(int(time.time()) // cache_ttl_seconds)
self.bare_container = dagger_client.container().from_(self.CRANE_IMAGE_ADDRESS).with_env_variable("CACHE_BUSTER", cache_buster)
self.authenticated_container = self.login()
def login(self) -> dagger.Container:
return (
self.bare_container.with_secret_variable("DOCKER_HUB_USERNAME", self.docker_hub_username_secret)
.with_secret_variable("DOCKER_HUB_PASSWORD", self.docker_hub_username_password)
.with_exec(["sh", "-c", "crane auth login index.docker.io -u $DOCKER_HUB_USERNAME -p $DOCKER_HUB_PASSWORD"])
)
async def digest(self, repository_and_tag: str) -> str:
return (await self.authenticated_container.with_exec(["digest", repository_and_tag], use_entrypoint=True).stdout()).strip()
async def ls(self, registry_name: str, repository_name: str) -> List[str]:
repository_address = f"{registry_name}/{repository_name}"
console.log(f"Fetching published images in {repository_address}...")
try:
crane_ls_output = await self.authenticated_container.with_exec(["ls", repository_address], use_entrypoint=True).stdout()
return crane_ls_output.splitlines()
except dagger.ExecError as exec_error:
# When the repository does not exist, crane ls returns an error with NAME_UNKNOWN in the stderr.
if "NAME_UNKNOWN" in exec_error.stderr:
console.log(f"Repository {repository_address} does not exist. Returning an empty list.")
return []
else:
raise exec_error
class RemoteRepository:
def __init__(self, crane_client: CraneClient, registry_name: str, repository_name: str):
self.crane_client = crane_client
self.registry_name = registry_name
self.repository_name = repository_name
async def get_all_images(self) -> List[published_image.PublishedImage]:
repository_address = f"{self.registry_name}/{self.repository_name}"
all_tags = await self.crane_client.ls(self.registry_name, self.repository_name)
# CraneClient ls lists the tags available for a repository, but not the digests.
# We want the digest to uniquely identify the image, so we need to fetch it separately with `crane digest`
available_addresses_without_digest = [f"{repository_address}:{tag}" for tag in all_tags]
available_addresses_with_digest = []
console.log(f"Fetching digests for {len(available_addresses_without_digest)} images...")
# TODO: This is a bottleneck, we should parallelize this
for address in available_addresses_without_digest:
digest = await self.crane_client.digest(address)
available_addresses_with_digest.append(f"{address}@{digest}")
console.log(f"Found digests for {len(available_addresses_with_digest)} images in {repository_address}")
return [published_image.PublishedImage.from_address(address) for address in available_addresses_with_digest]

View File

@@ -1,314 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Mapping, Optional, Tuple, Type
import dagger
import semver
from base_images import consts, published_image
from base_images.bases import AirbyteConnectorBaseImage
from base_images.java.bases import AirbyteJavaConnectorBaseImage
from base_images.python.bases import AirbyteManifestOnlyConnectorBaseImage, AirbytePythonConnectorBaseImage
from base_images.utils import docker
from connector_ops.utils import ConnectorLanguage # type: ignore
MANAGED_BASE_IMAGES = [AirbytePythonConnectorBaseImage, AirbyteJavaConnectorBaseImage]
@dataclass
class ChangelogEntry:
version: semver.VersionInfo
changelog_entry: str
dockerfile_example: str
def to_serializable_dict(self):
return {
"version": str(self.version),
"changelog_entry": self.changelog_entry,
"dockerfile_example": self.dockerfile_example,
}
@staticmethod
def from_dict(entry_dict: Dict):
return ChangelogEntry(
version=semver.VersionInfo.parse(entry_dict["version"]),
changelog_entry=entry_dict["changelog_entry"],
dockerfile_example=entry_dict["dockerfile_example"],
)
@dataclass
class VersionRegistryEntry:
published_docker_image: Optional[published_image.PublishedImage]
changelog_entry: Optional[ChangelogEntry]
version: semver.VersionInfo
@property
def published(self) -> bool:
return self.published_docker_image is not None
class VersionRegistry:
def __init__(
self,
ConnectorBaseImageClass: Type[AirbyteConnectorBaseImage],
entries: List[VersionRegistryEntry],
) -> None:
self.ConnectorBaseImageClass: Type[AirbyteConnectorBaseImage] = ConnectorBaseImageClass
self._entries: List[VersionRegistryEntry] = entries
@staticmethod
def get_changelog_dump_path(ConnectorBaseImageClass: Type[AirbyteConnectorBaseImage]) -> Path:
"""Returns the path where the changelog is dumped to disk.
Args:
ConnectorBaseImageClass (Type[AirbyteConnectorBaseImage]): The base image version class bound to the registry.
Returns:
Path: The path where the changelog JSON is dumped to disk.
"""
registries_dir = Path("generated/changelogs")
registries_dir.mkdir(exist_ok=True, parents=True)
return registries_dir / f'{ConnectorBaseImageClass.repository.replace("-", "_").replace("/", "_")}.json' # type: ignore
@property
def changelog_dump_path(self) -> Path:
"""Returns the path where the changelog JSON is dumped to disk.
Returns:
Path: The path where the changelog JSON is dumped to disk.
"""
return self.get_changelog_dump_path(self.ConnectorBaseImageClass)
@staticmethod
def get_changelog_entries(ConnectorBaseImageClass: Type[AirbyteConnectorBaseImage]) -> List[ChangelogEntry]:
"""Returns the changelog entries for a given base image version class.
The changelog entries are loaded from the checked in changelog dump JSON file.
Args:
ConnectorBaseImageClass (Type[AirbyteConnectorBaseImage]): The base image version class bound to the registry.
Returns:
List[ChangelogEntry]: The changelog entries for a given base image version class.
"""
change_log_dump_path = VersionRegistry.get_changelog_dump_path(ConnectorBaseImageClass)
if not change_log_dump_path.exists():
changelog_entries = []
else:
changelog_entries = [ChangelogEntry.from_dict(raw_entry) for raw_entry in json.loads(change_log_dump_path.read_text())]
return changelog_entries
@staticmethod
async def get_all_published_base_images(
dagger_client: dagger.Client,
docker_credentials: Tuple[str, str],
ConnectorBaseImageClass: Type[AirbyteConnectorBaseImage],
cache_ttl_seconds: int = 0,
) -> List[published_image.PublishedImage]:
"""Returns all the published base images for a given base image version class.
Args:
dagger_client (dagger.Client): The dagger client used to build the registry.
docker_credentials (Tuple[str, str]): The docker credentials used to fetch published images from DockerHub.
ConnectorBaseImageClass (Type[AirbyteConnectorBaseImage]): The base image version class bound to the registry.
cache_ttl_seconds (int, optional): The cache time to live in seconds for crane output. Defaults to 0.
Returns:
List[published_image.PublishedImage]: The published base images for a given base image version class.
"""
crane_client = docker.CraneClient(dagger_client, docker_credentials, cache_ttl_seconds=cache_ttl_seconds)
remote_registry = docker.RemoteRepository(crane_client, consts.REMOTE_REGISTRY, ConnectorBaseImageClass.repository) # type: ignore
return await remote_registry.get_all_images()
@staticmethod
async def load(
ConnectorBaseImageClass: Type[AirbyteConnectorBaseImage],
dagger_client: dagger.Client,
docker_credentials: Tuple[str, str],
cache_ttl_seconds: int = 0,
) -> VersionRegistry:
"""Instantiates a registry by fetching available versions from the remote registry and loading the changelog from disk.
Args:
ConnectorBaseImageClass (Type[AirbyteConnectorBaseImage]): The base image version class bound to the registry.
dagger_client (dagger.Client): The dagger client used to build the registry.
docker_credentials (Tuple[str, str]): The docker credentials used to fetch published images from DockerHub.
cache_ttl_seconds (int, optional): The cache time to live in seconds for crane output. Defaults to 0.
Returns:
VersionRegistry: The registry.
"""
# Loading the local structured changelog file which is stored as a json file.
changelog_entries = VersionRegistry.get_changelog_entries(ConnectorBaseImageClass)
# Build a dict of changelog entries by version number for easier lookup
changelog_entries_by_version = {entry.version: entry for entry in changelog_entries}
# Instantiate a crane client and a remote registry to fetch published images from DockerHub
published_docker_images = await VersionRegistry.get_all_published_base_images(
dagger_client, docker_credentials, ConnectorBaseImageClass, cache_ttl_seconds=cache_ttl_seconds
)
# Build a dict of published images by version number for easier lookup
published_docker_images_by_version = dict()
for image in published_docker_images:
try:
published_docker_images_by_version[image.version] = image
# Skip any images with invalid version tags (i.e. "test_build")
except ValueError:
continue
# We union the set of versions from the changelog and the published images to get all the versions we have to consider
all_versions = set(changelog_entries_by_version.keys()) | set(published_docker_images_by_version.keys())
registry_entries = []
# Iterate over all the versions we have to consider and build a registry entry for each of them
# The registry entry will contain the published image if available, and the changelog entry if available
# If the version is not published, the published image will be None
# If the version is not in the changelog, the changelog entry will be None
for version in all_versions:
published_docker_image = published_docker_images_by_version.get(version)
changelog_entry = changelog_entries_by_version.get(version)
registry_entries.append(VersionRegistryEntry(published_docker_image, changelog_entry, version))
return VersionRegistry(ConnectorBaseImageClass, registry_entries)
def save_changelog(self):
"""Writes the changelog to disk. The changelog is dumped as a json file with a list of ChangelogEntry objects."""
as_json = json.dumps([entry.changelog_entry.to_serializable_dict() for entry in self.entries if entry.changelog_entry])
self.changelog_dump_path.write_text(as_json)
def add_entry(self, new_entry: VersionRegistryEntry) -> List[VersionRegistryEntry]:
"""Registers a new entry in the registry and saves the changelog locally.
Args:
new_entry (VersionRegistryEntry): The new entry to register.
Returns:
List[VersionRegistryEntry]: All the entries sorted by version number in descending order.
"""
self._entries.append(new_entry)
self.save_changelog()
return self.entries
@property
def entries(self) -> List[VersionRegistryEntry]:
"""Returns all the base image versions sorted by version number in descending order.
Returns:
List[Type[VersionRegistryEntry]]: All the published versions sorted by version number in descending order.
"""
return sorted(self._entries, key=lambda entry: entry.version, reverse=True)
@property
def latest_entry(self) -> Optional[VersionRegistryEntry]:
"""Returns the latest entry this registry.
The latest entry is the one with the highest version number.
If no entry is available, returns None.
Returns:
Optional[VersionRegistryEntry]: The latest registry entry, or None if no entry is available.
"""
try:
return self.entries[0]
except IndexError:
return None
@property
def latest_published_entry(self) -> Optional[VersionRegistryEntry]:
"""Returns the latest published entry this registry.
The latest published entry is the one with the highest version number among the published entries.
If no entry is available, returns None.
Returns:
Optional[VersionRegistryEntry]: The latest published registry entry, or None if no entry is available.
"""
try:
return [entry for entry in self.entries if entry.published][0]
except IndexError:
return None
def get_entry_for_version(self, version: semver.VersionInfo) -> Optional[VersionRegistryEntry]:
"""Returns the entry for a given version.
If no entry is available, returns None.
Returns:
Optional[VersionRegistryEntry]: The registry entry for the given version, or None if no entry is available.
"""
for entry in self.entries:
if entry.version == version:
return entry
return None
@property
def latest_not_pre_released_published_entry(self) -> Optional[VersionRegistryEntry]:
"""Returns the latest entry with a not pre-released version in this registry which is published.
If no entry is available, returns None.
It is meant to be used externally to get the latest published version.
Returns:
Optional[VersionRegistryEntry]: The latest registry entry with a not pre-released version, or None if no entry is available.
"""
try:
not_pre_release_published_entries = [entry for entry in self.entries if not entry.version.prerelease and entry.published]
return not_pre_release_published_entries[0]
except IndexError:
return None
async def get_python_registry(
dagger_client: dagger.Client, docker_credentials: Tuple[str, str], cache_ttl_seconds: int = 0
) -> VersionRegistry:
return await VersionRegistry.load(
AirbytePythonConnectorBaseImage, dagger_client, docker_credentials, cache_ttl_seconds=cache_ttl_seconds
)
async def get_manifest_only_registry(
dagger_client: dagger.Client, docker_credentials: Tuple[str, str], cache_ttl_seconds: int = 0
) -> VersionRegistry:
return await VersionRegistry.load(
AirbyteManifestOnlyConnectorBaseImage, dagger_client, docker_credentials, cache_ttl_seconds=cache_ttl_seconds
)
async def get_java_registry(
dagger_client: dagger.Client, docker_credentials: Tuple[str, str], cache_ttl_seconds: int = 0
) -> VersionRegistry:
return await VersionRegistry.load(AirbyteJavaConnectorBaseImage, dagger_client, docker_credentials, cache_ttl_seconds=cache_ttl_seconds)
async def get_registry_for_language(
dagger_client: dagger.Client, language: ConnectorLanguage, docker_credentials: Tuple[str, str], cache_ttl_seconds: int = 0
) -> VersionRegistry:
"""Returns the registry for a given language.
It is meant to be used externally to get the registry for a given connector language.
Args:
dagger_client (dagger.Client): The dagger client used to build the registry.
language (ConnectorLanguage): The connector language.
docker_credentials (Tuple[str, str]): The docker credentials used to fetch published images from DockerHub.
cache_ttl_seconds (int, optional): The cache time to live in seconds for crane output. Defaults to 0.
Raises:
NotImplementedError: Raised if the registry for the given language is not implemented yet.
Returns:
VersionRegistry: The registry for the given language.
"""
if language in [ConnectorLanguage.PYTHON, ConnectorLanguage.LOW_CODE]:
return await get_python_registry(dagger_client, docker_credentials, cache_ttl_seconds=cache_ttl_seconds)
elif language is ConnectorLanguage.MANIFEST_ONLY:
return await get_manifest_only_registry(dagger_client, docker_credentials, cache_ttl_seconds=cache_ttl_seconds)
elif language is ConnectorLanguage.JAVA:
return await get_java_registry(dagger_client, docker_credentials, cache_ttl_seconds=cache_ttl_seconds)
else:
raise NotImplementedError(f"Registry for language {language} is not implemented yet.")
async def get_all_registries(dagger_client: dagger.Client, docker_credentials: Tuple[str, str]) -> List[VersionRegistry]:
return [
await get_python_registry(dagger_client, docker_credentials),
await get_java_registry(dagger_client, docker_credentials),
# await get_manifest_only_registry(dagger_client, docker_credentials),
]

View File

@@ -1,42 +0,0 @@
[
{
"version": "2.0.1",
"changelog_entry": "Bump Amazon Coretto image version for compatibility with Apple M4 architecture.",
"dockerfile_example": "FROM docker.io/amazoncorretto:21-al2023@sha256:c90f38f8a5c4494cb773a984dc9fa9a727b3e6c2f2ee2cba27c834a6e101af0d\nRUN sh -c set -o xtrace && yum install -y shadow-utils tar openssl findutils && yum update -y --security && yum clean all && rm -rf /var/cache/yum && groupadd --gid 1000 airbyte && useradd --uid 1000 --gid airbyte --shell /bin/bash --create-home airbyte && mkdir /secrets && mkdir /config && mkdir --mode 755 /airbyte && mkdir --mode 755 /custom_cache && chown -R airbyte:airbyte /airbyte && chown -R airbyte:airbyte /custom_cache && chown -R airbyte:airbyte /secrets && chown -R airbyte:airbyte /config && chown -R airbyte:airbyte /usr/share/pki/ca-trust-source && chown -R airbyte:airbyte /etc/pki/ca-trust && chown -R airbyte:airbyte /tmp\nENV AIRBYTE_SPEC_CMD=/airbyte/javabase.sh --spec\nENV AIRBYTE_CHECK_CMD=/airbyte/javabase.sh --check\nENV AIRBYTE_DISCOVER_CMD=/airbyte/javabase.sh --discover\nENV AIRBYTE_READ_CMD=/airbyte/javabase.sh --read\nENV AIRBYTE_WRITE_CMD=/airbyte/javabase.sh --write\nENV AIRBYTE_ENTRYPOINT=/airbyte/base.sh"
},
{
"version": "2.0.0",
"changelog_entry": "~Release non root base image~",
"dockerfile_example": "FROM docker.io/amazoncorretto:21-al2023@sha256:5454cb606e803fce56861fdbc9eab365eaa2ab4f357ceb8c1d56f4f8c8a7bc33\nRUN sh -c set -o xtrace && yum install -y shadow-utils tar openssl findutils && yum update -y --security && yum clean all && rm -rf /var/cache/yum && groupadd --gid 1000 airbyte && useradd --uid 1000 --gid airbyte --shell /bin/bash --create-home airbyte && mkdir /secrets && mkdir /config && mkdir --mode 755 /airbyte && mkdir --mode 755 /custom_cache && chown -R airbyte:airbyte /airbyte && chown -R airbyte:airbyte /custom_cache && chown -R airbyte:airbyte /secrets && chown -R airbyte:airbyte /config && chown -R airbyte:airbyte /usr/share/pki/ca-trust-source && chown -R airbyte:airbyte /etc/pki/ca-trust && chown -R airbyte:airbyte /tmp\nENV AIRBYTE_SPEC_CMD=/airbyte/javabase.sh --spec\nENV AIRBYTE_CHECK_CMD=/airbyte/javabase.sh --check\nENV AIRBYTE_DISCOVER_CMD=/airbyte/javabase.sh --discover\nENV AIRBYTE_READ_CMD=/airbyte/javabase.sh --read\nENV AIRBYTE_WRITE_CMD=/airbyte/javabase.sh --write\nENV AIRBYTE_ENTRYPOINT=/airbyte/base.sh"
},
{
"version": "2.0.0-rc.2",
"changelog_entry": "Fine tune permissions and reproduce platform java base implementation",
"dockerfile_example": "FROM docker.io/amazoncorretto:21-al2023@sha256:5454cb606e803fce56861fdbc9eab365eaa2ab4f357ceb8c1d56f4f8c8a7bc33\nRUN sh -c set -o xtrace && yum install -y shadow-utils tar openssl findutils && yum update -y --security && yum clean all && rm -rf /var/cache/yum && groupadd --gid 1000 airbyte && useradd --uid 1000 --gid airbyte --shell /bin/bash --create-home airbyte && mkdir /secrets && mkdir /config && mkdir --mode 755 /airbyte && mkdir --mode 755 /custom_cache && chown -R airbyte:airbyte /airbyte && chown -R airbyte:airbyte /custom_cache && chown -R airbyte:airbyte /secrets && chown -R airbyte:airbyte /config && chown -R airbyte:airbyte /usr/share/pki/ca-trust-source && chown -R airbyte:airbyte /etc/pki/ca-trust && chown -R airbyte:airbyte /tmp\nENV AIRBYTE_SPEC_CMD=/airbyte/javabase.sh --spec\nENV AIRBYTE_CHECK_CMD=/airbyte/javabase.sh --check\nENV AIRBYTE_DISCOVER_CMD=/airbyte/javabase.sh --discover\nENV AIRBYTE_READ_CMD=/airbyte/javabase.sh --read\nENV AIRBYTE_WRITE_CMD=/airbyte/javabase.sh --write\nENV AIRBYTE_ENTRYPOINT=/airbyte/base.sh"
},
{
"version": "2.0.0-rc.1",
"changelog_entry": " Make the java base image non root",
"dockerfile_example": "FROM docker.io/amazoncorretto:21-al2023@sha256:5454cb606e803fce56861fdbc9eab365eaa2ab4f357ceb8c1d56f4f8c8a7bc33\nRUN sh -c set -o xtrace && yum update -y --security && yum install -y /usr/sbin/adduser tar openssl findutils && yum clean all && adduser --base-dir /airbyte --uid 1000 --user-group --system airbyte && mkdir --mode 755 /airbyte && mkdir --mode 755 /custom_cache && chown -R airbyte:airbyte /airbyte\nENV AIRBYTE_SPEC_CMD=/airbyte/javabase.sh --spec\nENV AIRBYTE_CHECK_CMD=/airbyte/javabase.sh --check\nENV AIRBYTE_DISCOVER_CMD=/airbyte/javabase.sh --discover\nENV AIRBYTE_READ_CMD=/airbyte/javabase.sh --read\nENV AIRBYTE_WRITE_CMD=/airbyte/javabase.sh --write\nENV AIRBYTE_ENTRYPOINT=/airbyte/base.sh"
},
{
"version": "1.0.0",
"changelog_entry": "Create a base image for our java connectors based on Amazon Corretto.",
"dockerfile_example": "FROM docker.io/amazoncorretto:21-al2023@sha256:5454cb606e803fce56861fdbc9eab365eaa2ab4f357ceb8c1d56f4f8c8a7bc33\nRUN sh -c set -o xtrace && yum update -y --security && yum install -y tar openssl findutils && yum clean all\nENV AIRBYTE_SPEC_CMD=/airbyte/javabase.sh --spec\nENV AIRBYTE_CHECK_CMD=/airbyte/javabase.sh --check\nENV AIRBYTE_DISCOVER_CMD=/airbyte/javabase.sh --discover\nENV AIRBYTE_READ_CMD=/airbyte/javabase.sh --read\nENV AIRBYTE_WRITE_CMD=/airbyte/javabase.sh --write\nENV AIRBYTE_ENTRYPOINT=/airbyte/base.sh"
},
{
"version": "1.0.0-rc.4",
"changelog_entry": "Bundle yum calls in a single RUN",
"dockerfile_example": "FROM docker.io/amazoncorretto:21-al2023@sha256:5454cb606e803fce56861fdbc9eab365eaa2ab4f357ceb8c1d56f4f8c8a7bc33\nRUN sh -c set -o xtrace && yum update -y --security && yum install -y tar openssl findutils && yum clean all\nENV AIRBYTE_SPEC_CMD=/airbyte/javabase.sh --spec\nENV AIRBYTE_CHECK_CMD=/airbyte/javabase.sh --check\nENV AIRBYTE_DISCOVER_CMD=/airbyte/javabase.sh --discover\nENV AIRBYTE_READ_CMD=/airbyte/javabase.sh --read\nENV AIRBYTE_WRITE_CMD=/airbyte/javabase.sh --write\nENV AIRBYTE_ENTRYPOINT=/airbyte/base.sh"
},
{
"version": "1.0.0-rc.2",
"changelog_entry": "Set entrypoint to base.sh",
"dockerfile_example": "FROM docker.io/amazoncorretto:21-al2023@sha256:5454cb606e803fce56861fdbc9eab365eaa2ab4f357ceb8c1d56f4f8c8a7bc33\nRUN yum update -y --security\nRUN yum install -y tar openssl findutils\nENV AIRBYTE_SPEC_CMD=/airbyte/javabase.sh --spec\nENV AIRBYTE_CHECK_CMD=/airbyte/javabase.sh --check\nENV AIRBYTE_DISCOVER_CMD=/airbyte/javabase.sh --discover\nENV AIRBYTE_READ_CMD=/airbyte/javabase.sh --read\nENV AIRBYTE_WRITE_CMD=/airbyte/javabase.sh --write\nENV AIRBYTE_ENTRYPOINT=/airbyte/base.sh"
},
{
"version": "1.0.0-rc.1",
"changelog_entry": "Create a base image for our java connectors.",
"dockerfile_example": "FROM docker.io/amazoncorretto:21-al2023@sha256:5454cb606e803fce56861fdbc9eab365eaa2ab4f357ceb8c1d56f4f8c8a7bc33\nRUN yum update -y --security\nRUN yum install -y tar openssl findutils\nENV AIRBYTE_SPEC_CMD=/airbyte/javabase.sh --spec\nENV AIRBYTE_CHECK_CMD=/airbyte/javabase.sh --check\nENV AIRBYTE_DISCOVER_CMD=/airbyte/javabase.sh --discover\nENV AIRBYTE_READ_CMD=/airbyte/javabase.sh --read\nENV AIRBYTE_WRITE_CMD=/airbyte/javabase.sh --write\nENV AIRBYTE_ENTRYPOINT=/airbyte/base.sh"
}
]

View File

@@ -1,67 +0,0 @@
[
{
"version": "4.0.2",
"changelog_entry": "Bump to Python 3.11.13 to patch security vulnerabilities",
"dockerfile_example": "FROM docker.io/python:3.11.13-slim-bookworm@sha256:139020233cc412efe4c8135b0efe1c7569dc8b28ddd88bddb109b764f8977e30\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN adduser --uid 1000 --system --group --no-create-home airbyte\nRUN mkdir --mode 755 /custom_cache\nRUN mkdir --mode 755 /airbyte\nRUN chown airbyte:airbyte /airbyte\nENV PIP_CACHE_DIR=/custom_cache/pip\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.8.4\nRUN sh -c apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get clean\nRUN sh -c apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+deb12u1\nRUN mkdir -p 755 /usr/share/nltk_data"
},
{
"version": "4.0.1-rc.1",
"changelog_entry": "Bump to Python 3.11.13 to patch security vulnerabilities",
"dockerfile_example": "FROM docker.io/python:3.11.13-slim-bookworm@sha256:139020233cc412efe4c8135b0efe1c7569dc8b28ddd88bddb109b764f8977e30\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN adduser --uid 1000 --system --group --no-create-home airbyte\nRUN mkdir --mode 755 /custom_cache\nRUN mkdir --mode 755 /airbyte\nRUN chown airbyte:airbyte /airbyte\nENV PIP_CACHE_DIR=/custom_cache/pip\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.8.4\nRUN sh -c apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get clean\nRUN sh -c apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+deb12u1\nRUN mkdir -p 755 /usr/share/nltk_data"
},
{
"version": "4.0.0",
"changelog_entry": "Python 3.11.11",
"dockerfile_example": "FROM docker.io/python:3.11.11-slim-bookworm@sha256:6ed5bff4d7d377e2a27d9285553b8c21cfccc4f00881de1b24c9bc8d90016e82\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN adduser --uid 1000 --system --group --no-create-home airbyte\nRUN mkdir --mode 755 /custom_cache\nRUN mkdir --mode 755 /airbyte\nRUN chown airbyte:airbyte /airbyte\nENV PIP_CACHE_DIR=/custom_cache/pip\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.8.4\nRUN sh -c apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get clean\nRUN sh -c apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+b1\nRUN mkdir -p 755 /usr/share/nltk_data"
},
{
"version": "3.0.0",
"changelog_entry": "Create airbyte user",
"dockerfile_example": "FROM docker.io/python:3.10.14-slim-bookworm@sha256:2407c61b1a18067393fecd8a22cf6fceede893b6aaca817bf9fbfe65e33614a3\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN adduser --uid 1000 --system --group --no-create-home airbyte\nRUN mkdir --mode 755 /custom_cache\nRUN mkdir --mode 755 /airbyte\nRUN chown airbyte:airbyte /airbyte\nENV PIP_CACHE_DIR=/custom_cache/pip\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1\nRUN sh -c apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get clean\nRUN sh -c apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+b1\nRUN mkdir -p 755 /usr/share/nltk_data"
},
{
"version": "3.0.0-rc.1",
"changelog_entry": "Update Python 3.10.4 image + create airbyte user",
"dockerfile_example": "FROM docker.io/python:3.10.14-slim-bookworm@sha256:2407c61b1a18067393fecd8a22cf6fceede893b6aaca817bf9fbfe65e33614a3\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN adduser --uid 1000 --system --group --no-create-home airbyte\nRUN mkdir --mode 755 /custom_cache\nRUN mkdir --mode 755 /airbyte\nRUN chown airbyte:airbyte /airbyte\nENV PIP_CACHE_DIR=/custom_cache/pip\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1\nRUN sh -c apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get clean\nRUN sh -c apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+b1\nRUN mkdir -p 755 /usr/share/nltk_data"
},
{
"version": "2.0.0",
"changelog_entry": "Use Python 3.10",
"dockerfile_example": "FROM docker.io/python:3.10.14-slim-bookworm@sha256:3b37199fbc5a730a551909b3efa7b29105c859668b7502451c163f2a4a7ae1ed\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1\nRUN sh -c apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get clean\nRUN sh -c apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+b1\nRUN mkdir /usr/share/nltk_data"
},
{
"version": "1.2.3",
"changelog_entry": "Use latest root image version and update system packages",
"dockerfile_example": "FROM docker.io/python:3.9.19-slim-bookworm@sha256:e6941b744e8eb9df6cf6baf323d2b9ad1dfe17d118f5efee14634aff4d47c76f\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1\nRUN sh -c apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get clean\nRUN sh -c apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+b1\nRUN mkdir /usr/share/nltk_data"
},
{
"version": "1.2.2",
"changelog_entry": "Fix Python 3.9.19 image digest",
"dockerfile_example": "FROM docker.io/python:3.9.19-slim-bookworm@sha256:088d9217202188598aac37f8db0929345e124a82134ac66b8bb50ee9750b045b\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1\nRUN sh -c apt update && apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+b1\nRUN mkdir /usr/share/nltk_data"
},
{
"version": "1.2.2-rc.1",
"changelog_entry": "Create an airbyte user and use it",
"dockerfile_example": "FROM docker.io/python:3.9.19-slim-bookworm@sha256:088d9217202188598aac37f8db0929345e124a82134ac66b8bb50ee9750b045b\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN sh -c apt update && apt-get install -y socat=1.7.4.4-2\nRUN adduser --uid 1000 --system --group --no-create-home airbyte\nRUN mkdir --mode 755 /custom_cache\nRUN mkdir --mode 755 /airbyte\nRUN chown airbyte:airbyte /airbyte\nENV PIP_CACHE_DIR=/custom_cache/pip\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+b1\nRUN mkdir -p 755 /usr/share/nltk_data"
},
{
"version": "1.2.1",
"changelog_entry": "Upgrade to Python 3.9.19 + update pip and setuptools",
"dockerfile_example": "FROM docker.io/python:3.9.19-slim-bookworm@sha256:b92e6f45b58d9cafacc38563e946f8d249d850db862cbbd8befcf7f49eef8209\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN pip install --upgrade pip==24.0 setuptools==70.0.0\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1\nRUN sh -c apt update && apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+b1\nRUN mkdir /usr/share/nltk_data"
},
{
"version": "1.2.0",
"changelog_entry": "Add CDK system dependencies: nltk data, tesseract, poppler.",
"dockerfile_example": "FROM docker.io/python:3.9.18-slim-bookworm@sha256:44b7f161ed03f85e96d423b9916cdc8cb0509fb970fd643bdbc9896d49e1cad0\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN pip install --upgrade pip==23.2.1\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1\nRUN sh -c apt update && apt-get install -y socat=1.7.4.4-2\nRUN sh -c apt-get update && apt-get install -y tesseract-ocr=5.3.0-2 poppler-utils=22.12.0-2+b1\nRUN mkdir /usr/share/nltk_data"
},
{
"version": "1.1.0",
"changelog_entry": "Install socat",
"dockerfile_example": "FROM docker.io/python:3.9.18-slim-bookworm@sha256:44b7f161ed03f85e96d423b9916cdc8cb0509fb970fd643bdbc9896d49e1cad0\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN pip install --upgrade pip==23.2.1\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1\nRUN sh -c apt update && apt-get install -y socat=1.7.4.4-2"
},
{
"version": "1.0.0",
"changelog_entry": "Initial release: based on Python 3.9.18, on slim-bookworm system, with pip==23.2.1 and poetry==1.6.1",
"dockerfile_example": "FROM docker.io/python:3.9.18-slim-bookworm@sha256:44b7f161ed03f85e96d423b9916cdc8cb0509fb970fd643bdbc9896d49e1cad0\nRUN ln -snf /usr/share/zoneinfo/Etc/UTC /etc/localtime\nRUN pip install --upgrade pip==23.2.1\nENV POETRY_VIRTUALENVS_CREATE=false\nENV POETRY_VIRTUALENVS_IN_PROJECT=false\nENV POETRY_NO_INTERACTION=1\nRUN pip install poetry==1.6.1"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
[tool.poetry]
name = "airbyte-connectors-base-images"
version = "1.6"
description = "This package is used to generate and publish the base images for Airbyte Connectors."
authors = ["Augustin Lafanechere <augustin@airbyte.io>"]
readme = "README.md"
packages = [{ include = "base_images" }]
include = ["generated"]
[tool.poetry.dependencies]
python = "^3.11"
dagger-io = "==0.13.3"
beartype = ">=0.18.2"
gitpython = "^3.1.35"
rich = "^13.5.2"
semver = "^3.0.1"
connector-ops = { path = "../connector_ops", develop = true }
inquirer = "^3.1.3"
jinja2 = "^3.1.2"
[tool.poetry.group.dev.dependencies]
pytest = "^8"
pytest-mock = "^3.10.0"
pytest-cov = "^4.1.0"
mypy = "^1.5.1"
vulture = "^2.9.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
generate-docs = "base_images.commands:generate_docs"
generate-release = "base_images.commands:generate_release"
publish = "base_images.commands:publish_existing_version"
[tool.poe.tasks]
test = "pytest tests"
[tool.airbyte_ci]
python_versions = ["3.11"]
optional_poetry_groups = ["dev"]
poe_tasks = ["test"]
mount_docker_socket = true

View File

@@ -1,2 +0,0 @@
[pytest]
addopts = --cov=base_images --cov-report=term-missing

View File

@@ -1,3 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#

View File

@@ -1,25 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import platform
import sys
import dagger
import pytest
@pytest.fixture(scope="module")
def anyio_backend():
return "asyncio"
@pytest.fixture(scope="module")
async def dagger_client():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
yield client
@pytest.fixture(scope="session")
def current_platform():
return dagger.Platform(f"linux/{platform.machine()}")

View File

@@ -1,3 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#

View File

@@ -1,40 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import pytest
import semver
from base_images import root_images
from base_images.python import bases
pytestmark = [
pytest.mark.anyio,
]
class TestAirbytePythonConnectorBaseImage:
@pytest.fixture
def dummy_version(self):
return semver.VersionInfo.parse("0.0.0-rc.1")
def test_class_attributes(self):
"""Spot any regression in the class attributes."""
assert bases.AirbytePythonConnectorBaseImage.root_image == root_images.PYTHON_3_11_13
assert bases.AirbytePythonConnectorBaseImage.repository == "airbyte/python-connector-base"
assert bases.AirbytePythonConnectorBaseImage.pip_cache_name == "pip_cache"
async def test_run_sanity_checks(self, dagger_client, current_platform, dummy_version):
base_image_version = bases.AirbytePythonConnectorBaseImage(dagger_client, dummy_version)
await base_image_version.run_sanity_checks(current_platform)
async def test_pip_cache_volume(self, dagger_client, current_platform, dummy_version):
base_image_version = bases.AirbytePythonConnectorBaseImage(dagger_client, dummy_version)
container = base_image_version.get_container(current_platform)
assert "/custom_cache/pip" in await container.mounts()
async def test_is_using_bookworm(self, dagger_client, current_platform, dummy_version):
base_image_version = bases.AirbytePythonConnectorBaseImage(dagger_client, dummy_version)
container = base_image_version.get_container(current_platform)
cat_output = await container.with_exec(["cat", "/etc/os-release"]).stdout()
assert "Debian GNU/Linux 12 (bookworm)" in [kv.split("=")[1].replace('"', "") for kv in cat_output.splitlines()]

View File

@@ -1,56 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from contextlib import nullcontext as does_not_raise
import pytest
from base_images import root_images
from base_images.errors import SanityCheckError
from base_images.python import sanity_checks
pytestmark = [
pytest.mark.anyio,
]
@pytest.mark.parametrize(
"docker_image, python_version, expected_error",
[
(root_images.PYTHON_3_9_18.address, "3.9.18", does_not_raise()),
(root_images.PYTHON_3_9_18.address, "3.9.19", pytest.raises(SanityCheckError)),
("hello-world:latest", "3.9.19", pytest.raises(SanityCheckError)),
],
)
async def test_check_python_version(dagger_client, docker_image, python_version, expected_error):
container_with_python = dagger_client.container().from_(docker_image)
with expected_error:
await sanity_checks.check_python_version(container_with_python, python_version)
@pytest.mark.parametrize(
"docker_image, pip_version, expected_error",
[
(root_images.PYTHON_3_9_18.address, "23.0.1", does_not_raise()),
(root_images.PYTHON_3_9_18.address, "23.0.2", pytest.raises(SanityCheckError)),
("hello-world:latest", "23.0.1", pytest.raises(SanityCheckError)),
],
)
async def test_check_pip_version(dagger_client, docker_image, pip_version, expected_error):
container_with_python = dagger_client.container().from_(docker_image)
with expected_error:
await sanity_checks.check_pip_version(container_with_python, pip_version)
@pytest.mark.parametrize(
"docker_image, poetry_version, expected_error",
[
("pfeiffermax/python-poetry:1.6.0-poetry1.6.1-python3.9.18-slim-bookworm", "1.6.1", does_not_raise()),
("pfeiffermax/python-poetry:1.6.0-poetry1.4.2-python3.9.18-bookworm", "1.6.1", pytest.raises(SanityCheckError)),
(root_images.PYTHON_3_9_18.address, "23.0.2", pytest.raises(SanityCheckError)),
],
)
async def test_check_poetry_version(dagger_client, docker_image, poetry_version, expected_error):
container_with_python = dagger_client.container().from_(docker_image)
with expected_error:
await sanity_checks.check_poetry_version(container_with_python, poetry_version)

View File

@@ -1,112 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from contextlib import nullcontext as does_not_raise
import pytest
from base_images import root_images, sanity_checks
from base_images.errors import SanityCheckError
pytestmark = [
pytest.mark.anyio,
]
@pytest.mark.parametrize(
"docker_image, expected_env_var_name, expected_env_var_value, expected_error",
[
(root_images.PYTHON_3_9_18.address, "PYTHON_VERSION", "3.9.18", does_not_raise()),
(root_images.PYTHON_3_9_18.address, "PYTHON_VERSION", "3.9.19", pytest.raises(SanityCheckError)),
(root_images.PYTHON_3_9_18.address, "NOT_EXISTING_ENV_VAR", "3.9.19", pytest.raises(SanityCheckError)),
],
)
async def test_check_env_var_with_printenv(dagger_client, docker_image, expected_env_var_name, expected_env_var_value, expected_error):
container = dagger_client.container().from_(docker_image)
with expected_error:
await sanity_checks.check_env_var_with_printenv(container, expected_env_var_name, expected_env_var_value)
container_without_printenv = container.with_exec(["rm", "/usr/bin/printenv"])
with pytest.raises(SanityCheckError):
await sanity_checks.check_env_var_with_printenv(container_without_printenv, expected_env_var_name, expected_env_var_value)
async def test_check_timezone_is_utc(dagger_client):
container = dagger_client.container().from_(root_images.PYTHON_3_9_18.address)
# This containers has UTC as timezone by default
await sanity_checks.check_timezone_is_utc(container)
container_not_on_utc = container.with_exec(["ln", "-sf", "/usr/share/zoneinfo/Europe/Paris", "/etc/localtime"])
with pytest.raises(SanityCheckError):
await sanity_checks.check_timezone_is_utc(container_not_on_utc)
container_without_date = container.with_exec(["rm", "/usr/bin/date"])
with pytest.raises(SanityCheckError):
await sanity_checks.check_timezone_is_utc(container_without_date)
async def test_check_a_command_is_available_using_version_option(dagger_client):
container = dagger_client.container().from_(root_images.PYTHON_3_9_18.address)
await sanity_checks.check_a_command_is_available_using_version_option(container, "bash")
container_without_bash = container.with_exec(["rm", "/usr/bin/bash"])
with pytest.raises(SanityCheckError):
await sanity_checks.check_a_command_is_available_using_version_option(container_without_bash, "bash")
container_without_ls = container.with_exec(["rm", "/usr/bin/ls"])
with pytest.raises(SanityCheckError):
await sanity_checks.check_a_command_is_available_using_version_option(container_without_ls, "ls")
container_without_date = container.with_exec(["rm", "/usr/bin/date"])
with pytest.raises(SanityCheckError):
await sanity_checks.check_a_command_is_available_using_version_option(container_without_date, "date")
container_without_printenv = container.with_exec(["rm", "/usr/bin/printenv"])
with pytest.raises(SanityCheckError):
await sanity_checks.check_a_command_is_available_using_version_option(container_without_printenv, "printenv")
async def test_check_socat_version(mocker):
# Mocking is used in this test because it's hard to install a different socat version in the PYTHON_3_9_18 container
# Mock the container and its 'with_exec' method
mock_container = mocker.Mock()
# Set the expected version
expected_version = "1.2.3.4"
# Mock the 'stdout' method and return an output different from the socat -V command
mock_stdout = mocker.AsyncMock(return_value="foobar")
mock_container.with_exec.return_value.stdout = mock_stdout
# Run the function
with pytest.raises(SanityCheckError) as exc_info:
await sanity_checks.check_socat_version(mock_container, expected_version)
# Check the error message
assert str(exc_info.value) == "Could not parse the socat version from the output: foobar"
# Mock the 'stdout' method and return a "socat version" line but with a version structure not matching the pattern from the socat -V command
mock_stdout = mocker.AsyncMock(
return_value="socat by Gerhard Rieger and contributors - see www.dest-unreach.org\nsocat version 1.1 on 06 Nov 2022 08:15:51"
)
mock_container.with_exec.return_value.stdout = mock_stdout
# Run the function
with pytest.raises(SanityCheckError) as exc_info:
await sanity_checks.check_socat_version(mock_container, expected_version)
# Check the error message
assert str(exc_info.value) == "Could not find the socat version in the version output: socat version 1.1 on 06 Nov 2022 08:15:51"
# Mock the 'stdout' method and return a correct "socat version" line but with a version different from the expected one
mock_stdout = mocker.AsyncMock(
return_value="socat by Gerhard Rieger and contributors - see www.dest-unreach.org\nsocat version 1.7.4.4 on 06 Nov 2022 08:15:51"
)
mock_container.with_exec.return_value.stdout = mock_stdout
# Run the function
with pytest.raises(SanityCheckError) as exc_info:
await sanity_checks.check_socat_version(mock_container, expected_version)
# Check the error message
assert str(exc_info.value) == "unexpected socat version: 1.7.4.4"
# Mock the 'stdout' method and return a correct "socat version" matching the expected one
mock_stdout = mocker.AsyncMock(
return_value=f"socat by Gerhard Rieger and contributors - see www.dest-unreach.org\nsocat version {expected_version} on 06 Nov 2022 08:15:51"
)
mock_container.with_exec.return_value.stdout = mock_stdout
# No exception should be raised by this function call
await sanity_checks.check_socat_version(mock_container, expected_version)

View File

@@ -1,152 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import json
from pathlib import Path
import pytest
import semver
from base_images import version_registry
from base_images.python.bases import AirbytePythonConnectorBaseImage
class TestChangelogEntry:
def test_to_serializable_dict(self):
changelog_entry = version_registry.ChangelogEntry(semver.VersionInfo.parse("1.0.0"), "first version", "Dockerfile example")
assert (
json.dumps(changelog_entry.to_serializable_dict())
== '{"version": "1.0.0", "changelog_entry": "first version", "dockerfile_example": "Dockerfile example"}'
), "The changelog entry should be serializable to JSON"
class TestVersionRegistry:
@pytest.fixture
def fake_entries(self, mocker):
# Please keep this list ordered by version
return [
version_registry.VersionRegistryEntry(
published_docker_image=mocker.Mock(), changelog_entry="first version", version=semver.VersionInfo.parse("1.0.0")
),
version_registry.VersionRegistryEntry(
published_docker_image=mocker.Mock(), changelog_entry="second version", version=semver.VersionInfo.parse("2.0.0")
),
version_registry.VersionRegistryEntry(
published_docker_image=mocker.Mock(), changelog_entry="pre-release", version=semver.VersionInfo.parse("3.0.0-rc.1")
),
version_registry.VersionRegistryEntry(
published_docker_image=None, changelog_entry="third version", version=semver.VersionInfo.parse("3.0.0")
),
]
def test_entries(self, fake_entries):
entries = version_registry.VersionRegistry(AirbytePythonConnectorBaseImage, fake_entries).entries
versions = [entry.version for entry in entries]
assert set(versions) == set(
[entry.version for entry in fake_entries]
), "The entries should be unique by version and contain all the entries passed as argument"
assert versions == sorted(versions, reverse=True), "The entries should be sorted by version in descending order"
def test_latest_entry(self, fake_entries):
vr = version_registry.VersionRegistry(AirbytePythonConnectorBaseImage, fake_entries)
assert vr.latest_entry == fake_entries[-1]
def test_get_entry_for_version(self, fake_entries):
vr = version_registry.VersionRegistry(AirbytePythonConnectorBaseImage, fake_entries)
entry = vr.get_entry_for_version(semver.VersionInfo.parse("1.0.0"))
assert entry.version == semver.VersionInfo.parse("1.0.0")
def test_latest_published_entry(self, fake_entries):
vr = version_registry.VersionRegistry(AirbytePythonConnectorBaseImage, fake_entries)
assert vr.latest_published_entry == fake_entries[-2]
def latest_not_pre_released_published_entry(self, fake_entries):
vr = version_registry.VersionRegistry(AirbytePythonConnectorBaseImage, fake_entries)
assert vr.latest_not_pre_released_published_entry == fake_entries[1]
def test_get_changelog_dump_path(self, mocker):
mock_connector_class = mocker.Mock()
mock_connector_class.repository = "example-repo"
path = version_registry.VersionRegistry.get_changelog_dump_path(mock_connector_class)
expected_changelog_dump_path = Path("generated/changelogs/example_repo.json")
assert path == expected_changelog_dump_path
assert version_registry.VersionRegistry(mock_connector_class, []).changelog_dump_path == expected_changelog_dump_path
def test_get_changelog_entries_with_existing_json(self, mocker, tmp_path):
dummy_change_log_path = tmp_path / "changelog.json"
dummy_changelog_entry = version_registry.ChangelogEntry(semver.VersionInfo.parse("1.0.0"), "Initial release", "")
dummy_change_log_path.write_text(json.dumps([dummy_changelog_entry.to_serializable_dict()]))
mock_connector_class = mocker.Mock()
mocker.patch.object(version_registry.VersionRegistry, "get_changelog_dump_path", return_value=dummy_change_log_path)
changelog_entries = version_registry.VersionRegistry.get_changelog_entries(mock_connector_class)
assert len(changelog_entries) == 1
assert isinstance(changelog_entries[0], version_registry.ChangelogEntry)
assert changelog_entries[0].version == semver.VersionInfo.parse("1.0.0")
assert changelog_entries[0].changelog_entry == "Initial release"
def test_get_changelog_entries_without_json(self, mocker, tmp_path):
dummy_change_log_path = tmp_path / "changelog.json"
mock_connector_class = mocker.Mock()
mocker.patch.object(version_registry.VersionRegistry, "get_changelog_dump_path", return_value=dummy_change_log_path)
changelog_entries = version_registry.VersionRegistry.get_changelog_entries(mock_connector_class)
assert len(changelog_entries) == 0
@pytest.fixture
def mock_dagger_client(self, mocker):
return mocker.Mock()
@pytest.fixture
def fake_docker_credentials(self):
return ("username", "password")
@pytest.fixture
def fake_changelog_entries(self):
return [
version_registry.ChangelogEntry(semver.VersionInfo.parse("1.0.0"), "first version", ""),
version_registry.ChangelogEntry(semver.VersionInfo.parse("2.0.0"), "second unpublished version", ""),
]
@pytest.fixture
def fake_published_images(self, mocker):
# Mock the published images to include only one version (2.0.0)
return [mocker.Mock(version=semver.VersionInfo.parse("1.0.0"))]
@pytest.mark.anyio
async def test_get_all_published_base_images(self, mocker, mock_dagger_client, fake_docker_credentials):
mock_crane_client = mocker.Mock()
mocker.patch.object(version_registry.docker, "CraneClient", return_value=mock_crane_client)
mock_remote_registry = mocker.AsyncMock()
mocker.patch.object(version_registry.docker, "RemoteRepository", return_value=mock_remote_registry)
sample_published_images = [mocker.Mock(), mocker.Mock()]
mock_remote_registry.get_all_images.return_value = sample_published_images
published_images = await version_registry.VersionRegistry.get_all_published_base_images(
mock_dagger_client, fake_docker_credentials, mocker.Mock()
)
assert published_images == sample_published_images
@pytest.mark.anyio
async def test_load_with_mocks(
self, mocker, mock_dagger_client, fake_docker_credentials, fake_changelog_entries, fake_published_images
):
mocker.patch.object(version_registry.VersionRegistry, "get_changelog_entries", return_value=fake_changelog_entries)
mocker.patch.object(version_registry.VersionRegistry, "get_all_published_base_images", return_value=fake_published_images)
registry = await version_registry.VersionRegistry.load(mocker.Mock(), mock_dagger_client, fake_docker_credentials)
assert len(registry.entries) == 2, "Two entries should be in the registry"
version_1_entry = registry.get_entry_for_version(semver.VersionInfo.parse("1.0.0"))
assert version_1_entry is not None, "The version 1.0.0 should be in the registry even if not published"
assert version_1_entry.published, "The version 1.0.0 should be published"
version_2_entry = registry.get_entry_for_version(semver.VersionInfo.parse("2.0.0"))
assert version_2_entry is not None, "The version 2.0.0 should be in the registry even if not published"
assert version_2_entry.published is False, "The version 2.0.0 should not be published"

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,6 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.dependencies]
python = "^3.11"
airbyte-connectors-base-images = { path = "../base_images", develop = false }
connector-ops = { path = "../connector_ops", develop = false }
metadata-service = { path = "../metadata_service/lib", develop = false }
pydash = "^6.0.2"

View File

@@ -1,7 +1,5 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from base_images.python.bases import AirbytePythonConnectorBaseImage # type: ignore
CONNECTORS_QA_DOC_TEMPLATE_NAME = "qa_checks.md.j2"
DOCKER_HUB_PASSWORD_ENV_VAR_NAME = "DOCKER_HUB_PASSWORD"
DOCKER_HUB_USERNAME_ENV_VAR_NAME = "DOCKER_HUB_USERNAME"
@@ -22,4 +20,4 @@ SETUP_PY_FILE_NAME = "setup.py"
VALID_LICENSES = {"MIT", "ELV2", "AIRBYTE ENTERPRISE"}
# Derived from other constants
AIRBYTE_PYTHON_CONNECTOR_BASE_IMAGE_NAME = f"{DOCKER_INDEX}/{AirbytePythonConnectorBaseImage.repository}"
AIRBYTE_PYTHON_CONNECTOR_BASE_IMAGE_NAME = f"{DOCKER_INDEX}/airbyte/python-connector-base"

View File

@@ -122,7 +122,7 @@ Maintaining business logic in smaller projects also increases velocity, as intro
#### Good examples of this principle
- `connectors-qa`: We want to run specific static checks on all our connectors: we introduced a specific python package ([`connectors-qa`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/connectors_qa/README.md#L1))which declares and run the checks on connectors. We orchestrate the run of this package inside the [QaChecks](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/common.py#L122) step. This class is just aware of the tool location, its entry point, and what has to be mounted to the container for the command to run.
- Internal package testing: We expose an `airbyte-ci test` command which can run a CI pipeline on an internal poetry package. The pipeline logic is declared at the package level with `poe` tasks in the package `pyproject.toml`. `airbyte-ci` is made aware about what is has to run by parsing the content of the `[tool.airbyte_ci]` section of the `pyproject.toml`file. [Example](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/base_images/pyproject.toml#L39)
- Internal package testing: We expose an `airbyte-ci test` command which can run a CI pipeline on an internal poetry package. The pipeline logic is declared at the package level with `poe` tasks in the package `pyproject.toml`. `airbyte-ci` is made aware about what is has to run by parsing the content of the `[tool.airbyte_ci]` section of the `pyproject.toml`file. [Example](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/ci_credentials/pyproject.toml#L35)
### No command or pipeline should be language specific

View File

@@ -7,7 +7,6 @@ from abc import ABC
from typing import TYPE_CHECKING
import docker # type: ignore
from base_images.bases import AirbyteConnectorBaseImage # type: ignore
from click import UsageError
from connector_ops.utils import Connector # type: ignore
from dagger import Container, ExecError, Platform, QueryError
@@ -34,7 +33,7 @@ class BuildConnectorImagesBase(Step, ABC):
"""
context: ConnectorContext
USER = AirbyteConnectorBaseImage.USER
USER = "airbyte"
@property
def title(self) -> str:

View File

@@ -1,20 +1,20 @@
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, List, Optional
import dagger
import semver
import yaml
from base_images import version_registry # type: ignore
from connector_ops.utils import METADATA_FILE_NAME # type: ignore
from connector_ops.utils import METADATA_FILE_NAME, ConnectorLanguage # type: ignore
from pipelines.airbyte_ci.connectors.context import ConnectorContext
from pipelines.dagger.actions.system.docker import with_crane
from pipelines.helpers.connectors.dagger_fs import dagger_read_file, dagger_write_file
from pipelines.models.steps import StepModifyingFiles, StepResult, StepStatus
if TYPE_CHECKING:
from typing import Optional
import dagger
@@ -40,17 +40,51 @@ class UpdateBaseImageMetadata(StepModifyingFiles):
self.modified_files = []
self.connector_directory = connector_directory
def _get_repository_for_language(self, language: ConnectorLanguage) -> str:
"""Map connector language to DockerHub repository."""
if language in [ConnectorLanguage.PYTHON, ConnectorLanguage.LOW_CODE]:
return "airbyte/python-connector-base"
elif language is ConnectorLanguage.MANIFEST_ONLY:
return "airbyte/source-declarative-manifest"
elif language is ConnectorLanguage.JAVA:
return "airbyte/java-connector-base"
else:
raise NotImplementedError(f"Registry for language {language} is not implemented yet.")
def _parse_latest_stable_tag(self, tags: List[str]) -> Optional[str]:
"""Parse tags to find latest stable (non-prerelease) version."""
valid_versions = []
for tag in tags:
try:
version = semver.VersionInfo.parse(tag)
if not version.prerelease: # Exclude pre-release versions
valid_versions.append(version)
except ValueError:
continue # Skip non-semver tags
if valid_versions:
return str(max(valid_versions))
return None
async def get_latest_base_image_address(self) -> Optional[str]:
try:
if self.context.docker_hub_username is None or self.context.docker_hub_password is None:
if not (self.context.docker_hub_username and self.context.docker_hub_password):
raise ValueError("Docker Hub credentials are required to get the latest base image address")
version_registry_for_language = await version_registry.get_registry_for_language(
self.dagger_client,
self.context.connector.language,
(self.context.docker_hub_username.value, self.context.docker_hub_password.value),
cache_ttl_seconds=self.BASE_IMAGE_LIST_CACHE_TTL_SECONDS,
)
return version_registry_for_language.latest_not_pre_released_published_entry.published_docker_image.address
repository = self._get_repository_for_language(self.context.connector.language)
crane_container = with_crane(self.context)
# List all tags
tags_output = await crane_container.with_exec(["crane", "ls", f"docker.io/{repository}"]).stdout()
tags = [tag.strip() for tag in tags_output.strip().split("\n") if tag.strip()]
latest_tag = self._parse_latest_stable_tag(tags)
if latest_tag:
# Get the digest for the specific tag to ensure immutable reference
digest_output = await crane_container.with_exec(["crane", "digest", f"docker.io/{repository}:{latest_tag}"]).stdout()
digest = digest_output.strip()
return f"docker.io/{repository}:{latest_tag}@{digest}"
return None
except NotImplementedError:
return None

View File

@@ -7,7 +7,6 @@ from pathlib import Path
INTERNAL_POETRY_PACKAGES = [
"airbyte-ci/connectors/auto_merge",
"airbyte-ci/connectors/pipelines",
"airbyte-ci/connectors/base_images",
"airbyte-ci/connectors/connectors_insights",
"airbyte-ci/connectors/connector_ops",
"airbyte-ci/connectors/connectors_qa",

View File

@@ -1,28 +1,4 @@
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
[[package]]
name = "airbyte-connectors-base-images"
version = "1.6"
description = "This package is used to generate and publish the base images for Airbyte Connectors."
optional = false
python-versions = "^3.11"
groups = ["main"]
files = []
develop = true
[package.dependencies]
beartype = ">=0.18.2"
connector-ops = {path = "../connector_ops", develop = true}
dagger-io = "==0.13.3"
gitpython = "^3.1.35"
inquirer = "^3.1.3"
jinja2 = "^3.1.2"
rich = "^13.5.2"
semver = "^3.0.1"
[package.source]
type = "directory"
url = "../base_images"
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "airbyte-protocol-models"
@@ -51,19 +27,6 @@ files = [
{file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
]
[[package]]
name = "ansicon"
version = "1.89.0"
description = "Python wrapper for loading Jason Hood's ANSICON"
optional = false
python-versions = "*"
groups = ["main"]
markers = "platform_system == \"Windows\""
files = [
{file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"},
{file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"},
]
[[package]]
name = "anyio"
version = "4.9.0"
@@ -83,7 +46,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
trio = ["trio (>=0.26.1)"]
[[package]]
@@ -131,12 +94,12 @@ files = [
]
[package.extras]
benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"]
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
[[package]]
name = "auto-merge"
@@ -181,28 +144,12 @@ files = [
]
[package.extras]
dev = ["autoapi (>=0.9.0)", "click", "coverage (>=5.5)", "equinox", "jax[cpu]", "jaxtyping", "langchain", "mypy (>=0.800)", "nuitka (>=1.2.6)", "numba", "numpy", "pandera", "pydata-sphinx-theme (<=0.7.2)", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "rich-click", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)", "xarray"]
dev = ["autoapi (>=0.9.0)", "click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\"", "jax[cpu] ; sys_platform == \"linux\"", "jaxtyping ; sys_platform == \"linux\"", "langchain", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\"", "numba ; python_version < \"3.13.0\"", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pydata-sphinx-theme (<=0.7.2)", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "rich-click", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)", "xarray"]
doc-rtd = ["autoapi (>=0.9.0)", "pydata-sphinx-theme (<=0.7.2)", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)"]
test = ["click", "coverage (>=5.5)", "equinox", "jax[cpu]", "jaxtyping", "langchain", "mypy (>=0.800)", "nuitka (>=1.2.6)", "numba", "numpy", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "rich-click", "sphinx", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)", "xarray"]
test-tox = ["click", "equinox", "jax[cpu]", "jaxtyping", "langchain", "mypy (>=0.800)", "nuitka (>=1.2.6)", "numba", "numpy", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "rich-click", "sphinx", "typing-extensions (>=3.10.0.0)", "xarray"]
test = ["click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\"", "jax[cpu] ; sys_platform == \"linux\"", "jaxtyping ; sys_platform == \"linux\"", "langchain", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\"", "numba ; python_version < \"3.13.0\"", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "rich-click", "sphinx", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)", "xarray"]
test-tox = ["click", "equinox ; sys_platform == \"linux\"", "jax[cpu] ; sys_platform == \"linux\"", "jaxtyping ; sys_platform == \"linux\"", "langchain", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\"", "numba ; python_version < \"3.13.0\"", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "rich-click", "sphinx", "typing-extensions (>=3.10.0.0)", "xarray"]
test-tox-coverage = ["coverage (>=5.5)"]
[[package]]
name = "blessed"
version = "1.21.0"
description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities."
optional = false
python-versions = ">=2.7"
groups = ["main"]
files = [
{file = "blessed-1.21.0-py2.py3-none-any.whl", hash = "sha256:f831e847396f5a2eac6c106f4dfadedf46c4f804733574b15fe86d2ed45a9588"},
{file = "blessed-1.21.0.tar.gz", hash = "sha256:ece8bbc4758ab9176452f4e3a719d70088eb5739798cd5582c9e05f2a28337ec"},
]
[package.dependencies]
jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""}
wcwidth = ">=0.1.4"
[[package]]
name = "cachetools"
version = "5.5.2"
@@ -234,8 +181,8 @@ attrs = ">=23.1.0"
bson = ["pymongo (>=4.4.0)"]
cbor2 = ["cbor2 (>=5.4.6)"]
msgpack = ["msgpack (>=1.0.5)"]
msgspec = ["msgspec (>=0.18.5)"]
orjson = ["orjson (>=3.9.2)"]
msgspec = ["msgspec (>=0.18.5) ; implementation_name == \"cpython\""]
orjson = ["orjson (>=3.9.2) ; implementation_name == \"cpython\""]
pyyaml = ["pyyaml (>=6.0)"]
tomlkit = ["tomlkit (>=0.11.8)"]
ujson = ["ujson (>=5.7.0)"]
@@ -586,7 +533,7 @@ files = [
]
[package.extras]
toml = ["tomli"]
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "cryptography"
@@ -637,10 +584,10 @@ files = [
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"]
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
@@ -704,7 +651,7 @@ files = [
wrapt = ">=1.10,<2"
[package.extras]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"]
[[package]]
name = "docker"
@@ -741,22 +688,6 @@ files = [
{file = "dpath-2.2.0.tar.gz", hash = "sha256:34f7e630dc55ea3f219e555726f5da4b4b25f2200319c8e6902c394258dd6a3e"},
]
[[package]]
name = "editor"
version = "1.6.6"
description = "🖋 Open the default text editor 🖋"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "editor-1.6.6-py3-none-any.whl", hash = "sha256:e818e6913f26c2a81eadef503a2741d7cca7f235d20e217274a009ecd5a74abf"},
{file = "editor-1.6.6.tar.gz", hash = "sha256:bb6989e872638cd119db9a4fce284cd8e13c553886a1c044c6b8d8a160c871f8"},
]
[package.dependencies]
runs = "*"
xmod = "*"
[[package]]
name = "exceptiongroup"
version = "1.2.2"
@@ -819,7 +750,7 @@ gitdb = ">=4.0.1,<5"
[package.extras]
doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"]
test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"]
test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""]
[[package]]
name = "google-api-core"
@@ -844,7 +775,7 @@ requests = ">=2.18.0,<3.0.0"
[package.extras]
async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"]
grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"]
grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""]
grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
@@ -868,11 +799,11 @@ rsa = ">=3.1.4,<5"
[package.extras]
aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"]
enterprise-cert = ["cryptography", "pyopenssl"]
pyjwt = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"]
pyopenssl = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"]
pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"]
pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"]
reauth = ["pyu2f (>=0.1.5)"]
requests = ["requests (>=2.20.0,<3.0.0)"]
testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0)", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"]
testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"]
urllib3 = ["packaging", "urllib3"]
[[package]]
@@ -1042,14 +973,14 @@ httpx = {version = ">=0.23.1,<1", optional = true, markers = "extra == \"httpx\"
yarl = ">=1.6,<2.0"
[package.extras]
aiohttp = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)"]
all = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "websockets (>=10,<12)"]
aiohttp = ["aiohttp (>=3.8.0,<4) ; python_version <= \"3.11\"", "aiohttp (>=3.9.0b0,<4) ; python_version > \"3.11\""]
all = ["aiohttp (>=3.8.0,<4) ; python_version <= \"3.11\"", "aiohttp (>=3.9.0b0,<4) ; python_version > \"3.11\"", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "websockets (>=10,<12)"]
botocore = ["botocore (>=1.21,<2)"]
dev = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "httpx (>=0.23.1,<1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "sphinx (>=5.3.0,<6)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "vcrpy (==4.4.0)", "vcrpy (==7.0.0)", "websockets (>=10,<12)"]
dev = ["aiofiles", "aiohttp (>=3.8.0,<4) ; python_version <= \"3.11\"", "aiohttp (>=3.9.0b0,<4) ; python_version > \"3.11\"", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "httpx (>=0.23.1,<1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "sphinx (>=5.3.0,<6)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "vcrpy (==4.4.0) ; python_version <= \"3.8\"", "vcrpy (==7.0.0) ; python_version > \"3.8\"", "websockets (>=10,<12)"]
httpx = ["httpx (>=0.23.1,<1)"]
requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)"]
test = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "vcrpy (==4.4.0)", "vcrpy (==7.0.0)", "websockets (>=10,<12)"]
test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.4.0)", "vcrpy (==7.0.0)"]
test = ["aiofiles", "aiohttp (>=3.8.0,<4) ; python_version <= \"3.11\"", "aiohttp (>=3.9.0b0,<4) ; python_version > \"3.11\"", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "vcrpy (==4.4.0) ; python_version <= \"3.8\"", "vcrpy (==7.0.0) ; python_version > \"3.8\"", "websockets (>=10,<12)"]
test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.4.0) ; python_version <= \"3.8\"", "vcrpy (==7.0.0) ; python_version > \"3.8\""]
websockets = ["websockets (>=10,<12)"]
[[package]]
@@ -1215,7 +1146,7 @@ httpcore = "==1.*"
idna = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
@@ -1252,12 +1183,12 @@ files = [
zipp = ">=3.20"
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
perf = ["ipython"]
test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
type = ["pytest-mypy"]
[[package]]
@@ -1272,23 +1203,6 @@ files = [
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
]
[[package]]
name = "inquirer"
version = "3.4.0"
description = "Collection of common interactive command line user interfaces, based on Inquirer.js"
optional = false
python-versions = ">=3.8.1"
groups = ["main"]
files = [
{file = "inquirer-3.4.0-py3-none-any.whl", hash = "sha256:bb0ec93c833e4ce7b51b98b1644b0a4d2bb39755c39787f6a504e4fee7a11b60"},
{file = "inquirer-3.4.0.tar.gz", hash = "sha256:8edc99c076386ee2d2204e5e3653c2488244e82cb197b2d498b3c1b5ffb25d0b"},
]
[package.dependencies]
blessed = ">=1.19.0"
editor = ">=1.6.0"
readchar = ">=4.2.0"
[[package]]
name = "jinja2"
version = "3.1.6"
@@ -1307,22 +1221,6 @@ MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "jinxed"
version = "1.3.0"
description = "Jinxed Terminal Library"
optional = false
python-versions = "*"
groups = ["main"]
markers = "platform_system == \"Windows\""
files = [
{file = "jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5"},
{file = "jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf"},
]
[package.dependencies]
ansicon = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "macholib"
version = "1.16.3"
@@ -1644,7 +1542,6 @@ description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.10"
groups = ["main"]
markers = "python_version == \"3.11\""
files = [
{file = "numpy-2.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f4a922da1729f4c40932b2af4fe84909c7a6e167e6e99f71838ce3a29f3fe26"},
{file = "numpy-2.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6f91524d31b34f4a5fee24f5bc16dcd1491b668798b6d85585d836c1e633a6a"},
@@ -2271,7 +2168,7 @@ files = [
]
[package.extras]
dev = ["Sphinx", "black", "build", "coverage", "docformatter", "flake8", "flake8-black", "flake8-bugbear", "flake8-isort", "importlib-metadata (<5)", "invoke", "isort", "pylint", "pytest", "pytest-cov", "sphinx-rtd-theme", "tox", "twine", "wheel"]
dev = ["Sphinx", "black", "build", "coverage", "docformatter", "flake8", "flake8-black", "flake8-bugbear", "flake8-isort", "importlib-metadata (<5) ; python_version == \"3.7\"", "invoke", "isort", "pylint", "pytest", "pytest-cov", "sphinx-rtd-theme", "tox", "twine", "wheel"]
[[package]]
name = "pygit2"
@@ -2659,18 +2556,6 @@ files = [
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "readchar"
version = "4.2.1"
description = "Library to easily read single chars and key strokes"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "readchar-4.2.1-py3-none-any.whl", hash = "sha256:a769305cd3994bb5fa2764aa4073452dc105a4ec39068ffe6efd3c20c60acc77"},
{file = "readchar-4.2.1.tar.gz", hash = "sha256:91ce3faf07688de14d800592951e5575e9c7a3213738ed01d394dcc949b79adb"},
]
[[package]]
name = "requests"
version = "2.32.3"
@@ -2831,21 +2716,6 @@ files = [
{file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"},
]
[[package]]
name = "runs"
version = "1.2.2"
description = "🏃 Run a block of text as a subprocess 🏃"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "runs-1.2.2-py3-none-any.whl", hash = "sha256:0980dcbc25aba1505f307ac4f0e9e92cbd0be2a15a1e983ee86c24c87b839dfd"},
{file = "runs-1.2.2.tar.gz", hash = "sha256:9dc1815e2895cfb3a48317b173b9f1eac9ba5549b36a847b5cc60c3bf82ecef1"},
]
[package.dependencies]
xmod = "*"
[[package]]
name = "segment-analytics-python"
version = "2.3.3"
@@ -2949,13 +2819,13 @@ files = [
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
[[package]]
name = "simpleeval"
@@ -3203,23 +3073,11 @@ files = [
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]]
name = "wrapt"
version = "1.17.2"
@@ -3321,18 +3179,6 @@ files = [
{file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"},
]
[[package]]
name = "xmod"
version = "1.8.1"
description = "🌱 Turn any object into a module 🌱"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "xmod-1.8.1-py3-none-any.whl", hash = "sha256:a24e9458a4853489042522bdca9e50ee2eac5ab75c809a91150a8a7f40670d48"},
{file = "xmod-1.8.1.tar.gz", hash = "sha256:38c76486b9d672c546d57d8035df0beb7f4a9b088bc3fb2de5431ae821444377"},
]
[[package]]
name = "yarl"
version = "1.20.0"
@@ -3465,14 +3311,14 @@ files = [
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = "~3.11"
content-hash = "33fe31639d0b0fcf89f8a426f56c7f2bc885406f97ad57fa93d304e46509e160"
content-hash = "40348b618d48960db388c3f77018908eb7ad483306154591ae7ce797dde399a6"

View File

@@ -23,7 +23,6 @@ airbyte-protocol-models = "^0.16.0"
jinja2 = "^3.1.6"
MarkupSafe = "==2.1.5" # pin transitive dependency to avoid issues with version resolution
requests = "^2.31"
airbyte-connectors-base-images = { path = "../base_images", develop = true }
connector-ops = { path = "../connector_ops", develop = true }
toml = "^0.10.2"
types-requests = "^2.31"

View File

@@ -1,5 +1,3 @@
# Migrated from airbyte-ci/connectors/base_images/README.md
# https://hub.docker.com/_/amazoncorretto/tags?name=21-al2023
ARG BASE_IMAGE=docker.io/amazoncorretto:21-al2023
FROM ${BASE_IMAGE}

View File

@@ -1,5 +1,3 @@
# Migrated from airbyte-ci/connectors/base_images/README.md
# https://hub.docker.com/_/python/tags?name=3.11.13-slim-bookworm
ARG BASE_IMAGE=docker.io/python:3.11.13-slim-bookworm@sha256:139020233cc412efe4c8135b0efe1c7569dc8b28ddd88bddb109b764f8977e30
FROM ${BASE_IMAGE}