Skip to content

Commit 5f28683

Browse files
authored
Merge pull request #208 from opsmill/atg-20250103-ihs-55
Adds `infrahubctl info` command
2 parents dfd2816 + 25e99a4 commit 5f28683

File tree

10 files changed

+396
-8
lines changed

10 files changed

+396
-8
lines changed

changelog/109.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Adds `infrahubctl info` command to display information of the connectivity status of the SDK.

infrahub_sdk/client.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@
4646
)
4747
from .object_store import ObjectStore, ObjectStoreSync
4848
from .protocols_base import CoreNode, CoreNodeSync
49-
from .queries import get_commit_update_mutation
49+
from .queries import QUERY_USER, get_commit_update_mutation
5050
from .query_groups import InfrahubGroupContext, InfrahubGroupContextSync
5151
from .schema import InfrahubSchema, InfrahubSchemaSync, NodeSchemaAPI
5252
from .store import NodeStore, NodeStoreSync
5353
from .timestamp import Timestamp
5454
from .types import AsyncRequester, HTTPMethod, SyncRequester
55-
from .utils import decode_json, is_valid_uuid
55+
from .utils import decode_json, get_user_permissions, is_valid_uuid
5656

5757
if TYPE_CHECKING:
5858
from types import TracebackType
@@ -272,6 +272,22 @@ def _initialize(self) -> None:
272272
self._request_method: AsyncRequester = self.config.requester or self._default_request_method
273273
self.group_context = InfrahubGroupContext(self)
274274

