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()

Reply via email to