Skip to content

Restore CLI commands from custom executors and auth managers - #72858

Closed
FrankYang0529 wants to merge 2 commits into
apache:mainfrom
FrankYang0529:airflow-cli-custom-executor-auth-manager-commands
Closed

FrankYang0529 wants to merge 2 commits into
apache:mainfrom
FrankYang0529:airflow-cli-custom-executor-auth-manager-commands

Conversation

@FrankYang0529

Copy link
Copy Markdown
Member

Why

  • An executor or auth manager configured by module path, without being packaged as a provider, loses every CLI command it defines in get_cli_commands(). The commands are missing with no warning and no error.
  • Before the provider cli section was introduced (Introduce a "cli" section in provider metadata #59805), cli_parser looped over every configured executor and called the configured auth manager.
  • core-concepts/executor/index.rst and core-concepts/auth-manager/index.rst still document get_cli_commands(), and the auth manager page still shows [core] auth_manager = my_company.auth_managers.MyCustomAuthManager as the way to configure a custom class.

How

  • Build the set of class paths a provider cli section already covers, skip those, and send everything else through the fallback.
  • Skip classes shipped in airflow-core by module prefix, so the default configuration never imports them.
  • Give commands registered by a provider priority over the fallback. A subclass of a provider executor or auth manager inherits get_cli_commands(). Without this, it registers the same names a second time. cli_parser raises CliConflictError on a duplicate name and the CLI fails to start.

Verification

  • Unit test: uv run --project airflow-core pytest airflow-core/tests/unit/cli/test_cli_parser.py -q
  • Integration test:
  1. Setup
mkdir -p files/demo/my_company
touch files/demo/my_company/__init__.py

cat > files/demo/my_company/executors.py <<'EOF'
from airflow.cli.cli_config import ActionCommand, GroupCommand
from airflow.executors.base_executor import BaseExecutor
from airflow.providers.celery.executors.celery_executor import CeleryExecutor


def ping(args):
    print("pong from MyExecutor")


class MyExecutor(BaseExecutor):
    """Plain custom executor: implements BaseExecutor, not packaged as a provider."""

    @staticmethod
    def get_cli_commands():
        return [
            GroupCommand(
                name="my-executor",
                help="Manage MyExecutor",
                subcommands=[ActionCommand(name="ping", help="Ping", func=ping, args=())],
            )
        ]


class MyCeleryExecutor(CeleryExecutor):
    """Custom executor built on a provider executor: inherits CeleryExecutor.get_cli_commands()."""
EOF

cat > files/demo/my_company/auth_managers.py <<'EOF'
from airflow.api_fastapi.auth.managers.simple.simple_auth_manager import SimpleAuthManager
from airflow.cli.cli_config import ActionCommand, GroupCommand
from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager


def whoami(args):
    print("hello from MyCustomAuthManager")


class MyCustomAuthManager(SimpleAuthManager):
    """Plain custom auth manager, not packaged as a provider."""

    @staticmethod
    def get_cli_commands():
        return [
            GroupCommand(
                name="my-auth",
                help="Manage MyCustomAuthManager",
                subcommands=[ActionCommand(name="whoami", help="Who am I", func=whoami, args=())],
            )
        ]


class MyFabAuthManager(FabAuthManager):
    """Custom auth manager built on a provider auth manager: inherits FabAuthManager.get_cli_commands()."""
EOF
uv sync --frozen --project airflow-core
export AIRFLOW_HOME="$(pwd)/files/demo/home"
export AIRFLOW__CORE__LOAD_EXAMPLES=False
export PYTHONPATH="$(pwd)/files/demo"
  1. Check custom executor
AIRFLOW__CORE__EXECUTOR=my_company.executors.MyExecutor .venv/bin/airflow --help 2>/dev/null | grep -c my-executor
AIRFLOW__CORE__EXECUTOR=my_company.executors.MyExecutor .venv/bin/airflow my-executor ping

On main branch, the count is 0. On this branch, it shows 1 and pong from MyExecutor.

  1. Check custom auth manager
AIRFLOW__CORE__AUTH_MANAGER=my_company.auth_managers.MyCustomAuthManager .venv/bin/airflow --help 2>/dev/null | grep -c my-auth
AIRFLOW__CORE__AUTH_MANAGER=my_company.auth_managers.MyCustomAuthManager .venv/bin/airflow my-auth whoami

On main branch, the count is 0. On this branch, it shows 1 and hello from MyCustomAuthManager.

  1. Check classes inheriting from a provider
AIRFLOW__CORE__EXECUTOR=my_company.executors.MyCeleryExecutor .venv/bin/airflow --help 2>/dev/null | grep -cE '^\s+celery\s'
AIRFLOW__CORE__AUTH_MANAGER=my_company.auth_managers.MyFabAuthManager .venv/bin/airflow --help 2>/dev/null | grep -cE '^\s+users\s'

These two are regression guards rather than bug demos. On main they also print 1, because the fallback that imports the class and calls get_cli_commands() never runs there. What they prove is that re-opening that fallback does not start registering duplicate commands.


Was generative AI tooling used to co-author this PR?
  • Yes - Claude Code

  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@FrankYang0529
FrankYang0529 marked this pull request as ready for review September 10, 2026 04:33

@potiuk potiuk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the regression, and the reproduction in the description makes it easy to confirm. Adding _exclude_registered_commands so a subclass of a provider executor doesn't re-register the provider's own commands is the right instinct — I think it just needs to go one step further.

provider_command_names is a snapshot taken before the fallback runs (see inline on cli_parser.py:108). It dedupes against commands registered through a provider cli section, but not against commands the fallback loops register themselves. With a provider executor whose provider has no cli section — the population this fallback exists for — plus a custom subclass:

[core]
executor = airflow.providers.celery.executors.celery_executor.CeleryExecutor,my_company.executors.MyCeleryExecutor

executors_defined_cli is empty so neither is skipped, CeleryExecutor registers celery, and MyCeleryExecutor inherits get_cli_commands() and registers celery a second time against an empty provider_command_names. That hits the duplicate check at cli_parser.py:178 and raises CliConflictError at import time, so every airflow command fails. Before this PR the loop never reached the subclass, so it's newly reachable.

Making the set live inside the helper covers both loops:

    result: list[CLICommand] = []
    for command in commands:
        if command.name in registered_names:
            log.debug(
                "Skipping CLI command '%s' from %s: already registered by a provider.", command.name, source
            )
            continue
        registered_names.add(command.name)
        result.append(command)
    return result

Smaller observations

  • test_cli_parser.py:182 — swapping airflow.auth.managers for airflow.api_fastapi.auth.managers is a genuine fix (the old path no longer exists, so that param was passing vacuously). But replacing airflow.executors.executor_loader with airflow.executors.base_executor drops the guard #59805 added, because ExecutorLoader moved to a module-level import — and the docstring on line 185 still says the test covers executor_loader. For what it's worth there's no actual import-cost regression: cli_config already pulls executor_loader in via from airflow.jobs.job import JobState. Keeping that one import lazy where it was preserves the guard for the AIRFLOW_PACKAGE_NAME docs path at no cost; if dropping it is deliberate, please update the docstring and say why.
  • Worth a newsfragment — airflow-core/newsfragments/72858.bugfix.rst. Custom executors and auth managers silently losing their CLI commands is user-visible, which is what AGENTS.md asks for: "For airflow-core … user-facing changes, add a newsfragment."
  • cli_parser.py:135 and :188 — executors_defined_cli / auth_managers_defined_cli are exact complements of the *_not_defined_cli dicts built immediately above them, over the same *_without_check iterable. Deriving them from the dict already computed avoids the second pass and keeps the two from drifting.

Suggested additional reviewers

@dheerajturaga, @henry3260 (CODEOWNERS for airflow-core/src/airflow/cli/) and @vincbeck (CODEOWNERS for airflow-core/src/airflow/api_fastapi/auth/, for the auth-manager half).


This review was drafted by an AI-assisted tool and confirmed by an Airflow maintainer. The findings below are observations, not blockers; an Airflow maintainer — a real person — will take the next look at the PR. If you think a finding is mis-applied, please reply on the PR and a maintainer will weigh in.

More on how Airflow handles maintainer review: contributing-docs/05_pull_requests.rst.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

Comment thread airflow-core/src/airflow/cli/cli_parser.py Outdated
Comment thread airflow-core/tests/unit/cli/test_cli_parser.py
Comment thread airflow-core/src/airflow/cli/cli_parser.py
@potiuk

potiuk commented Sep 25, 2026

Copy link
Copy Markdown
Member

Hello @FrankYang0529 - thank you for your contributions to Apache Airflow!

The Airflow community has introduced a limit of 5 open pull requests at a time for contributors without write access to the repository. You currently have 30 open pull requests, so - as a one-time step of introducing the limit - we closed the ones where maintainers have not engaged yet:

These pull requests stay open because maintainers are already engaged in them - they count towards your limit:

This is not a judgement of you or of your changes. We never told contributors before that opening many pull requests at once was a problem, so there is nothing to feel bad about - and nothing is lost: your branches, commits and the review history stay where they are.

What we ask you to do is to make your first prioritization decision: choose which of the pull requests above matter most to you, and reopen them (up to 5 open at a time, including the ones still open) with the "Reopen pull request" button or gh pr reopen <PR_NUMBER> --repo apache/airflow. Reopen the ones you are ready to follow through - keep them rebased, respond to review comments and fix failing checks.

While your pull requests are waiting for review, the most valuable thing you can do is help in other ways - reviewing other contributors' pull requests, helping with issues, and taking part in the discussions on the devlist and Slack.

Why we introduced the limit, what it means for you and how to reopen or restore a pull request is explained in https://github.com/apache/airflow/blob/main/contributing-docs/32_open_pull_request_limit.rst.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

@FrankYang0529

Copy link
Copy Markdown
Member Author

@potiuk You had reviewed this before #72858 (review). Could you help me review this again when you have time? Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:CLI closed because of open PR limit Closed as a one-time step of introducing the open pull request limit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants