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 69b72e39c05 Multi-Team: Add teams inspect CLI command to display team
resources (#72036)
69b72e39c05 is described below
commit 69b72e39c050587826eca524c036dda09c1e2bab
Author: SameerMesiah97 <[email protected]>
AuthorDate: Tue Sep 22 23:45:57 2026 +0100
Multi-Team: Add teams inspect CLI command to display team resources (#72036)
* Add teams inspect CLI command
Adds a new command to display the resources
associated with a team. The command reports DAG bundle associations,
pools, connections, and variables for the requested team, providing a
single view of team-owned resources.
Also adds unit tests covering teams with associated resources, empty
teams, and nonexistent teams.
* Use AirflowConsole for team inspect output to format results consistently
across table, JSON, YAML, and plain output modes, add filtering coverage for
resources belonging to other teams, and update empty-team output tests.
* Remove mapper from console print and add multi-team check to teams
inspect command. Add unit test for multi-team disabled scenario.
---------
Co-authored-by: Sameer Mesiah <[email protected]>
---
airflow-core/newsfragments/72036.feature.rst | 1 +
airflow-core/src/airflow/cli/cli_config.py | 7 ++
.../src/airflow/cli/commands/team_command.py | 47 ++++++++
.../tests/unit/cli/commands/test_team_command.py | 123 +++++++++++++++++++++
4 files changed, 178 insertions(+)
diff --git a/airflow-core/newsfragments/72036.feature.rst
b/airflow-core/newsfragments/72036.feature.rst
new file mode 100644
index 00000000000..c28d6e32467
--- /dev/null
+++ b/airflow-core/newsfragments/72036.feature.rst
@@ -0,0 +1 @@
+Add an ``airflow teams inspect`` CLI command for displaying resources
associated with a team, including Dag bundles, pools, connections, and
variables.
diff --git a/airflow-core/src/airflow/cli/cli_config.py
b/airflow-core/src/airflow/cli/cli_config.py
index 234a812ba8d..e6f1a01975f 100644
--- a/airflow-core/src/airflow/cli/cli_config.py
+++ b/airflow-core/src/airflow/cli/cli_config.py
@@ -1692,6 +1692,13 @@ TEAMS_COMMANDS = (
func=lazy_load_command("airflow.cli.commands.team_command.team_verify"),
args=(ARG_VERBOSE,),
),
+ ActionCommand(
+ name="inspect",
+ help="Inspect a team",
+ description="Display resources associated with a team.\n",
+
func=lazy_load_command("airflow.cli.commands.team_command.team_inspect"),
+ args=(ARG_TEAM_NAME, ARG_OUTPUT, ARG_VERBOSE),
+ ),
)
STATE_STORE_COMMANDS = (
ActionCommand(
diff --git a/airflow-core/src/airflow/cli/commands/team_command.py
b/airflow-core/src/airflow/cli/commands/team_command.py
index bc9bbeb8075..5f739e475ce 100644
--- a/airflow-core/src/airflow/cli/commands/team_command.py
+++ b/airflow-core/src/airflow/cli/commands/team_command.py
@@ -303,3 +303,50 @@ def team_verify(args, *, session=NEW_SESSION):
raise SystemExit(1)
print("Verification succeeded.")
+
+
+@cli_utils.action_cli
+@providers_configuration_loaded
+@provide_session
+def team_inspect(args, *, session=NEW_SESSION):
+ """Inspect resources belonging to a team."""
+ if not conf.getboolean("core", "multi_team"):
+ print("Multi-team is not enabled.")
+ return
+
+ team_name = _extract_team_name(args)
+
+ team = session.scalar(select(Team).where(Team.name == team_name))
+ if team is None:
+ raise SystemExit(f"Team '{team_name}' does not exist")
+
+ bundle_names = session.scalars(
+ select(dag_bundle_team_association_table.c.dag_bundle_name)
+ .where(dag_bundle_team_association_table.c.team_name == team_name)
+ .order_by(dag_bundle_team_association_table.c.dag_bundle_name)
+ ).all()
+
+ pool_names = session.scalars(
+ select(Pool.pool).where(Pool.team_name ==
team_name).order_by(Pool.pool)
+ ).all()
+
+ connection_ids = session.scalars(
+ select(Connection.conn_id).where(Connection.team_name ==
team_name).order_by(Connection.conn_id)
+ ).all()
+
+ variable_keys = session.scalars(
+ select(Variable.key).where(Variable.team_name ==
team_name).order_by(Variable.key)
+ ).all()
+
+ AirflowConsole().print_as(
+ data=[
+ {
+ "name": team.name,
+ "dag_bundles": bundle_names,
+ "pools": pool_names,
+ "connections": connection_ids,
+ "variables": variable_keys,
+ }
+ ],
+ output=args.output,
+ )
diff --git a/airflow-core/tests/unit/cli/commands/test_team_command.py
b/airflow-core/tests/unit/cli/commands/test_team_command.py
index 90004fd9ae7..4f1889dd383 100644
--- a/airflow-core/tests/unit/cli/commands/test_team_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_team_command.py
@@ -628,3 +628,126 @@ class TestCliTeams:
team_command.team_verify(self.parser.parse_args(["teams",
"verify"]))
assert "references unknown team 'missing-team'" in stdout.getvalue()
+
+ def test_team_inspect(self, stdout_capture):
+ """Test inspecting a team with associated resources."""
+ self.session.add_all([Team(name="team1"), Team(name="team2")])
+ self.session.commit()
+
+ self.session.add_all(
+ [
+ DagBundleModel(name="bundle1"),
+ DagBundleModel(name="bundle2"),
+ ]
+ )
+ self.session.commit()
+
+ self.session.execute(
+ dag_bundle_team_association_table.insert(),
+ [
+ {"dag_bundle_name": "bundle1", "team_name": "team1"},
+ {"dag_bundle_name": "bundle2", "team_name": "team2"},
+ ],
+ )
+
+ self.session.add_all(
+ [
+ Pool(
+ pool=Pool.get_default_team_pool_name("team1"),
+ slots=128,
+ description="Default pool",
+ include_deferred=False,
+ team_name="team1",
+ ),
+ Connection(conn_id="conn1", conn_type="http",
team_name="team1"),
+ Variable(key="var1", val="value", team_name="team1"),
+ Pool(
+ pool="pool1",
+ slots=5,
+ description="Additional pool",
+ include_deferred=False,
+ team_name="team1",
+ ),
+ Pool(
+ pool=Pool.get_default_team_pool_name("team2"),
+ slots=128,
+ description="Default pool",
+ include_deferred=False,
+ team_name="team2",
+ ),
+ Connection(conn_id="conn2", conn_type="http",
team_name="team2"),
+ Variable(key="var2", val="value", team_name="team2"),
+ Pool(
+ pool="pool2",
+ slots=5,
+ description="Additional pool",
+ include_deferred=False,
+ team_name="team2",
+ ),
+ ]
+ )
+ self.session.commit()
+
+ with conf_vars({("core", "multi_team"): "True"}):
+ with stdout_capture as stdout:
+ team_command.team_inspect(
+ self.parser.parse_args(["teams", "inspect", "team1",
"--output", "json"])
+ )
+
+ assert json.loads(stdout.getvalue()) == [
+ {
+ "name": "team1",
+ "dag_bundles": ["bundle1"],
+ "pools": [
+ Pool.get_default_team_pool_name("team1"),
+ "pool1",
+ ],
+ "connections": ["conn1"],
+ "variables": ["var1"],
+ }
+ ]
+
+ def test_team_inspect_empty_team(self, stdout_capture):
+ """Test inspecting a team with no associated resources."""
+ self.session.add(Team(name="team1"))
+ self.session.commit()
+
+ self.session.add(
+ Pool(
+ pool=Pool.get_default_team_pool_name("team1"),
+ slots=128,
+ description="Default pool",
+ include_deferred=False,
+ team_name="team1",
+ )
+ )
+
+ self.session.commit()
+ with conf_vars({("core", "multi_team"): "True"}):
+ with stdout_capture as stdout:
+ team_command.team_inspect(
+ self.parser.parse_args(["teams", "inspect", "team1",
"--output", "json"])
+ )
+
+ assert json.loads(stdout.getvalue()) == [
+ {
+ "name": "team1",
+ "dag_bundles": [],
+ "pools": [Pool.get_default_team_pool_name("team1")],
+ "connections": [],
+ "variables": [],
+ }
+ ]
+
+ def test_team_inspect_nonexistent_team(self):
+ """Test inspecting a team that does not exist.""" #
+ with conf_vars({("core", "multi_team"): "True"}):
+ with pytest.raises(SystemExit, match="Team 'team1' does not
exist"):
+ team_command.team_inspect(self.parser.parse_args(["teams",
"inspect", "team1"]))
+
+ def test_team_inspect_multi_team_disabled(self, stdout_capture):
+ with conf_vars({("core", "multi_team"): "False"}):
+ with stdout_capture as stdout:
+ team_command.team_inspect(self.parser.parse_args(["teams",
"inspect", "team1"]))
+
+ assert "Multi-team is not enabled." in stdout.getvalue()