Skip to content

Commit f7779cb

Browse files
authored
Merge pull request #994 from GitGuardian/salomevoltz/refacto-json-output
fix(config_list): Add JSON output
2 parents 2265aa5 + def57dd commit f7779cb

File tree

9 files changed

+252
-67
lines changed

9 files changed

+252
-67
lines changed

.pre-commit-config.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
default_stages: [commit]
1+
default_stages: [pre-commit]
22
repos:
33
- repo: https://github.com/ambv/black
44
rev: 24.3.0
@@ -68,7 +68,7 @@ repos:
6868
name: GitGuardian Shield
6969
entry: pipenv run ggshield secret scan pre-commit
7070
language: system
71-
stages: [commit]
71+
stages: [pre-commit]
7272

7373
- repo: local
7474
hooks:
@@ -77,14 +77,14 @@ repos:
7777
entry: pipenv run ggshield secret scan pre-push
7878
language: system
7979
pass_filenames: false
80-
stages: [push]
80+
stages: [pre-push]
8181

8282
- repo: https://github.com/gitguardian/ggshield
8383
rev: v1.33.0
8484
hooks:
8585
- id: ggshield
8686
language_version: python3
87-
stages: [commit]
87+
stages: [pre-commit]
8888

8989
- repo: local
9090
hooks:
@@ -94,12 +94,12 @@ repos:
9494
language: system
9595
pass_filenames: false
9696
types: [python]
97-
stages: [commit]
97+
stages: [pre-commit]
9898

9999
- id: import-linter
100100
name: Import Linter
101101
entry: pipenv run lint-imports
102102
language: system
103103
pass_filenames: false
104104
types: [python]
105-
stages: [commit]
105+
stages: [pre-commit]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
### Added
2+
3+
- `ggshield config list` command now supports the `--json` option, allowing output in JSON format.
4+
5+
### Changed
6+
7+
- `ggshield api-status --json` now also outputs the instance URL.

doc/schemas/api-status.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"minimum": 200,
99
"description": "HTTP status code for the request"
1010
},
11+
"instance": {
12+
"type": "string",
13+
"format": "uri",
14+
"description": "URL of the GitGuardian instance"
15+
},
1116
"detail": {
1217
"type": "string",
1318
"description": "Human-readable version of the status"
@@ -21,5 +26,11 @@
2126
"description": "Version of the secrets engine"
2227
}
2328
},
24-
"required": ["status_code", "detail", "app_version", "secrets_engine_version"]
29+
"required": [
30+
"status_code",
31+
"instance",
32+
"detail",
33+
"app_version",
34+
"secrets_engine_version"
35+
]
2536
}

doc/schemas/config_list.json

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"title": "ggshield config list",
4+
"type": "object",
5+
"properties": {
6+
"instances": {
7+
"type": "array",
8+
"items": {
9+
"type": "object",
10+
"properties": {
11+
"instance_name": {
12+
"type": "string",
13+
"description": "Name of the GitGuardian instance"
14+
},
15+
"default_token_lifetime": {
16+
"type": ["string", "null"],
17+
"description": "Default token lifetime"
18+
},
19+
"workspace_id": {
20+
"type": ["number", "string"],
21+
"description": "Workspace ID"
22+
},
23+
"url": {
24+
"type": "string",
25+
"format": "uri",
26+
"description": "URL of the GitGuardian instance"
27+
},
28+
"token": {
29+
"type": "string",
30+
"description": "API Token for the instance"
31+
},
32+
"token_name": {
33+
"type": "string",
34+
"description": "Name of the token"
35+
},
36+
"expiry": {
37+
"type": "string",
38+
"description": "Expiration date of the token"
39+
}
40+
},
41+
"required": [
42+
"instance_name",
43+
"workspace_id",
44+
"url",
45+
"token",
46+
"token_name",
47+
"expiry"
48+
]
49+
}
50+
},
51+
"global_values": {
52+
"type": "object",
53+
"properties": {
54+
"instance": {
55+
"type": ["string", "null"],
56+
"description": "Name of the default GitGuardian instance"
57+
},
58+
"default_token_lifetime": {
59+
"type": ["string", "null"],
60+
"description": "Default token lifetime"
61+
}
62+
},
63+
"required": ["instance", "default_token_lifetime"]
64+
}
65+
},
66+
"required": ["instances", "global_values"]
67+
}

ggshield/cmd/config/config_list.py

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,112 @@
1-
from typing import Any, Tuple
1+
import json
2+
from dataclasses import dataclass, field
3+
from typing import Any, Dict, List, Optional
24

35
import click
46

5-
from ggshield.cmd.utils.common_options import add_common_options
7+
from ggshield.cmd.utils.common_options import (
8+
add_common_options,
9+
json_option,
10+
text_json_format_option,
11+
)
612
from ggshield.cmd.utils.context_obj import ContextObj
13+
from ggshield.core.config.auth_config import InstanceConfig
714

815
from .constants import DATETIME_FORMAT, FIELDS
916

1017

