automated-actions-cli 0.1.0__tar.gz
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.
- automated_actions_cli-0.1.0/PKG-INFO +155 -0
- automated_actions_cli-0.1.0/README.md +136 -0
- automated_actions_cli-0.1.0/automated_actions_cli/__init__.py +0 -0
- automated_actions_cli-0.1.0/automated_actions_cli/__main__.py +4 -0
- automated_actions_cli-0.1.0/automated_actions_cli/cli.py +198 -0
- automated_actions_cli-0.1.0/automated_actions_cli/config.py +20 -0
- automated_actions_cli-0.1.0/automated_actions_cli/formatter.py +41 -0
- automated_actions_cli-0.1.0/automated_actions_cli/utils.py +51 -0
- automated_actions_cli-0.1.0/pyproject.toml +131 -0
|
@@ -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,136 @@
|
|
|
1
|
+
# `automated_actions_cli` Package 💻🚀
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## 🎯 Overview
|
|
6
|
+
|
|
7
|
+
The `automated_actions_cli` provides a user-friendly way to:
|
|
8
|
+
|
|
9
|
+
* List available automated actions.
|
|
10
|
+
* Trigger predefined actions with necessary parameters.
|
|
11
|
+
* Query the status and results of submitted actions.
|
|
12
|
+
* Perform administrative tasks (e.g., token generation for service accounts, if applicable).
|
|
13
|
+
|
|
14
|
+
## ✨ Key Technologies
|
|
15
|
+
|
|
16
|
+
* **[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.
|
|
17
|
+
* **`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.
|
|
18
|
+
|
|
19
|
+
## ⚙️ Core Functionality
|
|
20
|
+
|
|
21
|
+
The CLI acts as a wrapper around the `automated_actions_client`. When a user executes a CLI command:
|
|
22
|
+
|
|
23
|
+
1. Typer parses the command, subcommands, arguments, and options.
|
|
24
|
+
2. Authentication is handled (see [Authentication](#-authentication) section below).
|
|
25
|
+
3. The appropriate function from `automated_actions_client` is called with the user-provided parameters.
|
|
26
|
+
4. The response from the API (via the client) is then processed and presented to the user in a human-readable format.
|
|
27
|
+
|
|
28
|
+
Commands are typically structured in a hierarchical way, for example:
|
|
29
|
+
`automated-actions <command> [arguments_and_options]`
|
|
30
|
+
|
|
31
|
+
## 🔑 Authentication (Kerberos)
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
39
|
+
## 💡 Usage Examples
|
|
40
|
+
|
|
41
|
+
**1. Listing all user available action:**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
$ automated-actions me
|
|
45
|
+
---
|
|
46
|
+
allowed_actions:
|
|
47
|
+
- action-cancel
|
|
48
|
+
- action-detail
|
|
49
|
+
- action-list
|
|
50
|
+
- create-token
|
|
51
|
+
- me
|
|
52
|
+
- openshift-workload-restart-unthrottled
|
|
53
|
+
created_at: 1747919185.209177
|
|
54
|
+
email: your-email@address.com
|
|
55
|
+
name: Your Name
|
|
56
|
+
updated_at: 1747996842.377609
|
|
57
|
+
username: your-username
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**2. List all past actions:**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
$ automated-actions action-list
|
|
64
|
+
---
|
|
65
|
+
- action_id: b8ff9963-516f-437a-b93c-b7354cd5225d
|
|
66
|
+
created_at: 1747919261.038378
|
|
67
|
+
name: openshift-workload-restart
|
|
68
|
+
owner: your-username
|
|
69
|
+
result: ok
|
|
70
|
+
status: SUCCESS
|
|
71
|
+
task_args:
|
|
72
|
+
cluster: cluster-1
|
|
73
|
+
kind: Pod
|
|
74
|
+
name: example-74d78dbfbf-89rl2
|
|
75
|
+
namespace: namespace-dev
|
|
76
|
+
updated_at: 1747919261.962519
|
|
77
|
+
...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**2. Triggering an action (e.g., restarting an OpenShift deployment):**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
$ automated-actions openshift-workload-restart --cluster "my-cluster" --namespace "my-namespace" --kind Deployment --name "my-app"
|
|
84
|
+
---
|
|
85
|
+
- action_id: b8ff9963-516f-437a-b93c-b7354cd5225d
|
|
86
|
+
created_at: 1747919261.038378
|
|
87
|
+
name: openshift-workload-restart
|
|
88
|
+
owner: your-username
|
|
89
|
+
result: ok
|
|
90
|
+
status: PENDING
|
|
91
|
+
task_args:
|
|
92
|
+
cluster: my-cluster
|
|
93
|
+
kind: Deployment
|
|
94
|
+
name: my-app
|
|
95
|
+
namespace: my-namespac
|
|
96
|
+
updated_at: 1747919261.962519
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 🧑💻 Development
|
|
100
|
+
|
|
101
|
+
See the main project `README.md` for general development instructions.
|
|
102
|
+
|
|
103
|
+
### Running the CLI Locally
|
|
104
|
+
|
|
105
|
+
During development, you can invoke the CLI directly using `automated-actions`:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
automated-actions --help
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Relationship with `automated_actions_client`
|
|
112
|
+
|
|
113
|
+
* **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.
|
|
114
|
+
* **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`.
|
|
115
|
+
* **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.
|
|
116
|
+
|
|
117
|
+
### Testing
|
|
118
|
+
|
|
119
|
+
Tests for this package are located within its `tests/` directory. These typically involve:
|
|
120
|
+
|
|
121
|
+
* Using Typer's `CliRunner` to invoke CLI commands and assert their output and exit codes.
|
|
122
|
+
* Mocking the `automated_actions_client` to avoid making real API calls during unit tests.
|
|
123
|
+
* Integration tests that do make real API calls to a test instance of the server.
|
|
124
|
+
|
|
125
|
+
To run tests specifically for this package:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
make test
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 🤝 Contributing to this Package
|
|
132
|
+
|
|
133
|
+
* Follow the general contributing guidelines in the main project `README.md`.
|
|
134
|
+
* Ensure new CLI commands are intuitive and provide helpful error messages.
|
|
135
|
+
* Add or update tests for any new or modified commands.
|
|
136
|
+
* Keep Typer's auto-completion features in mind for a good user experience.
|
|
File without changes
|
|
@@ -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,131 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "automated-actions-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Automated Actions Client"
|
|
5
|
+
authors = [
|
|
6
|
+
# Feel free to add or change authors
|
|
7
|
+
{ name = "AppSRE", email = "sd-app-sre@redhat.com" },
|
|
8
|
+
]
|
|
9
|
+
license = { text = "Apache 2.0" }
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = "~= 3.12.0"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"appdirs==1.4.4",
|
|
14
|
+
"automated-actions-client",
|
|
15
|
+
"httpx-gssapi==0.4",
|
|
16
|
+
"pydantic-settings==2.9.1",
|
|
17
|
+
"pyyaml==6.0.2",
|
|
18
|
+
"rich==14.0.0",
|
|
19
|
+
"typer==0.16.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
homepage = "https://github.com/app-sre/automated-actions"
|
|
24
|
+
repository = "https://github.com/app-sre/automated-actions"
|
|
25
|
+
documentation = "https://github.com/app-sre/automated-actions"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
# Development dependencies
|
|
30
|
+
"ruff==0.11.12",
|
|
31
|
+
"mypy==1.16.0",
|
|
32
|
+
"pytest==8.4.0",
|
|
33
|
+
"pytest-cov==6.1.1",
|
|
34
|
+
"types-pyyaml==6.0.12.20250516",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
automated-actions = 'automated_actions_cli.__main__:app'
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["hatchling"]
|
|
42
|
+
build-backend = "hatchling.build"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.sdist]
|
|
45
|
+
only-include = ["automated_actions_cli"]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
only-include = ["automated_actions_cli"]
|
|
49
|
+
|
|
50
|
+
# Ruff configuration
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 88
|
|
53
|
+
src = ["automated_actions_cli"]
|
|
54
|
+
fix = true
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
preview = true
|
|
58
|
+
select = ["ALL"]
|
|
59
|
+
ignore = [
|
|
60
|
+
"ANN401", # allow ANY
|
|
61
|
+
"CPY", # Missing copyright header
|
|
62
|
+
"D100", # Missing docstring in public module
|
|
63
|
+
"D101", # Missing docstring in public class
|
|
64
|
+
"D102", # Missing docstring in public method
|
|
65
|
+
"D103", # Missing docstring in public function
|
|
66
|
+
"D104", # Missing docstring in public package
|
|
67
|
+
"D105", # Missing docstring in magic method
|
|
68
|
+
"D107", # Missing docstring in __init__
|
|
69
|
+
"D203", # 1 blank line required before class docstring
|
|
70
|
+
"D211", # No blank lines allowed before class docstring
|
|
71
|
+
"D212", # multi-line-summary-first-line
|
|
72
|
+
"D213", # multi-line-summary-second-line
|
|
73
|
+
"D4", # Doc string style
|
|
74
|
+
"E501", # Line too long
|
|
75
|
+
"G004", # Logging statement uses f-string
|
|
76
|
+
"PLR0904", # Too many public methods
|
|
77
|
+
"PLR0913", # Too many arguments
|
|
78
|
+
"PLR0917", # Too many positional arguments
|
|
79
|
+
"S101", # Use of assert detected. Pytest uses assert
|
|
80
|
+
"S404", # subprocess import
|
|
81
|
+
"EM101", # Exception must not use a string literal, assign to variable first
|
|
82
|
+
"EM102", # Exception must not use an f-string literal, assign to variable first
|
|
83
|
+
"S603", # subprocess
|
|
84
|
+
"S607", # subprocess.run w/o full path
|
|
85
|
+
"TRY003", # Avoid specifying long messages outside the exception class
|
|
86
|
+
"TRY300", # try-consider-else
|
|
87
|
+
# pydoclint
|
|
88
|
+
"DOC",
|
|
89
|
+
# May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
|
|
90
|
+
"W191",
|
|
91
|
+
"E111",
|
|
92
|
+
"E114",
|
|
93
|
+
"E117",
|
|
94
|
+
"D206",
|
|
95
|
+
"D300",
|
|
96
|
+
"Q",
|
|
97
|
+
"COM812",
|
|
98
|
+
"COM819",
|
|
99
|
+
"ISC001",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
[tool.ruff.format]
|
|
103
|
+
preview = true
|
|
104
|
+
|
|
105
|
+
[tool.ruff.lint.isort]
|
|
106
|
+
known-first-party = ["automated_actions_cli"]
|
|
107
|
+
|
|
108
|
+
[tool.mypy]
|
|
109
|
+
files = ["automated_actions_cli"]
|
|
110
|
+
enable_error_code = ["truthy-bool", "redundant-expr"]
|
|
111
|
+
plugins = ["pydantic.mypy"]
|
|
112
|
+
no_implicit_optional = true
|
|
113
|
+
check_untyped_defs = true
|
|
114
|
+
warn_unused_ignores = true
|
|
115
|
+
show_error_codes = true
|
|
116
|
+
disallow_untyped_defs = true
|
|
117
|
+
disallow_incomplete_defs = true
|
|
118
|
+
|
|
119
|
+
[[tool.mypy.overrides]]
|
|
120
|
+
# Below are all of the packages that don't implement stub packages. Mypy will throw an error if we don't ignore the
|
|
121
|
+
# missing imports. See: https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
|
|
122
|
+
module = ["appdirs.*", "httpx_gssapi.*"]
|
|
123
|
+
ignore_missing_imports = true
|
|
124
|
+
|
|
125
|
+
# Coverage configuration
|
|
126
|
+
[tool.coverage.run]
|
|
127
|
+
branch = true
|
|
128
|
+
omit = ["*/tests/*"]
|
|
129
|
+
|
|
130
|
+
# [tool.coverage.report]
|
|
131
|
+
# fail_under = 90
|