From 3099aeaf767fa0e631e74a67ff4d2385ccc81a1c Mon Sep 17 00:00:00 2001 From: Dris Date: Sun, 8 May 2022 15:28:32 +0100 Subject: [PATCH 1/2] Fix defaultAirflowRepository comment Signed-off-by: Dris --- chart/values.yaml | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/chart/values.yaml b/chart/values.yaml index 0ec7be6bdd544..882e164fc2821 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -42,7 +42,7 @@ securityContext: {} # Used for mount paths airflowHome: /opt/airflow -# Default airflow repository -- overrides all the specific images below +# Default airflow repository -- overridden by all the specific images below defaultAirflowRepository: apache/airflow # Default airflow tag to deploy @@ -1406,8 +1406,12 @@ cleanup: # Command to use when running the cleanup cronjob (templated). command: ~ # Args to use when running the cleanup cronjob (templated). - args: ["bash", "-c", "exec airflow kubernetes cleanup-pods --namespace={{ .Release.Namespace }}"] - + args: + [ + "bash", + "-c", + "exec airflow kubernetes cleanup-pods --namespace={{ .Release.Namespace }}", + ] # Select certain nodes for airflow cleanup pods. nodeSelector: {} @@ -1462,23 +1466,23 @@ config: core: dags_folder: '{{ include "airflow_dags" . }}' # This is ignored when used with the official Docker image - load_examples: 'False' - executor: '{{ .Values.executor }}' + load_examples: "False" + executor: "{{ .Values.executor }}" # For Airflow 1.10, backward compatibility; moved to [logging] in 2.0 - colored_console_log: 'False' + colored_console_log: "False" remote_logging: '{{- ternary "True" "False" .Values.elasticsearch.enabled }}' logging: remote_logging: '{{- ternary "True" "False" .Values.elasticsearch.enabled }}' - colored_console_log: 'False' + colored_console_log: "False" metrics: statsd_on: '{{ ternary "True" "False" .Values.statsd.enabled }}' statsd_port: 9125 statsd_prefix: airflow statsd_host: '{{ printf "%s-statsd" .Release.Name }}' webserver: - enable_proxy_fix: 'True' + enable_proxy_fix: "True" # For Airflow 1.10 - rbac: 'True' + rbac: "True" celery: worker_concurrency: 16 scheduler: @@ -1490,27 +1494,27 @@ config: # `run_duration` included for Airflow 1.10 backward compatibility; removed in 2.0. run_duration: 41460 elasticsearch: - json_format: 'True' + json_format: "True" log_id_template: "{dag_id}_{task_id}_{execution_date}_{try_number}" elasticsearch_configs: max_retries: 3 timeout: 30 - retry_timeout: 'True' + retry_timeout: "True" kerberos: - keytab: '{{ .Values.kerberos.keytabPath }}' - reinit_frequency: '{{ .Values.kerberos.reinitFrequency }}' - principal: '{{ .Values.kerberos.principal }}' - ccache: '{{ .Values.kerberos.ccacheMountPath }}/{{ .Values.kerberos.ccacheFileName }}' + keytab: "{{ .Values.kerberos.keytabPath }}" + reinit_frequency: "{{ .Values.kerberos.reinitFrequency }}" + principal: "{{ .Values.kerberos.principal }}" + ccache: "{{ .Values.kerberos.ccacheMountPath }}/{{ .Values.kerberos.ccacheFileName }}" celery_kubernetes_executor: - kubernetes_queue: 'kubernetes' + kubernetes_queue: "kubernetes" kubernetes: - namespace: '{{ .Release.Namespace }}' + namespace: "{{ .Release.Namespace }}" airflow_configmap: '{{ include "airflow_config" . }}' airflow_local_settings_configmap: '{{ include "airflow_config" . }}' pod_template_file: '{{ include "airflow_pod_template_file" . }}/pod_template_file.yaml' - worker_container_repository: '{{ .Values.images.airflow.repository | default .Values.defaultAirflowRepository }}' - worker_container_tag: '{{ .Values.images.airflow.tag | default .Values.defaultAirflowTag }}' - multi_namespace_mode: '{{ if .Values.multiNamespaceMode }}True{{ else }}False{{ end }}' + worker_container_repository: "{{ .Values.images.airflow.repository | default .Values.defaultAirflowRepository }}" + worker_container_tag: "{{ .Values.images.airflow.tag | default .Values.defaultAirflowTag }}" + multi_namespace_mode: "{{ if .Values.multiNamespaceMode }}True{{ else }}False{{ end }}" # yamllint enable rule:line-length # Whether Airflow can launch workers and/or pods in multiple namespaces From b8f067144a709b9944b0800e5cb494c527bc1f0f Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sat, 7 May 2022 23:53:11 +0200 Subject: [PATCH 2/2] Fix defaultAirflowRepository comment --- .pre-commit-config.yaml | 40 +-- BREEZE.rst | 29 +++ chart/values.yaml | 2 +- dev/README_RELEASE_AIRFLOW.md | 45 +++- dev/breeze/README.md | 2 +- dev/breeze/setup.cfg | 2 +- .../commands/release_management_commands.py | 224 ++++++++++++++++- .../src/airflow_breeze/global_constants.py | 4 +- dev/prepare_prod_docker_images.sh | 93 ------- dev/retag_docker_images.py | 5 +- docs/docker-stack/build-arg-ref.rst | 5 + docs/docker-stack/build.rst | 228 +++++++++++------- images/breeze/output-commands.svg | 53 ++-- images/breeze/output-release-prod-images.svg | 151 ++++++++++++ setup.cfg | 2 +- 15 files changed, 636 insertions(+), 249 deletions(-) delete mode 100755 dev/prepare_prod_docker_images.sh create mode 100644 images/breeze/output-release-prod-images.svg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8f8783cd5603..bc736d0028452 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -324,7 +324,7 @@ repos: files: ^setup\.cfg$|^setup\.py$ pass_filenames: false entry: ./scripts/ci/pre_commit/pre_commit_check_order_setup.py - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.2.0'] - id: check-extra-packages-references name: Checks setup extra packages description: Checks if all the libraries in setup.py are listed in extra-packages-ref.rst file @@ -332,7 +332,7 @@ repos: files: ^setup\.py$|^docs/apache-airflow/extra-packages-ref\.rst$ pass_filenames: false entry: ./scripts/ci/pre_commit/pre_commit_check_setup_extra_packages_ref.py - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] # This check might be removed when min-airflow-version in providers is 2.2 - id: check-airflow-2-1-compatibility name: Check that providers are 2.1 compatible. @@ -340,14 +340,14 @@ repos: language: python pass_filenames: true files: ^airflow/providers/.*\.py$ - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: update-breeze-file name: Update output of breeze commands in BREEZE.rst entry: ./scripts/ci/pre_commit/pre_commit_breeze_cmd_line.py language: python files: ^BREEZE\.rst$|^dev/breeze/.*$ pass_filenames: false - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: update-local-yml-file name: Update mounts in the local yml file entry: ./scripts/ci/pre_commit/pre_commit_local_yml_mounts.sh @@ -379,7 +379,7 @@ repos: language: python files: ^Dockerfile$ pass_filenames: false - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: update-supported-versions name: Updates supported versions in documentation entry: ./scripts/ci/pre_commit/pre_commit_supported_versions.py @@ -582,7 +582,7 @@ repos: - 'jsonschema>=3.2.0,<5.0.0' - 'tabulate==0.8.8' - 'jsonpath-ng==1.5.3' - - 'rich>=12.3.0' + - 'rich==12.3.0' - id: check-pre-commit-information-consistent name: Update information about pre-commit hooks and verify ids and names entry: ./scripts/ci/pre_commit/pre_commit_check_pre_commit_hooks.py @@ -590,7 +590,7 @@ repos: - --max-length=70 language: python files: ^\.pre-commit-config\.yaml$|^scripts/ci/pre_commit/pre_commit_check_pre_commit_hook_names\.py$ - additional_dependencies: ['pyyaml', 'jinja2', 'black==22.3.0', 'tabulate', 'rich>=12.3.0'] + additional_dependencies: ['pyyaml', 'jinja2', 'black==22.3.0', 'tabulate', 'rich==12.3.0'] require_serial: true pass_filenames: false - id: check-airflow-providers-have-extras @@ -600,7 +600,7 @@ repos: files: ^setup\.py$|^airflow/providers/.*\.py$ pass_filenames: false require_serial: true - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: update-breeze-readme-config-hash name: Update Breeze README.md with config files hash language: python @@ -615,7 +615,7 @@ repos: files: ^dev/breeze/.*$ pass_filenames: false require_serial: true - additional_dependencies: ['click', 'rich>=12.3.0'] + additional_dependencies: ['click', 'rich==12.3.0'] - id: check-system-tests-present name: Check if system tests have required segments of code entry: ./scripts/ci/pre_commit/pre_commit_check_system_tests.py @@ -623,7 +623,7 @@ repos: files: ^tests/system/.*/example_[^/]*.py$ exclude: ^tests/system/providers/google/bigquery/example_bigquery_queries\.py$ pass_filenames: true - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: lint-markdown name: Run markdownlint description: Checks the style of Markdown files. @@ -714,7 +714,7 @@ repos: language: python pass_filenames: true files: ^\.github/workflows/.*\.yml$ - additional_dependencies: ['PyYAML', 'rich>=12.3.0'] + additional_dependencies: ['PyYAML', 'rich==12.3.0'] - id: check-docstring-param-types name: Check that docstrings do not specify param types entry: ./scripts/ci/pre_commit/pre_commit_docstring_param_type.py @@ -722,7 +722,7 @@ repos: pass_filenames: true files: \.py$ exclude: ^airflow/_vendor/ - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: lint-chart-schema name: Lint chart/values.schema.json file entry: ./scripts/ci/pre_commit/pre_commit_chart_schema.py @@ -757,7 +757,7 @@ repos: entry: ./scripts/ci/pre_commit/pre_commit_mypy.py files: ^dev/.*\.py$ require_serial: true - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: run-mypy name: Run mypy for core language: python @@ -765,14 +765,14 @@ repos: files: \.py$ exclude: ^provider_packages|^docs|^airflow/_vendor/|^airflow/providers|^airflow/migrations|^dev require_serial: true - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: run-mypy name: Run mypy for providers language: python entry: ./scripts/ci/pre_commit/pre_commit_mypy.py --namespace-packages files: ^airflow/providers/.*\.py$ require_serial: true - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: run-mypy name: Run mypy for /docs/ folder language: python @@ -780,7 +780,7 @@ repos: files: ^docs/.*\.py$ exclude: ^docs/rtd-deprecation require_serial: true - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: run-flake8 name: Run flake8 language: python @@ -788,7 +788,7 @@ repos: files: \.py$ pass_filenames: true exclude: ^airflow/_vendor/ - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: lint-javascript name: ESLint against airflow/ui language: python @@ -796,7 +796,7 @@ repos: files: ^airflow/ui/ entry: ./scripts/ci/pre_commit/pre_commit_ui_lint.py pass_filenames: false - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: lint-javascript name: ESLint against current UI JavaScript files language: python @@ -804,12 +804,12 @@ repos: files: ^airflow/www/static/js/ entry: ./scripts/ci/pre_commit/pre_commit_www_lint.py pass_filenames: false - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] - id: update-migration-references name: Update migration ref doc language: python entry: ./scripts/ci/pre_commit/pre_commit_migration_reference.py pass_filenames: false files: ^airflow/migrations/versions/.*\.py$|^docs/apache-airflow/migrations-ref\.rst$ - additional_dependencies: ['rich>=12.3.0'] + additional_dependencies: ['rich==12.3.0'] ## ONLY ADD PRE-COMMITS HERE THAT REQUIRE CI IMAGE diff --git a/BREEZE.rst b/BREEZE.rst index 3af9d96c0540e..ac56edfad370e 100644 --- a/BREEZE.rst +++ b/BREEZE.rst @@ -556,6 +556,7 @@ do not need or have no access to run). Those are usually connected with releasin ``breeze prepare-provider-packages`` (when releasing provider packages) * Finding the updated dependencies since the last successful build when we have conflict with ``breeze find-newer-dependencies`` command +* Release production images to DockerHub with ``breeze release-prod-images`` command Details of Breeze usage @@ -947,6 +948,34 @@ Those are all available flags of ``verify-prod-image`` command: :width: 100% :alt: Breeze verify-prod-image +Releasing Production images to DockerHub +---------------------------------------- + +The **Production image** can be released by release managers who have permissions to push the image. This +happens only when there is an RC candidate or final version of Airflow released. + +You release "regular" and "slim" images as separate steps. + +Releasing "regular" images: + +.. code-block:: bash + + breeze release-prod-images --airflow-version 2.4.0 + +Or "slim" images: + +.. code-block:: bash + + breeze release-prod-images --airflow-version 2.4.0 --slim-images + +By default when you are releasing the "final" image, we also tag image with "latest" tags but this +step can be skipped if you pass the ``--skip-latest`` flag. + +These are all of the available flags for the ``release-prod-images`` command: + +.. image:: ./images/breeze/output-release-prod-images.svg + :width: 100% + :alt: Release prod images Running static checks diff --git a/chart/values.yaml b/chart/values.yaml index 0ec7be6bdd544..d97073b6a7124 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -42,7 +42,7 @@ securityContext: {} # Used for mount paths airflowHome: /opt/airflow -# Default airflow repository -- overrides all the specific images below +# Default airflow repository -- overridden by all the specific images below defaultAirflowRepository: apache/airflow # Default airflow tag to deploy diff --git a/dev/README_RELEASE_AIRFLOW.md b/dev/README_RELEASE_AIRFLOW.md index e1ab10da8c524..ebe94e6abbe23 100644 --- a/dev/README_RELEASE_AIRFLOW.md +++ b/dev/README_RELEASE_AIRFLOW.md @@ -133,7 +133,7 @@ for the PRs that are already correctly assigned to the milestone. You can also a question with `--assume-yes` flag. You cn review the list of PRs cherry-picked and produce a nice summary with `--print-summary` (this flag -assumes `--skip-assigned` so that the summary can be produced without questions: +assumes the `--skip-assigned` flag, so that the summary can be produced without questions: ```shell ,/dev/assign_cherry_picked_prs_with_milestone.py assign-prs --previous-release v2-2-stable \ @@ -493,10 +493,13 @@ to have an environment prepared to build multi-platform images. You can achieve ## Prerequisites -You need to have buildx plugin installed to run the build. Also you need to have regctl +You need to have buildx plugin installed to run the build. Also, you need to have regctl installed from https://github.com/regclient/regclient in order to tag the multi-platform images in DockerHub. The script to build images will refuse to work if you do not have those two installed. +You also need to have the right permissions to push the images, so you should run +`docker login` before and authenticate with your DockerHub token. + ## Setting environment with emulation According to the [official installation instructions](https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images) @@ -540,9 +543,16 @@ docker buildx ls airflow_cache1 tcp://127.0.0.1:2375 ``` +Preparing regular images: + +```shell script +breeze release-prod-images --airflow-version "${VERSION}" +``` + +Preparing slim images: ```shell script -./dev/prepare_prod_docker_images.sh ${VERSION} +breeze release-prod-images --airflow-version "${VERSION}" --slim-images ``` This will wipe Breeze cache and docker-context-files in order to make sure the build is "clean". It @@ -998,19 +1008,34 @@ At this point we release an official package: ## Manually prepare production Docker Image - Note that this scripts prepares multi-platform image, so you need to fulfill prerequisites as described above in the preparation of RC images. +Note that by default the `latest` images tagged are aliased to the just released image which is the usual +way we release. For example when you are releasing 2.3.N image and 2.3 is our latest branch the new image is +marked as "latest". + +In case we are releasing (which almost never happens so far) a critical bugfix release in one of +the older branches, you should add the `--skip-latest` flag. + +Preparing regular images: + +```shell script +breeze release-prod-images --airflow-version "${VERSION}" +``` + +Preparing slim images: + +```shell script +breeze release-prod-images --airflow-version "${VERSION}" --slim-images +``` + +Preparing a release that is not in the latest branch: + ```shell script -./dev/prepare_prod_docker_images.sh ${VERSION} +breeze release-prod-images --airflow-version "${VERSION}" --slim-images --skip-latest ``` -Note! When you release the 'final' (non-rc) version you will be asked if you want to -tag the images as latest - if you are releasing the latest stable branch, you -should answer y and tags will be created and pushed. If you are releasing a -patch release from an older branch, you should answer n and creating tags will -be skipped. ## Publish documentation diff --git a/dev/breeze/README.md b/dev/breeze/README.md index 2df4bfac42ff4..608f41a38003a 100644 --- a/dev/breeze/README.md +++ b/dev/breeze/README.md @@ -52,6 +52,6 @@ PLEASE DO NOT MODIFY THE HASH BELOW! IT IS AUTOMATICALLY UPDATED BY PRE-COMMIT. --------------------------------------------------------------------------------------------------------- -Package config hash: 6ab00f80228fe4392cdb1263a52c55416c153052010bd5baca25add84da7475d8601cc399342c72ad95df0e1f2de99e2a99e3d0b19631ff0480554360a9578b4 +Package config hash: dfdac0e241f273bbe3ae4a3d5e9c9f39426d792c2468753d94fd0dfa6b5d46331703c5d419e5e8bf3c199129acde63cfa61e30c6a3b9a920047e0d115fd1d57c --------------------------------------------------------------------------------------------------------- diff --git a/dev/breeze/setup.cfg b/dev/breeze/setup.cfg index 74698073139bb..af23bc1697da9 100644 --- a/dev/breeze/setup.cfg +++ b/dev/breeze/setup.cfg @@ -63,7 +63,7 @@ install_requires = pytest-xdist pyyaml requests - rich>=12.3.0 + rich==12.3.0 rich_click [options.packages.find] diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py index 1b4602c450492..8b8aab039aad2 100644 --- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py @@ -17,18 +17,24 @@ import multiprocessing as mp import shlex import sys +import time +from copy import deepcopy +from re import match from subprocess import CalledProcessError, CompletedProcess -from typing import IO, List, Optional, Tuple, Union +from typing import IO, Dict, List, Optional, Tuple, Union import click from airflow_breeze.commands.main_command import main from airflow_breeze.global_constants import ( ALLOWED_GENERATE_CONSTRAINTS_MODES, + ALLOWED_PLATFORMS, + CURRENT_PYTHON_MAJOR_MINOR_VERSIONS, DEFAULT_PYTHON_MAJOR_MINOR_VERSION, MOUNT_ALL, MOUNT_NONE, MOUNT_SELECTED, + MULTI_PLATFORM, ) from airflow_breeze.params.shell_params import ShellParams from airflow_breeze.utils.common_options import ( @@ -97,7 +103,6 @@ { "name": "Provider documentation preparation flags", "options": [ - "--skip-package-verification", "--debug", ], } @@ -121,13 +126,26 @@ ], }, ], + "breeze release-prod-images": [ + { + "name": "Release PROD IMAGE flags", + "options": [ + "--airflow-version", + "--dockerhub-repo", + "--slim-images", + "--limit-python", + "--limit-platform", + "--skip-latest", + ], + } + ], "breeze find-newer-dependencies": [ { "name": "Find newer dependencies flags", "options": [ "--python", "--timezone", - "--constraints-reference", + "--constraints-branch", "--updated-on-or-after", "--max-age", ], @@ -142,6 +160,7 @@ "prepare-provider-documentation", "prepare-provider-packages", "prepare-airflow-package", + "release-prod-images", "generate-constraints", "find-newer-dependencies", ], @@ -519,6 +538,205 @@ def verify_provider_packages( sys.exit(result_command.returncode) +def convert_build_args_dict_to_array_of_args(build_args: Dict[str, str]) -> List[str]: + array_of_args = [] + for key, value in build_args.items(): + array_of_args.append("--build-arg") + array_of_args.append(f'{key}={value}') + return array_of_args + + +def alias_image(image_from: str, image_to: str, dry_run: bool, verbose: bool): + get_console().print(f"[info]Creating {image_to} alias for {image_from}[/]") + run_command( + ["regctl", "image", "copy", "--force-recursive", "--digest-tags", image_from, image_to], + dry_run=dry_run, + verbose=verbose, + ) + + +@main.command( + name="release-prod-images", help="Release production images to DockerHub (needs DockerHub permissions)." +) +@click.option('--airflow-version', required=True, help="Airflow version to release (2.3.0, 2.3.0rc1 etc.)") +@click.option( + '--dockerhub-repo', + default="apache/airflow", + show_default=True, + help="DockerHub repository for the images", +) +@click.option( + '--slim-images', + is_flag=True, + help='Whether to prepare slim images instead of the regular ones.', +) +@click.option( + '--limit-python', + type=BetterChoice(CURRENT_PYTHON_MAJOR_MINOR_VERSIONS), + help="Specific python to build slim images for (if not specified - the images are built for all" + " available python versions)", +) +@click.option( + '--limit-platform', + type=BetterChoice(ALLOWED_PLATFORMS), + default=MULTI_PLATFORM, + show_default=True, + help="Specific platform to build images for (if not specified, multiplatform images will be built.", +) +@click.option( + '--skip-latest', + is_flag=True, + help="Whether to skip publishing the latest images (so that 'latest' images are not updated). " + "This should only be used if you release image for previous branches. Automatically set when " + "rc/alpha/beta images are built.", +) +@option_verbose +@option_dry_run +def release_prod_images( + airflow_version: str, + dockerhub_repo: str, + slim_images: bool, + limit_platform: str, + limit_python: Optional[str], + skip_latest: bool, + verbose: bool, + dry_run: bool, +): + if not match(r"^\d*\.\d*\.\d*$", airflow_version): + get_console().print( + f"[warning]Skipping latest image tagging as this is a pre-release version: {airflow_version}" + ) + skip_latest = True + else: + if skip_latest: + get_console().print("[info]Skipping latest image tagging as user requested it.[/]") + else: + get_console().print( + "[info]Also tagging the images with latest tags as this is release version.[/]" + ) + result_docker_buildx = run_command( + ["docker", 'buildx', 'version'], check=False, dry_run=dry_run, verbose=verbose + ) + if result_docker_buildx.returncode != 0: + get_console().print("[error]Docker buildx plugin must be installed to release the images[/]") + get_console().print() + get_console().print("See https://docs.docker.com/buildx/working-with-buildx/ for installation info.") + sys.exit(1) + result_inspect_builder = run_command( + ["docker", 'buildx', 'inspect', 'airflow_cache'], check=False, dry_run=dry_run, verbose=verbose + ) + if result_inspect_builder.returncode != 0: + get_console().print("[error]Regctl must be installed and on PATH to release the images[/]") + get_console().print() + get_console().print( + "See https://github.com/apache/airflow/blob/main/dev/README_RELEASE_AIRFLOW.md" + "#setting-up-cache-refreshing-with-hardware-armamd-support for " + "instructions on setting it up." + ) + sys.exit(1) + result_regctl = run_command(["regctl", 'version'], check=False, dry_run=dry_run, verbose=verbose) + if result_regctl.returncode != 0: + get_console().print("[error]Regctl must be installed and on PATH to release the images[/]") + get_console().print() + get_console().print( + "See https://github.com/regclient/regclient/blob/main/docs/regctl.md for installation info." + ) + sys.exit(1) + python_versions = CURRENT_PYTHON_MAJOR_MINOR_VERSIONS if limit_python is None else [limit_python] + for python in python_versions: + if slim_images: + slim_build_args = { + "AIRFLOW_EXTRAS": "", + "AIRFLOW_CONSTRAINTS": "constraints-no-providers", + "PYTHON_BASE_IMAGE": f"python:{python}-slim-bullseye", + "AIRFLOW_VERSION": airflow_version, + } + get_console().print(f"[info]Building slim {airflow_version} image for Python {python}[/]") + python_build_args = deepcopy(slim_build_args) + slim_image_name = f"{dockerhub_repo}:slim-{airflow_version}-python{python}" + docker_buildx_command = [ + "docker", + "buildx", + "build", + "--builder", + "airflow_cache", + *convert_build_args_dict_to_array_of_args(build_args=python_build_args), + "--platform", + limit_platform, + ".", + "-t", + slim_image_name, + "--push", + ] + run_command(docker_buildx_command, verbose=verbose, dry_run=dry_run) + if python == DEFAULT_PYTHON_MAJOR_MINOR_VERSION: + alias_image( + slim_image_name, + f"{dockerhub_repo}:slim-{airflow_version}", + verbose=verbose, + dry_run=dry_run, + ) + else: + get_console().print(f"[info]Building regular {airflow_version} image for Python {python}[/]") + image_name = f"{dockerhub_repo}:{airflow_version}-python{python}" + regular_build_args = { + "PYTHON_BASE_IMAGE": f"python:{python}-slim-bullseye", + "AIRFLOW_VERSION": airflow_version, + } + docker_buildx_command = [ + "docker", + "buildx", + "build", + "--builder", + "airflow_cache", + *convert_build_args_dict_to_array_of_args(build_args=regular_build_args), + "--platform", + limit_platform, + ".", + "-t", + image_name, + "--push", + ] + run_command(docker_buildx_command, verbose=verbose, dry_run=dry_run) + if python == DEFAULT_PYTHON_MAJOR_MINOR_VERSION: + alias_image( + image_name, f"{dockerhub_repo}:{airflow_version}", verbose=verbose, dry_run=dry_run + ) + # in case of re-tagging the images might need few seconds to refresh multi-platform images in DockerHub + time.sleep(10) + if not skip_latest: + get_console().print("[info]Replacing latest images with links to the newly created images.[/]") + for python in python_versions: + if slim_images: + alias_image( + f"{dockerhub_repo}:slim-{airflow_version}-python{python}", + f"{dockerhub_repo}:slim-latest-python{python}", + verbose=verbose, + dry_run=dry_run, + ) + else: + alias_image( + f"{dockerhub_repo}:{airflow_version}-python{python}", + f"{dockerhub_repo}:latest-python{python}", + verbose=verbose, + dry_run=dry_run, + ) + if slim_images: + alias_image( + f"{dockerhub_repo}:slim-{airflow_version}", + f"{dockerhub_repo}:slim-latest", + verbose=verbose, + dry_run=dry_run, + ) + else: + alias_image( + f"{dockerhub_repo}:{airflow_version}", + f"{dockerhub_repo}:latest", + verbose=verbose, + dry_run=dry_run, + ) + + @main.command(name="find-newer-dependencies", help="Finds which dependencies are being upgraded.") @option_timezone @option_airflow_constraints_reference diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py index 3c9b95042d22a..6b72e97014e2b 100644 --- a/dev/breeze/src/airflow_breeze/global_constants.py +++ b/dev/breeze/src/airflow_breeze/global_constants.py @@ -35,6 +35,7 @@ ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS = ['3.7', '3.8', '3.9', '3.10'] DEFAULT_PYTHON_MAJOR_MINOR_VERSION = ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS[0] ALLOWED_BACKENDS = ['sqlite', 'mysql', 'postgres', 'mssql'] +ALLOWED_PROD_BACKENDS = ['mysql', 'postgres', 'mssql'] DEFAULT_BACKEND = ALLOWED_BACKENDS[0] ALLOWED_INTEGRATIONS = [ 'cassandra', @@ -84,7 +85,8 @@ ALLOWED_INSTALLATION_METHODS = ['.', 'apache-airflow'] ALLOWED_DEBIAN_VERSIONS = ['bullseye', 'buster'] ALLOWED_BUILD_CACHE = ["pulled", "local", "disabled"] -ALLOWED_PLATFORMS = ["linux/amd64", "linux/arm64", "linux/amd64,linux/arm64"] +MULTI_PLATFORM = "linux/amd64,linux/arm64" +ALLOWED_PLATFORMS = ["linux/amd64", "linux/arm64", MULTI_PLATFORM] ALLOWED_USE_AIRFLOW_VERSIONS = ['none', 'wheel', 'sdist'] PARAM_NAME_DESCRIPTION = { diff --git a/dev/prepare_prod_docker_images.sh b/dev/prepare_prod_docker_images.sh deleted file mode 100755 index ac446ad166f60..0000000000000 --- a/dev/prepare_prod_docker_images.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -AIRFLOW_SOURCES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)" -export AIRFLOW_SOURCES_DIR - -set -e - -CURRENT_PYTHON_MAJOR_MINOR_VERSIONS=("3.7" "3.8" "3.9" "3.10") - -usage() { - local cmdname - cmdname="$(basename -- "$0")" - - cat << EOF -Usage: ${cmdname} - -Prepares prod docker images for the version specified. - -EOF -} - -if [[ "$#" -ne 1 ]]; then - >&2 echo "You must provide Airflow version." - usage - exit 1 -fi - -airflow_version="${1}" - -if ! docker buildx version; then - >&2 echo "The buildx plugin must be installed!" - exit 1 -fi - -if ! regctl version; then - >&2 echo "The regctl must be installed and in the PATH!" - exit 1 -fi - - -for python_version in "${CURRENT_PYTHON_MAJOR_MINOR_VERSIONS[@]}" -do - image_name="apache/airflow:${airflow_version}-python${python_version}" - docker buildx build --builder "airflow_cache" \ - --build-arg PYTHON_BASE_IMAGE="python:${python_version}-slim-bullseye" \ - --build-arg AIRFLOW_VERSION="${airflow_version}" \ - --platform linux/amd64,linux/arm64 . -t "${image_name}" --push - docker pull "${image_name}" - breeze verify-prod-image --image-name "${image_name}" - if [[ ${python_version} == "3.7" ]]; then - regctl image copy "${image_name}" "apache/airflow:${airflow_version}" - fi -done - -if [[ ${INSTALL_AIRFLOW_VERSION} =~ .*rc.* ]]; then - echo - echo "Skipping tagging latest as this is an rc version" - echo - exit -fi - -echo "Should we tag version ${1} with latest tag [y/N]" -read -r RESPONSE - -if [[ ${RESPONSE} == 'n' || ${RESPONSE} = 'N' ]]; then - echo - echo "Skip tagging the image with latest tag." - echo - exit -fi - -for python_version in "${CURRENT_PYTHON_MAJOR_MINOR_VERSIONS[@]}" -do - regctl image copy "apache/airflow:${airflow_version}-python${python_version}" \ - "apache/airflow:latest-python${python_version}" -done - -regctl image copy "apache/airflow:${airflow_version}" "apache/airflow:latest" diff --git a/dev/retag_docker_images.py b/dev/retag_docker_images.py index be4e1bf363793..595059eca0574 100755 --- a/dev/retag_docker_images.py +++ b/dev/retag_docker_images.py @@ -61,7 +61,10 @@ def pull_push_all_images( prefix=target_prefix, branch=target_branch, repo=target_repo, python=python ) print(f"Copying image: {source_image} -> {target_image}") - subprocess.run(["regctl", "image", "copy", source_image, target_image], check=True) + subprocess.run( + ["regctl", "image", "copy", "--force-recursive", "--digest-tags", source_image, target_image], + check=True, + ) @click.group(invoke_without_command=True) diff --git a/docs/docker-stack/build-arg-ref.rst b/docs/docker-stack/build-arg-ref.rst index 46150aaf6c9fb..aee5d6b3e6598 100644 --- a/docs/docker-stack/build-arg-ref.rst +++ b/docs/docker-stack/build-arg-ref.rst @@ -51,6 +51,11 @@ Those are the most common arguments that you use when you want to build a custom +------------------------------------------+------------------------------------------+---------------------------------------------+ | ``AIRFLOW_UID`` | ``50000`` | Airflow user UID. | +------------------------------------------+------------------------------------------+---------------------------------------------+ +| ``AIRFLOW_CONSTRAINTS`` | ``constraints`` | Type of constraints to build the image. | +| | | This can be ``constraints`` for regular | +| | | images or ``constraints-no-providers`` for | +| | | slim images. | ++------------------------------------------+------------------------------------------+---------------------------------------------+ | ``AIRFLOW_CONSTRAINTS_REFERENCE`` | | Reference (branch or tag) from GitHub | | | | where constraints file is taken from | | | | It can be ``constraints-main`` or | diff --git a/docs/docker-stack/build.rst b/docs/docker-stack/build.rst index 808cbc202a746..115bd4d510062 100644 --- a/docs/docker-stack/build.rst +++ b/docs/docker-stack/build.rst @@ -23,8 +23,101 @@ Building the image Before you dive-deeply in the way how the Airflow Image is built, let us first explain why you might need to build the custom container image and we show a few typical ways you can do it. -Why custom image ? ------------------- +Quick start scenarios of image extending +---------------------------------------- + +The most common scenarios where you want to build your own image are adding a new ``apt`` package, +adding a new ``PyPI`` dependency and embedding DAGs into the image. +Example Dockerfiles for those scenarios are below, and you can read further +for more complex cases which might involve either extending or customizing the image. You will find +more information about more complex scenarios below, but if your goal is to quickly extend the Airflow +image with new provider, package, etc. then here is a quick start for you. + +Adding new ``apt`` package +.......................... + +The following example adds ``vim`` to the Airflow image. When adding packages via ``apt`` you should +switch to the ``root`` user when running the ``apt`` commands, but do not forget to switch back to the +``airflow`` user after installation is complete. + +.. exampleinclude:: docker-examples/extending/add-apt-packages/Dockerfile + :language: Dockerfile + :start-after: [START Dockerfile] + :end-before: [END Dockerfile] + + +Adding a new ``PyPI`` package +............................. + +The following example adds ``lxml`` python package from PyPI to the image. When adding packages via +``pip`` you need to use the ``airflow`` user rather than ``root``. Attempts to install ``pip`` packages +as ``root`` will fail with an appropriate error message. + +.. exampleinclude:: docker-examples/extending/add-pypi-packages/Dockerfile + :language: Dockerfile + :start-after: [START Dockerfile] + :end-before: [END Dockerfile] + +Embedding DAGs +.............. + +The following example adds ``test_dag.py`` to your image in the ``/opt/airflow/dags`` folder. + +.. exampleinclude:: docker-examples/extending/embedding-dags/Dockerfile + :language: Dockerfile + :start-after: [START Dockerfile] + :end-before: [END Dockerfile] + + +.. exampleinclude:: docker-examples/extending/embedding-dags/test_dag.py + :language: Python + :start-after: [START dag] + :end-before: [END dag] + + +Extending vs. customizing the image +----------------------------------- + +You might want to know very quickly whether you need to extend or customize the existing image +for Apache Airflow. This chapter gives you a short answer to those questions. + +Here is the comparison of the two approaches: + ++----------------------------------------------------+-----------+-------------+ +| | Extending | Customizing | ++====================================================+===========+=============+ +| Uses familiar 'FROM' pattern of image building | Yes | No | ++----------------------------------------------------+-----------+-------------+ +| Requires only basic knowledge about images | Yes | No | ++----------------------------------------------------+-----------+-------------+ +| Builds quickly | Yes | No | ++----------------------------------------------------+-----------+-------------+ +| Produces image heavily optimized for size | No | Yes | ++----------------------------------------------------+-----------+-------------+ +| Can build from custom airflow sources (forks) | No | Yes | ++----------------------------------------------------+-----------+-------------+ +| Can build on air-gaped system | No | Yes | ++----------------------------------------------------+-----------+-------------+ + +TL;DR; If you have a need to build custom image, it is easier to start with "Extending". However, if your +dependencies require compilation steps or when your require to build the image from security vetted +packages, switching to "Customizing" the image provides much more optimized images. For example, +if we compare equivalent images built by "Extending" and "Customization", they end up being +1.1GB and 874MB respectively - a 20% improvement in size for the Customized image. + +.. note:: + + You can also combine both - customizing & extending the image in one. You can build your + optimized base image first using ``customization`` method (for example by your admin team) with all + the heavy compilation required dependencies and you can publish it in your registry and let others + ``extend`` your image using ``FROM`` and add their own lightweight dependencies. This reflects well + the split where typically "Casual" users will Extend the image and "Power-users" will customize it. + +Airflow Summit 2020's `Production Docker Image `_ talk provides more +details about the context, architecture and customization/extension methods for the Production Image. + +Why customizing the image ? +--------------------------- The Apache Airflow community, releases Docker Images which are ``reference images`` for Apache Airflow. However, Airflow has more than 60 community managed providers (installable via extras) and some of the @@ -42,8 +135,8 @@ build and use your own image. You should only use installing dependencies dynami "hobbyist" and "quick start" scenarios when you want to iterate quickly to try things out and later replace it with your own images. -How to build your own image ---------------------------- +Building images primer +---------------------- .. note:: The ``Dockerfile`` does not strictly follow the `SemVer `_ approach of @@ -115,109 +208,62 @@ In the simplest case building your image consists of those steps: of storing and exposing the images, and it is most portable way of publishing the image. Both Docker-Compose and Kubernetes can make use of images exposed via registries. -The most common scenarios where you want to build your own image are adding a new ``apt`` package, -adding a new ``PyPI`` dependency and embedding DAGs into the image. -Example Dockerfiles for those scenarios are below, and you can read further -for more complex cases which might involve either extending or customizing the image. - -Adding new ``apt`` package -.......................... - -The following example adds ``vim`` to the airflow image. When adding packages via ``apt`` you should -switch to ``root`` user for the time of installation, but do not forget to switch back to the -``airflow`` user after installation is complete. - -.. exampleinclude:: docker-examples/extending/add-apt-packages/Dockerfile - :language: Dockerfile - :start-after: [START Dockerfile] - :end-before: [END Dockerfile] - - -Adding a new ``PyPI`` package -............................. - -The following example adds ``lxml`` python package from PyPI to the image. When adding packages via -``pip`` you need to use ``airflow`` user rather than ``root``. Attempts to install ``pip`` packages -with root, when you using typical ``pip install`` command will fail with appropriate error message. - -.. exampleinclude:: docker-examples/extending/add-pypi-packages/Dockerfile - :language: Dockerfile - :start-after: [START Dockerfile] - :end-before: [END Dockerfile] - -Embedding DAGs -.............. -The following example adds ``test_dag.py`` to your image in the ``/opt/airflow/dags`` folder. - -.. exampleinclude:: docker-examples/extending/embedding-dags/Dockerfile - :language: Dockerfile - :start-after: [START Dockerfile] - :end-before: [END Dockerfile] +Extending the image +------------------- +Extending the image is easiest if you just need to add some dependencies that do not require +compiling. The compilation framework of Linux (so called ``build-essential``) is pretty big, and +for the production images, size is really important factor to optimize for, so our Production Image +does not contain ``build-essential``. If you need a compiler like gcc or g++ or make/cmake etc. - those +are not found in the image and it is recommended that you follow the "customize" route instead. -.. exampleinclude:: docker-examples/extending/embedding-dags/test_dag.py - :language: Python - :start-after: [START dag] - :end-before: [END dag] +How to extend the image - it is something you are most likely familiar with - simply +build a new image using Dockerfile's ``FROM`` directive and add whatever you need. Then you can add your +Debian dependencies with ``apt`` or PyPI dependencies with ``pip install`` or any other stuff you need. -Extending vs. customizing the image ------------------------------------ +Base images +........... -You might want to know very quickly how you can extend or customize the existing image -for Apache Airflow. This chapter gives you a short answer to those questions. +There are two types of images you can extend your image from: +1) Regular Airflow image that contains the most common extras and providers, and all supported backend + database clients for AMD64 platform and Postgres for ARM64 platform. -Here is the comparison of the two types of building images. Here is your guide if you want to choose -how you want to build your image. +2) Slim Airflow image, which is a minimal image, contains all supported backends database clients installed + for AMD64 platform and Postgres for ARM64 platform, but contains no extras or providers, except + the 4 default providers. -+----------------------------------------------------+-----------+-------------+ -| | Extending | Customizing | -+====================================================+===========+=============+ -| Uses familiar 'FROM ' pattern of image building | Yes | No | -+----------------------------------------------------+-----------+-------------+ -| Requires only basic knowledge about images | Yes | No | -+----------------------------------------------------+-----------+-------------+ -| Builds quickly | Yes | No | -+----------------------------------------------------+-----------+-------------+ -| Produces image heavily optimized for size | No | Yes | -+----------------------------------------------------+-----------+-------------+ -| Can build from custom airflow sources (forks) | No | Yes | -+----------------------------------------------------+-----------+-------------+ -| Can build on air-gaped system | No | Yes | -+----------------------------------------------------+-----------+-------------+ +.. note:: Differences of slim image vs. regular image. -TL;DR; If you have a need to build custom image, it is easier to start with "Extending" however if your -dependencies require compilation step or when your require to build the image from security vetted -packages, switching to "Customizing" the image provides much more optimized images. In the example further -where we compare equivalent "Extending" and "Customizing" the image, similar images build by -Extending vs. Customization had shown 1.1GB vs 874MB image sizes respectively - with 20% improvement in -size of the Customized image. + The slim image is small comparing to regular image (~500 MB vs ~1.1GB) and you might need to add a + lot more packages and providers in order to make it useful for your case (but if you use only a + small subset of providers, it might be a good starting point for you). -.. note:: + The slim images might have dependencies in different versions than those used when providers are + preinstalled, simply because core Airflow might have less limits on the versions on its own. + When you install some providers they might require downgrading some dependencies if the providers + require different limits for the same dependencies. - You can also combine both - customizing & extending the image in one. You can build your - optimized base image first using ``customization`` method (for example by your admin team) with all - the heavy compilation required dependencies and you can publish it in your registry and let others - ``extend`` your image using ``FROM`` and add their own lightweight dependencies. This reflects well - the split where typically "Casual" users will Extend the image and "Power-users" will customize it. +Naming conventions for the images: -Airflow Summit 2020's `Production Docker Image `_ talk provides more -details about the context, architecture and customization/extension methods for the Production Image. ++----------------+------------------+---------------------------------+--------------------------------------+ +| Image | Python | Standard image | Slim image | ++================+==================+=================================+======================================+ +| Latest default | 3.7 | apache/airflow:latest | apache/airflow:slim-latest | +| Default | 3.7 | apache/airflow:X.Y.Z | apache/airflow:slim-X.Y.Z | +| Latest | 3.7,3.8,3.9,3.10 | apache/airflow:latest-pythonN.M | apache/airflow:slim-latest-pythonN.M | +| Specific | 3.7,3.8,3.9,3.10 | apache/airflow:X.Y.Z-pythonN.M | apache/airflow:slim-X.Y.Z-pythonN.M | ++----------------+------------------+---------------------------------+--------------------------------------+ +* The "latest" image is always the latest released stable version available. -Extending the image -------------------- +.. spelling:: -Extending the image is easiest if you just need to add some dependencies that do not require -compiling. The compilation framework of Linux (so called ``build-essential``) is pretty big, and -for the production images, size is really important factor to optimize for, so our Production Image -does not contain ``build-essential``. If you need compiler like gcc or g++ or make/cmake etc. - those -are not found in the image and it is recommended that you follow the "customize" route instead. + pythonN -How to extend the image - it is something you are most likely familiar with - simply -build a new image using Dockerfile's ``FROM`` directive and add whatever you need. Then you can add your -Debian dependencies with ``apt`` or PyPI dependencies with ``pip install`` or any other stuff you need. +Important notes for the base images +----------------------------------- You should be aware, about a few things: diff --git a/images/breeze/output-commands.svg b/images/breeze/output-commands.svg index e5e69be605f3b..b80cd59fb020e 100644 --- a/images/breeze/output-commands.svg +++ b/images/breeze/output-commands.svg @@ -1,4 +1,4 @@ - -
-
-
- +
+
+
+ -
Breeze commands
+
Breeze commands
-
+
Usage: breeze [OPTIONS] COMMAND [ARGS]...
@@ -193,6 +193,7 @@
prepare-provider-documentation Prepare CHANGELOG, README and COMMITS information for providers.
prepare-provider-packages Prepare sdist/whl packages of Airflow Providers.
prepare-airflow-package Prepare sdist/whl package of Airflow.
+
release-prod-images Release production images to DockerHub (needs DockerHub permissions).
generate-constraints Generates pinned constraint files with all extras from setup.py in parallel.
find-newer-dependencies Finds which dependencies are being upgraded.
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
diff --git a/images/breeze/output-release-prod-images.svg b/images/breeze/output-release-prod-images.svg new file mode 100644 index 0000000000000..ff21de350920f --- /dev/null +++ b/images/breeze/output-release-prod-images.svg @@ -0,0 +1,151 @@ + + + + +
+
+
+ + + + + +
Command: release-prod-images
+
+
+
+
Usage: breeze release-prod-images [OPTIONS]
+
+
Release production images to DockerHub (needs DockerHub permissions).
+
+
╭─ Release PROD IMAGE flags ───────────────────────────────────────────────────────────────────────────────────────────╮
+
* --airflow-version Airflow version to release (2.3.0, 2.3.0rc1 etc.) (TEXT) [required]
+
--dockerhub-repo DockerHub repository for the images (TEXT) [default: apache/airflow]
+
--slim-images Whether to prepare slim images instead of the regular ones.
+
--limit-python Specific python to build slim images for (if not specified - the images are built for all
+
available python versions)
+
(3.7 | 3.8 | 3.9 | 3.10)
+
--limit-platform Specific platform to build images for (if not specified, multiplatform images will be
+
built.
+
(linux/amd64 | linux/arm64 | linux/amd64,linux/arm64)
+
[default: linux/amd64,linux/arm64]
+
--skip-latest Whether to skip publishing the latest images (so that 'latest' images are not updated).
+
This should only be used if you release image for previous branches. Automatically set
+
when rc/alpha/beta images are built.
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+
--verbose -v Print verbose information about performed steps.
+
--dry-run -D If dry-run is set, commands are only printed, not executed.
+
--help -h Show this message and exit.
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+
+
+
+
+ +
+
diff --git a/setup.cfg b/setup.cfg index 770dfefa2086e..0b7a5764d4695 100644 --- a/setup.cfg +++ b/setup.cfg @@ -156,7 +156,7 @@ install_requires = python-dateutil>=2.3 python-nvd3>=0.15.0 python-slugify>=5.0 - rich>=12.3.0 + rich==12.3.0 setproctitle>=1.1.8 # SQL Alchemy 1.4.10 introduces a bug where for PyODBC driver UTCDateTime fields get wrongly converted # as string and fail to be converted back to datetime. It was supposed to be fixed in