275+
async def get_version(self) -> str:
276+
"""Return the Infrahub version."""
277+
response = await self.execute_graphql(query="query { InfrahubInfo { version }}")
278+
version = response.get("InfrahubInfo", {}).get("version", "")
279+
return version
280+
281+
async def get_user(self) -> dict:
282+
"""Return user information"""
283+
user_info = await self.execute_graphql(query=QUERY_USER)
284+
return user_info
285+
286+
async def get_user_permissions(self) -> dict:
287+
"""Return user permissions"""
288+
user_info = await self.get_user()
289+
return get_user_permissions(user_info["AccountProfile"]["member_of_groups"]["edges"])
290+
275291
@overload
276292
async def create(
277293
self,
@@ -1479,6 +1495,22 @@ def _initialize(self) -> None:
14791495
self._request_method: SyncRequester = self.config.sync_requester or self._default_request_method
14801496
self.group_context = InfrahubGroupContextSync(self)
14811497

1498+
def get_version(self) -> str:
1499+
"""Return the Infrahub version."""
1500+
response = self.execute_graphql(query="query { InfrahubInfo { version }}")
1501+
version = response.get("InfrahubInfo", {}).get("version", "")
1502+
return version
1503+
1504+
def get_user(self) -> dict:
1505+
"""Return user information"""
1506+
user_info = self.execute_graphql(query=QUERY_USER)
1507+
return user_info
1508+
1509+
def get_user_permissions(self) -> dict:
1510+
"""Return user permissions"""
1511+
user_info = self.get_user()
1512+
return get_user_permissions(user_info["AccountProfile"]["member_of_groups"]["edges"])
1513+
14821514
@overload
14831515
def create(
14841516
self,

infrahub_sdk/ctl/cli_commands.py

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import functools
55
import importlib
66
import logging
7+
import platform
78
import sys
89
from pathlib import Path
910
from typing import TYPE_CHECKING, Any, Callable
@@ -12,7 +13,11 @@
1213
import typer
1314
import ujson
1415
from rich.console import Console
16+
from rich.layout import Layout
1517
from rich.logging import RichHandler
18+
from rich.panel import Panel
19+
from rich.pretty import Pretty
20+
from rich.table import Table
1621
from rich.traceback import Traceback
1722

1823
from .. import __version__ as sdk_version
@@ -392,11 +397,106 @@ def protocols(
392397

393398
@app.command(name="version")
394399
@catch_exception(console=console)
395-
def version(_: str = CONFIG_PARAM) -> None:
396-
"""Display the version of Infrahub and the version of the Python SDK in use."""
400+
def version() -> None:
401+
"""Display the version of Python and the version of the Python SDK in use."""
397402

398-
client = initialize_client_sync()
399-
response = client.execute_graphql(query="query { InfrahubInfo { version }}")
403+
console.print(f"Python: {platform.python_version()}\nPython SDK: v{sdk_version}")
400404

401-
infrahub_version = response["InfrahubInfo"]["version"]
402-
console.print(f"Infrahub: v{infrahub_version}\nPython SDK: v{sdk_version}")
405+
406+
@app.command(name="info")
407+
@catch_exception(console=console)
408+
def info(detail: bool = typer.Option(False, help="Display detailed information."), _: str = CONFIG_PARAM) -> None: # noqa: PLR0915
409+
"""Display the status of the Python SDK."""
410+
411+
info: dict[str, Any] = {
412+
"error": None,
413+
"status": ":x:",
414+
"infrahub_version": "N/A",
415+
"user_info": {},
416+
"groups": {},
417+
}
418+
try:
419+
client = initialize_client_sync()
420+
info["infrahub_version"] = client.get_version()
421+
info["user_info"] = client.get_user()
422+
info["status"] = ":white_heavy_check_mark:"
423+
info["groups"] = client.get_user_permissions()
424+
except Exception as e:
425+
info["error"] = f"{e!s} ({e.__class__.__name__})"
426+
427+
if detail:
428+
layout = Layout()
429+
430+
# Layout structure
431+
new_console = Console(height=45)
432+
layout = Layout()
433+
layout.split_column(
434+
Layout(name="body", ratio=1),
435+
)
436+
layout["body"].split_row(
437+
Layout(name="left"),
438+
Layout(name="right"),
439+
)
440+
441+
layout["left"].split_column(
442+
Layout(name="connection_status", size=7),
443+
Layout(name="client_info", ratio=1),
444+
)
445+
446+
layout["right"].split_column(
447+
Layout(name="version_info", size=7),
448+
Layout(name="infrahub_info", ratio=1),
449+
)
450+
451+
# Connection status panel
452+
connection_status = Table(show_header=False, box=None)
453+
connection_status.add_row("Server Address:", client.config.address)
454+
connection_status.add_row("Status:", info["status"])
455+
if info["error"]:
456+
connection_status.add_row("Error Reason:", info["error"])
457+
layout["connection_status"].update(Panel(connection_status, title="Connection Status"))
458+
459+
# Version information panel
460+
version_info = Table(show_header=False, box=None)
461+
version_info.add_row("Python Version:", platform.python_version())
462+
version_info.add_row("Infrahub Version", info["infrahub_version"])
463+
version_info.add_row("Infrahub SDK:", sdk_version)
464+
layout["version_info"].update(Panel(version_info, title="Version Information"))
465+
466+
# SDK client configuration panel
467+
pretty_model = Pretty(client.config.model_dump(), expand_all=True)
468+
layout["client_info"].update(Panel(pretty_model, title="Client Info"))
469+
470+
# Infrahub information planel
471+
infrahub_info = Table(show_header=False, box=None)
472+
if info["user_info"]:
473+
infrahub_info.add_row("User:", info["user_info"]["AccountProfile"]["display_label"])
474+
infrahub_info.add_row("Description:", info["user_info"]["AccountProfile"]["description"]["value"])
475+
infrahub_info.add_row("Status:", info["user_info"]["AccountProfile"]["status"]["label"])
476+
infrahub_info.add_row(
477+
"Number of Groups:", str(info["user_info"]["AccountProfile"]["member_of_groups"]["count"])
478+
)
479+
480+
if groups := info["groups"]:
481+
infrahub_info.add_row("Groups:", "")
482+
for group, roles in groups.items():
483+
infrahub_info.add_row("", group, ", ".join(roles))
484+
485+
layout["infrahub_info"].update(Panel(infrahub_info, title="Infrahub Info"))
486+
487+
new_console.print(layout)
488+
else:
489+
# Simple output
490+
table = Table(show_header=False, box=None)
491+
table.add_row("Address:", client.config.address)
492+
table.add_row("Connection Status:", info["status"])
493+
if info["error"]:
494+
table.add_row("Connection Error:", info["error"])
495+
496+
table.add_row("Python Version:", platform.python_version())
497+
table.add_row("SDK Version:", sdk_version)
498+
table.add_row("Infrahub Version:", info["infrahub_version"])
499+
if account := info["user_info"].get("AccountProfile"):
500+
table.add_row("User:", account["display_label"])
501+
502+
console.print(table)

infrahub_sdk/queries.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,72 @@ def get_commit_update_mutation(is_read_only: bool = False) -> str:
4242
}
4343
}
4444
"""
45+
46+
QUERY_USER = """
47+
query GET_PROFILE_DETAILS {
48+
AccountProfile {
49+
id
50+
display_label
51+
account_type {
52+
value
53+
__typename
54+
updated_at
55+
}
56+
status {
57+
label
58+
value
59+
updated_at
60+
__typename
61+
}
62+
description {
63+
value
64+
updated_at
65+
__typename
66+
}
67+
label {
68+
value
69+
updated_at
70+
__typename
71+
}
72+
member_of_groups {
73+
count
74+
edges {
75+
node {
76+
display_label
77+
group_type {
78+
value
79+
}
80+
... on CoreAccountGroup {
81+
id
82+
roles {
83+
count
84+
edges {
85+
node {
86+
permissions {
87+
count
88+
edges {
89+
node {
90+
display_label
91+
identifier {
92+
value
93+
}
94+
}
95+
}
96+
}
97+
}
98+
}
99+
}
100+
display_label
101+
}
102+
}
103+
}
104+
}
105+
__typename
106+
name {
107+
value
108+
updated_at
109+
__typename
110+
}
111+
}
112+
}
113+
"""

infrahub_sdk/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,20 @@ def write_to_file(path: Path, value: Any) -> bool:
335335
written = path.write_text(to_write)
336336

337337
return written is not None
338+
339+
340+
def get_user_permissions(data: list[dict]) -> dict:
341+
groups = {}
342+
for group in data:
343+
group_name = group["node"]["display_label"]
344+
permissions = []
345+
346+
roles = group["node"].get("roles", {}).get("edges", [])
347+
for role in roles:
348+
role_permissions = role["node"].get("permissions", {}).get("edges", [])
349+
for permission in role_permissions:
350+
permissions.append(permission["node"]["identifier"]["value"])
351+
352+
groups[group_name] = permissions
353+
354+
return groups
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"data": {
3+
"AccountProfile": {
4+
"id": "1816ebcd-cea7-3bf7-3fc9-c51282f03fe7",
5+
"display_label": "Admin",
6+
"account_type": {
7+
"value": "User",
8+
"__typename": "TextAttribute",
9+
"updated_at": "2025-01-02T16:06:15.565985+00:00"
10+
},
11+
"status": {
12+
"label": "Active",
13+
"value": "active",
14+
"updated_at": "2025-01-02T16:06:15.565985+00:00",
15+
"__typename": "Dropdown"
16+
},
17+
"description": {
18+
"value": null,
19+
"updated_at": "2025-01-02T16:06:15.565985+00:00",
20+
"__typename": "TextAttribute"
21+
},
22+
"label": {
23+
"value": "Admin",
24+
"updated_at": "2025-01-02T16:06:15.565985+00:00",
25+
"__typename": "TextAttribute"
26+
},
27+
"member_of_groups": {
28+
"count": 1,
29+
"edges": [
30+
{
31+
"node": {
32+
"display_label": "Super Administrators",
33+
"group_type": {
34+
"value": "default"
35+
},
36+
"id": "1816ebce-1cbe-2e96-3fc3-c5124c324bac",
37+
"roles": {
38+
"count": 1,
39+
"edges": [
40+
{
41+
"node": {
42+
"permissions": {
43+
"count": 2,
44+
"edges": [
45+
{
46+
"node": {
47+
"display_label": "super_admin 6",
48+
"identifier": {
49+
"value": "global:super_admin:allow_all"
50+
}
51+
}
52+
},
53+
{
54+
"node": {
55+
"display_label": "* * any 6",
56+
"identifier": {
57+
"value": "object:*:*:any:allow_all"
58+
}
59+
}
60+
}
61+
]
62+
}
63+
}
64+
}
65+
]
66+
}
67+
}
68+
}
69+
]
70+
},
71+
"__typename": "CoreAccount",
72+
"name": {
73+
"value": "admin",
74+
"updated_at": "2025-01-02T16:06:15.565985+00:00",
75+
"__typename": "TextAttribute"
76+
}
77+
}
78+
}
79+
}

tests/unit/ctl/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
22
from pytest_httpx import HTTPXMock
33

4+
from tests.unit.sdk.conftest import mock_query_infrahub_user, mock_query_infrahub_version # noqa: F401
5+
46

57
@pytest.fixture
68
async def mock_branches_list_query(httpx_mock: HTTPXMock) -> HTTPXMock:

0 commit comments

Comments
 (0)