18+
@dataclass
19+
class InstanceInfo:
20+
instance_name: str
21+
default_token_lifetime: Optional[int]
22+
workspace_id: Any
23+
url: str
24+
token: str
25+
token_name: str
26+
expiry: str
27+
28+
29+
@dataclass
30+
class ConfigData:
31+
instances: List[InstanceInfo] = field(default_factory=list)
32+
global_values: Dict[str, Any] = field(default_factory=dict)
33+
34+
def as_dict(self) -> Dict[str, Any]:
35+
return {
36+
"instances": [instance.__dict__ for instance in self.instances],
37+
"global_values": self.global_values,
38+
}
39+
40+
41+
def get_instance_info(
42+
instance: InstanceConfig, default_token_lifetime: Any
43+
) -> InstanceInfo:
44+
"""Helper function to extract instance information."""
45+
instance_name = instance.name or instance.url
46+
account = instance.account
47+
48+
if account is not None:
49+
workspace_id = account.workspace_id
50+
token = account.token
51+
token_name = account.token_name
52+
expire_at = account.expire_at
53+
expiry = expire_at.strftime(DATETIME_FORMAT) if expire_at else "never"
54+
else:
55+
workspace_id = token = token_name = expiry = "not set"
56+
57+
_default_token_lifetime = instance.default_token_lifetime or default_token_lifetime
58+
59+
return InstanceInfo(
60+
instance_name=instance_name,
61+
default_token_lifetime=_default_token_lifetime,
62+
workspace_id=workspace_id,
63+
url=instance.url,
64+
token=token,
65+
token_name=token_name,
66+
expiry=expiry,
67+
)
68+
69+
1170
@click.command()
1271
@click.pass_context
72+
@json_option
73+
@text_json_format_option
1374
@add_common_options()
1475
def config_list_cmd(ctx: click.Context, **kwargs: Any) -> int:
1576
"""
1677
Print the list of configuration keys and values.
1778
"""
18-
config = ContextObj.get(ctx).config
79+
ctx_obj = ContextObj.get(ctx)
80+
config = ctx_obj.config
1981
default_token_lifetime = config.auth_config.default_token_lifetime
2082

21-
message_lines = []
22-
23-
def add_entries(*entries: Tuple[str, Any]):
24-
for key, value in entries:
25-
message_lines.append(f"{key}: {value}")
26-
27-
# List global values
28-
for field in FIELDS.values():
29-
config_obj = config.auth_config if field.auth_config else config.user_config
30-
value = getattr(config_obj, field.name)
31-
add_entries((field.name, value))
32-
message_lines.append("")
33-
34-
# List instance values
35-
for instance in config.auth_config.instances:
36-
instance_name = instance.name or instance.url
37-
38-
if instance.account is not None:
39-
workspace_id = instance.account.workspace_id
40-
token = instance.account.token
41-
token_name = instance.account.token_name
42-
expire_at = instance.account.expire_at
43-
expiry = (
44-
expire_at.strftime(DATETIME_FORMAT)
45-
if expire_at is not None
46-
else "never"
47-
)
48-
else:
49-
workspace_id = token = token_name = expiry = "not set"
50-
51-
_default_token_lifetime = (
52-
instance.default_token_lifetime
53-
if instance.default_token_lifetime is not None
54-
else default_token_lifetime
83+
config_data = ConfigData()
84+
for config_field in FIELDS.values():
85+
config_obj = (
86+
config.auth_config if config_field.auth_config else config.user_config
5587
)
88+
value = getattr(config_obj, config_field.name)
89+
config_data.global_values[config_field.name] = value
5690

57-
message_lines.append(f"[{instance_name}]")
58-
add_entries(
59-
("default_token_lifetime", _default_token_lifetime),
60-
("workspace_id", workspace_id),
61-
("url", instance.url),
62-
("token", token),
63-
("token_name", token_name),
64-
("expiry", expiry),
65-
)
91+
config_data.instances = [
92+
get_instance_info(instance, default_token_lifetime)
93+
for instance in config.auth_config.instances
94+
]
95+
96+
if ctx_obj.use_json:
97+
click.echo(json.dumps(config_data.as_dict()))
98+
else:
99+
message_lines = [
100+
f"{key}: {value}" for key, value in config_data.global_values.items()
101+
]
66102
message_lines.append("")
103+
for instance in config_data.instances:
104+
message_lines.append(f"[{instance.instance_name}]")
105+
for key, value in instance.__dict__.items():
106+
if key != "instance_name":
107+
message_lines.append(f"{key}: {value}")
108+
message_lines.append("")
109+
110+
click.echo("\n".join(message_lines).strip())
67111

68-
click.echo("\n".join(message_lines).strip())
69112
return 0

ggshield/cmd/status.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/python3
2+
import json
23
from typing import Any
34

45
import click
@@ -29,17 +30,18 @@ def status_cmd(ctx: click.Context, **kwargs: Any) -> int:
2930
if not isinstance(response, HealthCheckResponse):
3031
raise UnexpectedError("Unexpected health check response")
3132

32-
click.echo(
33-
response.to_json()
34-
if ctx_obj.use_json
35-
else (
33+
if ctx_obj.use_json:
34+
json_output = response.to_dict()
35+
json_output["instance"] = client.base_uri
36+
click.echo(json.dumps(json_output))
37+
else:
38+
click.echo(
3639
f"{format_text('API URL:', STYLE['key'])} {client.base_uri}\n"
3740
f"{format_text('Status:', STYLE['key'])} {format_healthcheck_status(response)}\n"
3841
f"{format_text('App version:', STYLE['key'])} {response.app_version or 'Unknown'}\n"
3942
f"{format_text('Secrets engine version:', STYLE['key'])} "
4043
f"{response.secrets_engine_version or 'Unknown'}\n"
4144
)
42-
)
4345

4446
return 0
4547

tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,12 @@ def api_status_json_schema() -> Dict[str, Any]:
318318
return _load_json_schema("api-status.json")
319319

320320

321+
@pytest.fixture(scope="session")
322+
def config_list_json_schema() -> Dict[str, Any]:
323+
"""Load the JSON schema for `config list` command."""
324+
return _load_json_schema("config_list.json")
325+
326+
321327
@pytest.fixture(scope="session")
322328
def sca_scan_all_json_schema() -> Dict[str, Any]:
323329
"""Load the JSON schema for `sca scan all` command."""

0 commit comments

Comments
 (0)