automated-actions-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,4 @@
1
+ from automated_actions_cli.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,198 @@
1
+ import atexit
2
+ import contextlib
3
+ import importlib
4
+ import logging
5
+ import os
6
+ import sys
7
+ from http.cookiejar import MozillaCookieJar
8
+ from importlib.metadata import version
9
+ from pathlib import Path
10
+ from typing import Annotated, Any
11
+
12
+ import httpx
13
+ import typer
14
+ from automated_actions_client import AuthenticatedClient, Client
15
+ from automated_actions_client.api.v1.me import sync as api_v1_me
16
+ from httpx_gssapi import OPTIONAL, HTTPSPNEGOAuth
17
+ from rich import print as rich_print
18
+ from rich.console import Console
19
+
20
+ from automated_actions_cli.config import config
21
+ from automated_actions_cli.formatter import JsonFormatter, OutputFormat, YamlFormatter
22
+ from automated_actions_cli.utils import (
23
+ blend_text,
24
+ kerberos_available,
25
+ kinit,
26
+ progress_spinner,
27
+ )
28
+
29
+ app = typer.Typer(
30
+ pretty_exceptions_show_locals=False,
31
+ rich_markup_mode="rich",
32
+ epilog="Made with [red]:heart:[/red] by [blue]AppSRE[/blue]",
33
+ )
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ console = Console(record=True, soft_wrap=True)
38
+
39
+ BANNER = """
40
+ [o_o]
41
+ <) )╯
42
+ | | |
43
+ (_|_)
44
+ -------------------
45
+ AUTOMATED ACTIONS
46
+ -------------------
47
+ """
48
+
49
+
50
+ def version_callback(*, value: bool) -> None:
51
+ if value:
52
+ rich_print(f"Version: {version('automated-actions-cli')}")
53
+ raise typer.Exit
54
+
55
+
56
+ class ClientWithCookieJar(Client):
57
+ def get_httpx_client(self) -> httpx.Client:
58
+ """Get the underlying httpx.Client, constructing a new one if not previously set"""
59
+ if self._client is None:
60
+ self._cookiejar = MozillaCookieJar(filename=config.cookies_file)
61
+ with contextlib.suppress(FileNotFoundError):
62
+ self._cookiejar.load()
63
+
64
+ self._client = httpx.Client(
65
+ base_url=self._base_url,
66
+ cookies=self._cookiejar,
67
+ headers=self._headers,
68
+ timeout=self._timeout,
69
+ verify=self._verify_ssl,
70
+ follow_redirects=self._follow_redirects,
71
+ **self._httpx_args,
72
+ )
73
+ return self._client
74
+
75
+ def __exit__(self, *args: object, **kwargs: Any) -> None:
76
+ # persist cookies
77
+ self._cookiejar.save()
78
+ super().__exit__(*args, **kwargs)
79
+ self._client = None
80
+
81
+
82
+ @app.callback(no_args_is_help=True)
83
+ def main(
84
+ ctx: typer.Context,
85
+ *,
86
+ url: Annotated[
87
+ str, typer.Option(help="Automated Action Server URL", envvar="AA_URL")
88
+ ] = "https://automated-actions.devshift.net",
89
+ debug: Annotated[
90
+ bool, typer.Option(help="Enable debug", envvar="AA_DEBUG")
91
+ ] = False,
92
+ screen_capture_file: Annotated[
93
+ Path | None,
94
+ typer.Option(
95
+ help="Capture screen recording as SVG",
96
+ writable=True,
97
+ envvar="AA_SCREEN_CAPTURE_FILE",
98
+ ),
99
+ ] = None,
100
+ version: Annotated[ # noqa: ARG001
101
+ bool | None, typer.Option(callback=version_callback, help="Display version")
102
+ ] = None,
103
+ quiet: Annotated[
104
+ bool, typer.Option(help="Don't print anything", envvar="AA_QUIET")
105
+ ] = False,
106
+ output: Annotated[
107
+ OutputFormat, typer.Option(help="Output format", envvar="AA_OUTPUT")
108
+ ] = OutputFormat.yaml,
109
+ color: Annotated[
110
+ bool, typer.Option(help="Use colored output", envvar="AA_COLOR")
111
+ ] = True,
112
+ ) -> None:
113
+ if "--help" in sys.argv:
114
+ rich_print(
115
+ blend_text(BANNER, (32, 32, 255), (255, 32, 255)),
116
+ )
117
+ # do not initialize the client and everything else if --help is passed
118
+ return
119
+
120
+ progress = None
121
+ if not quiet and not screen_capture_file:
122
+ progress = progress_spinner(console=console)
123
+ progress.start()
124
+ progress.add_task(description="Processing...", total=None)
125
+ atexit.register(progress.stop)
126
+
127
+ logging.basicConfig(
128
+ level="DEBUG" if debug else "INFO",
129
+ format="%(name)-20s: %(message)s",
130
+ )
131
+ logging.getLogger("httpx").setLevel(logging.WARNING)
132
+
133
+ if token := os.environ.get("AA_TOKEN"):
134
+ ctx.obj = {
135
+ "client": AuthenticatedClient(
136
+ base_url=str(url),
137
+ token=token,
138
+ raise_on_unexpected_status=True,
139
+ follow_redirects=True,
140
+ )
141
+ }
142
+
143
+ elif kerberos_available():
144
+ if progress:
145
+ progress.stop()
146
+ kinit()
147
+ if progress:
148
+ progress.start()
149
+
150
+ ctx.obj = {
151
+ "client": ClientWithCookieJar(
152
+ base_url=str(url),
153
+ raise_on_unexpected_status=True,
154
+ follow_redirects=True,
155
+ httpx_args={
156
+ "auth": HTTPSPNEGOAuth(mutual_authentication=OPTIONAL),
157
+ },
158
+ )
159
+ }
160
+ else:
161
+ logger.error(
162
+ "No bearer token or Kerberos authentication available. Please set AA_TOKEN or install and configure Kerberos."
163
+ )
164
+ raise typer.Exit(1)
165
+
166
+ printer = console.print if color else print
167
+ match output:
168
+ case OutputFormat.json:
169
+ ctx.obj["formatter"] = JsonFormatter(printer=printer)
170
+ case OutputFormat.yaml:
171
+ ctx.obj["formatter"] = YamlFormatter(printer=printer)
172
+ case _:
173
+ raise ValueError("Invalid output format")
174
+
175
+ # enforce the user to login
176
+ api_v1_me(client=ctx.obj["client"])
177
+
178
+ if screen_capture_file is not None:
179
+ screen_capture_file = screen_capture_file.with_suffix(".svg")
180
+ rich_print(f"Screen recording: {screen_capture_file}")
181
+ # strip $0 and screen_capture_file option
182
+ args = sys.argv[3:]
183
+ console.print(f"$ automated-actions {' '.join(args)}")
184
+ # title = command sub_command
185
+ title = " ".join(args[0:2])
186
+ atexit.register(console.save_svg, str(screen_capture_file), title=title)
187
+
188
+
189
+ def initialize_client_actions() -> None:
190
+ """Initialize typer commands from all available automated-actions-client actions."""
191
+ for action in dir(importlib.import_module("automated_actions_client.api.v1")):
192
+ if not action.startswith("_"):
193
+ app.add_typer(
194
+ importlib.import_module(f"automated_actions_client.api.v1.{action}").app
195
+ )
196
+
197
+
198
+ initialize_client_actions()
@@ -0,0 +1,20 @@
1
+ from pathlib import Path
2
+
3
+ from appdirs import AppDirs
4
+ from pydantic_settings import BaseSettings
5
+
6
+
7
+ class Config(BaseSettings):
8
+ # pydantic config
9
+ model_config = {"env_prefix": "aa_"}
10
+
11
+ appdirs: AppDirs = AppDirs("automated-actions", "app-sre")
12
+
13
+ @property
14
+ def cookies_file(self) -> Path:
15
+ user_cache_dir = Path(self.appdirs.user_cache_dir)
16
+ user_cache_dir.mkdir(parents=True, exist_ok=True)
17
+ return user_cache_dir / "cookies.txt"
18
+
19
+
20
+ config = Config()
@@ -0,0 +1,41 @@
1
+ import json
2
+ from collections.abc import Callable
3
+ from enum import StrEnum
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+
9
+ class OutputFormat(StrEnum):
10
+ """Output format for the command line interface."""
11
+
12
+ json = "json"
13
+ yaml = "yaml"
14
+
15
+
16
+ class JsonFormatter:
17
+ def __init__(self, printer: Callable[[str], None], indent: int = 4) -> None:
18
+ self._printer = printer
19
+ self._indent = indent
20
+
21
+ def __call__(self, data: Any) -> None:
22
+ """Format the data as JSON."""
23
+ self._printer(json.dumps(data, indent=self._indent, sort_keys=True))
24
+
25
+
26
+ class YamlFormatter:
27
+ def __init__(self, printer: Callable[[str], None], indent: int = 2) -> None:
28
+ self._printer = printer
29
+ self._indent = indent
30
+
31
+ def __call__(self, data: Any) -> None:
32
+ """Format the data as yaml."""
33
+ self._printer(
34
+ yaml.dump(
35
+ data,
36
+ indent=self._indent,
37
+ sort_keys=True,
38
+ explicit_start=True,
39
+ default_flow_style=False,
40
+ )
41
+ )
@@ -0,0 +1,51 @@
1
+ import logging
2
+ import shutil
3
+ import subprocess
4
+
5
+ from rich.console import Console
6
+ from rich.progress import Progress, SpinnerColumn, TextColumn
7
+ from rich.text import Text
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def blend_text(
13
+ message: str, color1: tuple[int, int, int], color2: tuple[int, int, int]
14
+ ) -> Text:
15
+ """Blend text from one color to another."""
16
+ text = Text(message)
17
+ r1, g1, b1 = color1
18
+ r2, g2, b2 = color2
19
+ dr = r2 - r1
20
+ dg = g2 - g1
21
+ db = b2 - b1
22
+ size = len(text)
23
+ for index in range(size):
24
+ blend = index / size
25
+ color = f"#{int(r1 + dr * blend):2X}{int(g1 + dg * blend):2X}{int(b1 + db * blend):2X}"
26
+ text.stylize(color, index, index + 1)
27
+ return text
28
+
29
+
30
+ def progress_spinner(console: Console) -> Progress:
31
+ """Display shiny progress spinner."""
32
+ return Progress(
33
+ SpinnerColumn(),
34
+ TextColumn("[progress.description]{task.description}"),
35
+ console=console,
36
+ transient=True,
37
+ )
38
+
39
+
40
+ def kerberos_available() -> bool:
41
+ return bool(shutil.which("kinit"))
42
+
43
+
44
+ def kinit() -> None:
45
+ """Acquire a kerberos ticket if needed."""
46
+ try:
47
+ # Check if the kerberos ticket is valid
48
+ subprocess.run(["klist", "-s"], check=True, capture_output=True)
49
+ except subprocess.CalledProcessError:
50
+ # If the ticket is not valid, acquire a new one
51
+ subprocess.run(["kinit"], check=True, capture_output=False)
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: automated-actions-cli
3
+ Version: 0.1.0
4
+ Summary: Automated Actions Client
5
+ Project-URL: homepage, https://github.com/app-sre/automated-actions
6
+ Project-URL: repository, https://github.com/app-sre/automated-actions
7
+ Project-URL: documentation, https://github.com/app-sre/automated-actions
8
+ Author-email: AppSRE <sd-app-sre@redhat.com>
9
+ License: Apache 2.0
10
+ Requires-Python: ~=3.12.0
11
+ Requires-Dist: appdirs==1.4.4
12
+ Requires-Dist: automated-actions-client
13
+ Requires-Dist: httpx-gssapi==0.4
14
+ Requires-Dist: pydantic-settings==2.9.1
15
+ Requires-Dist: pyyaml==6.0.2
16
+ Requires-Dist: rich==14.0.0
17
+ Requires-Dist: typer==0.16.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # `automated_actions_cli` Package 💻🚀
21
+
22
+ Welcome, developer, to the `automated_actions_cli` package! This is the command-line interface (CLI) for interacting with the Automated Actions system. It allows users (tenants, SREs) to trigger actions, check their status, and manage other aspects of the system directly from their terminal.
23
+
24
+ ## 🎯 Overview
25
+
26
+ The `automated_actions_cli` provides a user-friendly way to:
27
+
28
+ * List available automated actions.
29
+ * Trigger predefined actions with necessary parameters.
30
+ * Query the status and results of submitted actions.
31
+ * Perform administrative tasks (e.g., token generation for service accounts, if applicable).
32
+
33
+ ## ✨ Key Technologies
34
+
35
+ * **[Typer](https://typer.tiangolo.com/):** The CLI is built using Typer, which makes it easy to create modern, user-friendly command-line applications with excellent support for type hints, auto-completion, and help generation.
36
+ * **`automated_actions_client`:** This CLI heavily relies on the `automated_actions_client` package. The client package provides the actual Python functions and Pydantic models for making HTTP requests to the `automated_actions` API server.
37
+
38
+ ## ⚙️ Core Functionality
39
+
40
+ The CLI acts as a wrapper around the `automated_actions_client`. When a user executes a CLI command:
41
+
42
+ 1. Typer parses the command, subcommands, arguments, and options.
43
+ 2. Authentication is handled (see [Authentication](#-authentication) section below).
44
+ 3. The appropriate function from `automated_actions_client` is called with the user-provided parameters.
45
+ 4. The response from the API (via the client) is then processed and presented to the user in a human-readable format.
46
+
47
+ Commands are typically structured in a hierarchical way, for example:
48
+ `automated-actions <command> [arguments_and_options]`
49
+
50
+ ## 🔑 Authentication (Kerberos)
51
+
52
+ Authentication with the `automated_actions` API server is primarily handled via Red Hat SSO. Users are expected to have a valid Kerberos ticket-granting ticket (TGT) before using the CLI. This is typically obtained by running `kinit` and available on all Red Hat managed systems.
53
+
54
+ The CLI does execute `kinit` directly if the user has not already obtained a ticket. This is done using the `kinit` command, which prompts the user for their Kerberos password.
55
+
56
+ From the user's perspective, if they have a valid Kerberos ticket, authentication should be seamless. The CLI will automatically use the ticket to authenticate API requests. If no ticket is present or it's invalid, API calls will likely fail with an authentication error.
57
+
58
+ ## 💡 Usage Examples
59
+
60
+ **1. Listing all user available action:**
61
+
62
+ ```bash
63
+ $ automated-actions me
64
+ ---
65
+ allowed_actions:
66
+ - action-cancel
67
+ - action-detail
68
+ - action-list
69
+ - create-token
70
+ - me
71
+ - openshift-workload-restart-unthrottled
72
+ created_at: 1747919185.209177
73
+ email: your-email@address.com
74
+ name: Your Name
75
+ updated_at: 1747996842.377609
76
+ username: your-username
77
+ ```
78
+
79
+ **2. List all past actions:**
80
+
81
+ ```bash
82
+ $ automated-actions action-list
83
+ ---
84
+ - action_id: b8ff9963-516f-437a-b93c-b7354cd5225d
85
+ created_at: 1747919261.038378
86
+ name: openshift-workload-restart
87
+ owner: your-username
88
+ result: ok
89
+ status: SUCCESS
90
+ task_args:
91
+ cluster: cluster-1
92
+ kind: Pod
93
+ name: example-74d78dbfbf-89rl2
94
+ namespace: namespace-dev
95
+ updated_at: 1747919261.962519
96
+ ...
97
+ ```
98
+
99
+ **2. Triggering an action (e.g., restarting an OpenShift deployment):**
100
+
101
+ ```bash
102
+ $ automated-actions openshift-workload-restart --cluster "my-cluster" --namespace "my-namespace" --kind Deployment --name "my-app"
103
+ ---
104
+ - action_id: b8ff9963-516f-437a-b93c-b7354cd5225d
105
+ created_at: 1747919261.038378
106
+ name: openshift-workload-restart
107
+ owner: your-username
108
+ result: ok
109
+ status: PENDING
110
+ task_args:
111
+ cluster: my-cluster
112
+ kind: Deployment
113
+ name: my-app
114
+ namespace: my-namespac
115
+ updated_at: 1747919261.962519
116
+ ```
117
+
118
+ ## 🧑‍💻 Development
119
+
120
+ See the main project `README.md` for general development instructions.
121
+
122
+ ### Running the CLI Locally
123
+
124
+ During development, you can invoke the CLI directly using `automated-actions`:
125
+
126
+ ```bash
127
+ automated-actions --help
128
+ ```
129
+
130
+ ### Relationship with `automated_actions_client`
131
+
132
+ * **Generated Code:** Many CLI commands in this package might be **auto-generated** based on the OpenAPI specification, similar to how `automated_actions_client` is generated. This is often done using custom templates with `openapi-python-client` that output Typer application structures.
133
+ * **Manual Wrappers:** Alternatively, or in addition, there might be manually written Typer commands in this package that import and use functions and models from `automated_actions_client`.
134
+ * **Updates:** If the `automated_actions_client` is regenerated due to API changes, the CLI commands (especially auto-generated ones) might also need to be regenerated or updated to reflect those changes. The `make generate-client` (or a similar target like `make generate-cli`) from the project root should handle this.
135
+
136
+ ### Testing
137
+
138
+ Tests for this package are located within its `tests/` directory. These typically involve:
139
+
140
+ * Using Typer's `CliRunner` to invoke CLI commands and assert their output and exit codes.
141
+ * Mocking the `automated_actions_client` to avoid making real API calls during unit tests.
142
+ * Integration tests that do make real API calls to a test instance of the server.
143
+
144
+ To run tests specifically for this package:
145
+
146
+ ```bash
147
+ make test
148
+ ```
149
+
150
+ ## 🤝 Contributing to this Package
151
+
152
+ * Follow the general contributing guidelines in the main project `README.md`.
153
+ * Ensure new CLI commands are intuitive and provide helpful error messages.
154
+ * Add or update tests for any new or modified commands.
155
+ * Keep Typer's auto-completion features in mind for a good user experience.
@@ -0,0 +1,10 @@
1
+ automated_actions_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ automated_actions_cli/__main__.py,sha256=bAFyKcKeh2Z8JbQa1vp7_iEn9YeSKgFOnpaBigMnNbk,80
3
+ automated_actions_cli/cli.py,sha256=Y5RGwlAGVFfJowIN9gwPR4yvDRA7zCbbh5PmEMP_3HA,6178
4
+ automated_actions_cli/config.py,sha256=72RB43l4-xGJod6omBSxp2NuTXbfUU36v0NeQ8bcJBo,488
5
+ automated_actions_cli/formatter.py,sha256=zc8Botw86QUvQjMQG7drhAE0hJ2OMtgv7TfPZ6wIRP8,1049
6
+ automated_actions_cli/utils.py,sha256=w5_w5WLuW6pAtABvVMW1IOvx1tSPe-yZzw0u1-sLkBc,1426
7
+ automated_actions_cli-0.1.0.dist-info/METADATA,sha256=GgSoI7Bf-xEu5FDSAFHhzvKZHH191751NuMHN3oqBMs,6372
8
+ automated_actions_cli-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ automated_actions_cli-0.1.0.dist-info/entry_points.txt,sha256=kF-Yau0YT1omwWd5Rke2MfI_L3pe-zhl_XF6EU88E0s,73
10
+ automated_actions_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ automated-actions = automated_actions_cli.__main__:app