This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 49853842124 Remove airflow info `--file-io` as `file.io` dropped
anonymous uploads (#73054)
49853842124 is described below
commit 49853842124734d5bed2d3dff0141d3bc3d361eb
Author: Kaxil Naik <[email protected]>
AuthorDate: Sun Sep 13 10:47:40 2026 +0100
Remove airflow info `--file-io` as `file.io` dropped anonymous uploads
(#73054)
* Remove airflow info --file-io as file.io dropped anonymous uploads
The flag uploaded the info report to file.io and printed a shareable link.
file.io no longer accepts anonymous uploads: the apex host 301s to a static
site that answers only GET/HEAD/OPTIONS, and uploads from the site itself
now
go through LimeWire's API behind an account. The documented anonymous POST
endpoint is not wired up any more, so the flag could not succeed.
It also failed badly rather than cleanly. The error body is HTML, so the
resp.json() call in the failure branch raised json.JSONDecodeError before
the
intended FileIoException was reached. tenacity only retried FileIoException
and
_send_report_to_fileio only caught it, so the command ended in an unhandled
traceback.
Nothing replaces it: sending the report to a third-party host that can
vanish
the same way is what broke here. Redirect the output to a file instead. Note
that --file-io implied --anonymize, so pass --anonymize explicitly when
sharing
a report.
Closes: #73045
* Add newsfragment and drop the console parameter left dead by the removal
The removal is user-visible CLI surface, so it needs a significant
newsfragment;
without one it files under Uncategorized and nobody upgrading sees it. The
fragment carries the anonymization note, which matters because --file-io
applied
--anonymize implicitly and plain `airflow info` prints connection passwords
in
cleartext.
AirflowInfo.show() took a console argument only so render_text could hand
it a
separate console. render_text is gone and nothing else ever passed it.
The flag test asserted only argparse's exit code, which is 2 for a typo'd
flag
and a missing subcommand alike, so it did not distinguish "flag removed"
from
"command removed". Pin the stderr message the way the pools --output test
does.
---
airflow-core/newsfragments/73054.significant.rst | 13 +++++
airflow-core/src/airflow/cli/cli_config.py | 4 --
.../src/airflow/cli/commands/info_command.py | 61 ++--------------------
.../tests/unit/cli/commands/test_info_command.py | 48 ++++-------------
4 files changed, 28 insertions(+), 98 deletions(-)
diff --git a/airflow-core/newsfragments/73054.significant.rst
b/airflow-core/newsfragments/73054.significant.rst
new file mode 100644
index 00000000000..44b4a0f3467
--- /dev/null
+++ b/airflow-core/newsfragments/73054.significant.rst
@@ -0,0 +1,13 @@
+Removed the ``--file-io`` option from the ``airflow info`` command
+
+``--file-io`` uploaded the report to file.io and printed a shareable link.
file.io no longer
+accepts anonymous uploads, so the option could not succeed; it also ended in
an unhandled
+traceback rather than reporting the failure.
+
+Redirect the output instead::
+
+ airflow info --anonymize > airflow-info.txt
+
+Note that ``--file-io`` applied ``--anonymize`` implicitly. Pass
``--anonymize`` explicitly when
+sharing a report: plain ``airflow info`` prints connection strings, including
passwords, in
+cleartext.
diff --git a/airflow-core/src/airflow/cli/cli_config.py
b/airflow-core/src/airflow/cli/cli_config.py
index 9ab2d84a331..241aa602a68 100644
--- a/airflow-core/src/airflow/cli/cli_config.py
+++ b/airflow-core/src/airflow/cli/cli_config.py
@@ -922,9 +922,6 @@ ARG_ANONYMIZE = Arg(
help="Minimize any personal identifiable information. Use it when sharing
output with others.",
action="store_true",
)
-ARG_FILE_IO = Arg(
- ("--file-io",), help="Send output to file.io service and returns link.",
action="store_true"
-)
# config
ARG_SECTION = Arg(
@@ -2327,7 +2324,6 @@ core_commands: list[CLICommand] = [
func=lazy_load_command("airflow.cli.commands.info_command.show_info"),
args=(
ARG_ANONYMIZE,
- ARG_FILE_IO,
ARG_VERBOSE,
ARG_OUTPUT,
),
diff --git a/airflow-core/src/airflow/cli/commands/info_command.py
b/airflow-core/src/airflow/cli/commands/info_command.py
index 79c4ae8b7f3..4ec3255ec6e 100644
--- a/airflow-core/src/airflow/cli/commands/info_command.py
+++ b/airflow-core/src/airflow/cli/commands/info_command.py
@@ -28,9 +28,6 @@ from enum import Enum
from typing import Protocol
from urllib.parse import urlsplit, urlunsplit
-import httpx
-import tenacity
-
from airflow import configuration
from airflow.cli.simple_table import AirflowConsole
from airflow.dag_processing.bundles.manager import DagBundlesManager
@@ -40,8 +37,6 @@ from airflow.utils.platform import getuser
from airflow.utils.providers_configuration_loader import
providers_configuration_loaded
from airflow.version import version as airflow_version
-log = logging.getLogger(__name__)
-
class Anonymizer(Protocol):
"""Anonymizer protocol."""
@@ -311,7 +306,7 @@ class AirflowInfo:
def _providers_info(self):
return [(p.data["package-name"], p.version) for p in
ProvidersManager().providers.values()]
- def show(self, output: str, console: AirflowConsole | None = None) -> None:
+ def show(self, output: str) -> None:
"""Show information about Airflow instance."""
all_info = {
"Apache Airflow": self._airflow_info,
@@ -321,7 +316,7 @@ class AirflowInfo:
"Providers info": self._providers_info,
}
- console = console or AirflowConsole(show_header=False)
+ console = AirflowConsole(show_header=False)
if output in ("table", "plain"):
# Show each info as table with key, value column
for key, info in all_info.items():
@@ -333,58 +328,10 @@ class AirflowInfo:
data=[{k.lower().replace(" ", "_"): dict(v)} for k, v in
all_info.items()], output=output
)
- def render_text(self, output: str) -> str:
- """Export the info to string."""
- # The text is uploaded as a file: no escape codes, fixed width
regardless of the terminal.
- console = AirflowConsole(color_system=None, width=200)
- with console.capture() as capture:
- self.show(output=output, console=console)
- return capture.get()
-
-
-class FileIoException(Exception):
- """Raises when error happens in FileIo.io integration."""
-
-
[email protected](
- stop=tenacity.stop_after_attempt(5),
- wait=tenacity.wait_exponential(multiplier=1, max=10),
- retry=tenacity.retry_if_exception_type(FileIoException),
- before=tenacity.before_log(log, logging.DEBUG),
- after=tenacity.after_log(log, logging.DEBUG),
-)
-def _upload_text_to_fileio(content):
- """Upload text file to File.io service and return link."""
- resp = httpx.post("https://file.io", content=content)
- if resp.status_code not in [200, 201]:
- print(resp.json())
- raise FileIoException("Failed to send report to file.io service.")
- try:
- return resp.json()["link"]
- except ValueError as e:
- log.debug(e)
- raise FileIoException("Failed to send report to file.io service.")
-
-
-def _send_report_to_fileio(info):
- print("Uploading report to file.io service.")
- try:
- link = _upload_text_to_fileio(str(info))
- print("Report uploaded.")
- print(link)
- print()
- except FileIoException as ex:
- print(str(ex))
-
@suppress_logs_and_warning
@providers_configuration_loaded
def show_info(args):
"""Show information related to Airflow, system and other."""
- # Enforce anonymization, when file_io upload is tuned on.
- anonymizer = PiiAnonymizer() if args.anonymize or args.file_io else
NullAnonymizer()
- info = AirflowInfo(anonymizer)
- if args.file_io:
- _send_report_to_fileio(info.render_text(args.output))
- else:
- info.show(args.output)
+ anonymizer = PiiAnonymizer() if args.anonymize else NullAnonymizer()
+ AirflowInfo(anonymizer).show(args.output)
diff --git a/airflow-core/tests/unit/cli/commands/test_info_command.py
b/airflow-core/tests/unit/cli/commands/test_info_command.py
index 2dc3d60e4e6..eff3501f6f4 100644
--- a/airflow-core/tests/unit/cli/commands/test_info_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_info_command.py
@@ -16,12 +16,12 @@
# under the License.
from __future__ import annotations
+import contextlib
import importlib
import logging
import os
-from unittest import mock
+from io import StringIO
-import httpx
import pytest
from airflow.cli import cli_parser
@@ -162,39 +162,13 @@ class TestAirflowInfo:
assert airflow_version in output
assert "postgresql+psycopg2://p...s:PASSWORD@postgres/airflow" in
output
- @mock.patch.dict(os.environ, {"FORCE_COLOR": "1", "TERM":
"xterm-256color"})
- def test_render_text_stays_plain_on_a_color_terminal(self):
- instance = info_command.AirflowInfo(info_command.NullAnonymizer())
-
- rendered = instance.render_text("table")
-
- assert airflow_version in rendered
- assert "\x1b[" not in rendered
-
-
[email protected]
-def setup_parser():
- return cli_parser.get_parser()
-
+ def test_file_io_flag_is_rejected(self):
+ # --file-io uploaded the report to file.io, which stopped accepting
anonymous
+ # uploads; the flag is gone rather than failing on every invocation.
Pin the
+ # message, not just the exit code: argparse exits 2 for a missing
subcommand too.
+ with contextlib.redirect_stderr(StringIO()) as stderr:
+ with pytest.raises(SystemExit) as exc_info:
+ self.parser.parse_args(["info", "--file-io"])
-class TestInfoCommandMockHttpx:
- @conf_vars(
- {
- ("database", "sql_alchemy_conn"):
"postgresql+psycopg2://postgres:airflow@postgres/airflow",
- }
- )
- def test_show_info_anonymize_fileio(self, setup_parser,
cleanup_providers_manager, stdout_capture):
- with mock.patch("airflow.cli.commands.info_command.httpx.post") as
post:
- post.return_value = httpx.Response(
- status_code=200,
- json={
- "success": True,
- "key": "f9U3zs3I",
- "link": "https://file.io/TEST",
- "expiry": "14 days",
- },
- )
- with stdout_capture as stdout:
- info_command.show_info(setup_parser.parse_args(["info",
"--file-io", "--anonymize"]))
- assert "https://file.io/TEST" in stdout.getvalue()
- assert airflow_version in post.call_args.kwargs["content"]
+ assert exc_info.value.code == 2
+ assert "unrecognized arguments: --file-io" in stderr.getvalue()