kstlib 0.0.1a0__py3-none-any.whl → 1.0.1__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.
- kstlib/__init__.py +266 -1
- kstlib/__main__.py +16 -0
- kstlib/alerts/__init__.py +110 -0
- kstlib/alerts/channels/__init__.py +36 -0
- kstlib/alerts/channels/base.py +197 -0
- kstlib/alerts/channels/email.py +227 -0
- kstlib/alerts/channels/slack.py +389 -0
- kstlib/alerts/exceptions.py +72 -0
- kstlib/alerts/manager.py +651 -0
- kstlib/alerts/models.py +142 -0
- kstlib/alerts/throttle.py +263 -0
- kstlib/auth/__init__.py +139 -0
- kstlib/auth/callback.py +399 -0
- kstlib/auth/config.py +502 -0
- kstlib/auth/errors.py +127 -0
- kstlib/auth/models.py +316 -0
- kstlib/auth/providers/__init__.py +14 -0
- kstlib/auth/providers/base.py +393 -0
- kstlib/auth/providers/oauth2.py +645 -0
- kstlib/auth/providers/oidc.py +821 -0
- kstlib/auth/session.py +338 -0
- kstlib/auth/token.py +482 -0
- kstlib/cache/__init__.py +50 -0
- kstlib/cache/decorator.py +261 -0
- kstlib/cache/strategies.py +516 -0
- kstlib/cli/__init__.py +8 -0
- kstlib/cli/app.py +195 -0
- kstlib/cli/commands/__init__.py +5 -0
- kstlib/cli/commands/auth/__init__.py +39 -0
- kstlib/cli/commands/auth/common.py +122 -0
- kstlib/cli/commands/auth/login.py +325 -0
- kstlib/cli/commands/auth/logout.py +74 -0
- kstlib/cli/commands/auth/providers.py +57 -0
- kstlib/cli/commands/auth/status.py +291 -0
- kstlib/cli/commands/auth/token.py +199 -0
- kstlib/cli/commands/auth/whoami.py +106 -0
- kstlib/cli/commands/config.py +89 -0
- kstlib/cli/commands/ops/__init__.py +39 -0
- kstlib/cli/commands/ops/attach.py +49 -0
- kstlib/cli/commands/ops/common.py +269 -0
- kstlib/cli/commands/ops/list_sessions.py +252 -0
- kstlib/cli/commands/ops/logs.py +49 -0
- kstlib/cli/commands/ops/start.py +98 -0
- kstlib/cli/commands/ops/status.py +138 -0
- kstlib/cli/commands/ops/stop.py +60 -0
- kstlib/cli/commands/rapi/__init__.py +60 -0
- kstlib/cli/commands/rapi/call.py +341 -0
- kstlib/cli/commands/rapi/list.py +99 -0
- kstlib/cli/commands/rapi/show.py +206 -0
- kstlib/cli/commands/secrets/__init__.py +35 -0
- kstlib/cli/commands/secrets/common.py +425 -0
- kstlib/cli/commands/secrets/decrypt.py +88 -0
- kstlib/cli/commands/secrets/doctor.py +743 -0
- kstlib/cli/commands/secrets/encrypt.py +242 -0
- kstlib/cli/commands/secrets/shred.py +96 -0
- kstlib/cli/common.py +86 -0
- kstlib/config/__init__.py +76 -0
- kstlib/config/exceptions.py +110 -0
- kstlib/config/export.py +225 -0
- kstlib/config/loader.py +963 -0
- kstlib/config/sops.py +287 -0
- kstlib/db/__init__.py +54 -0
- kstlib/db/aiosqlcipher.py +137 -0
- kstlib/db/cipher.py +112 -0
- kstlib/db/database.py +367 -0
- kstlib/db/exceptions.py +25 -0
- kstlib/db/pool.py +302 -0
- kstlib/helpers/__init__.py +35 -0
- kstlib/helpers/exceptions.py +11 -0
- kstlib/helpers/time_trigger.py +396 -0
- kstlib/kstlib.conf.yml +890 -0
- kstlib/limits.py +963 -0
- kstlib/logging/__init__.py +108 -0
- kstlib/logging/manager.py +633 -0
- kstlib/mail/__init__.py +42 -0
- kstlib/mail/builder.py +626 -0
- kstlib/mail/exceptions.py +27 -0
- kstlib/mail/filesystem.py +248 -0
- kstlib/mail/transport.py +224 -0
- kstlib/mail/transports/__init__.py +19 -0
- kstlib/mail/transports/gmail.py +268 -0
- kstlib/mail/transports/resend.py +324 -0
- kstlib/mail/transports/smtp.py +326 -0
- kstlib/meta.py +72 -0
- kstlib/metrics/__init__.py +88 -0
- kstlib/metrics/decorators.py +1090 -0
- kstlib/metrics/exceptions.py +14 -0
- kstlib/monitoring/__init__.py +116 -0
- kstlib/monitoring/_styles.py +163 -0
- kstlib/monitoring/cell.py +57 -0
- kstlib/monitoring/config.py +424 -0
- kstlib/monitoring/delivery.py +579 -0
- kstlib/monitoring/exceptions.py +63 -0
- kstlib/monitoring/image.py +220 -0
- kstlib/monitoring/kv.py +79 -0
- kstlib/monitoring/list.py +69 -0
- kstlib/monitoring/metric.py +88 -0
- kstlib/monitoring/monitoring.py +341 -0
- kstlib/monitoring/renderer.py +139 -0
- kstlib/monitoring/service.py +392 -0
- kstlib/monitoring/table.py +129 -0
- kstlib/monitoring/types.py +56 -0
- kstlib/ops/__init__.py +86 -0
- kstlib/ops/base.py +148 -0
- kstlib/ops/container.py +577 -0
- kstlib/ops/exceptions.py +209 -0
- kstlib/ops/manager.py +407 -0
- kstlib/ops/models.py +176 -0
- kstlib/ops/tmux.py +372 -0
- kstlib/ops/validators.py +287 -0
- kstlib/py.typed +0 -0
- kstlib/rapi/__init__.py +118 -0
- kstlib/rapi/client.py +875 -0
- kstlib/rapi/config.py +861 -0
- kstlib/rapi/credentials.py +887 -0
- kstlib/rapi/exceptions.py +213 -0
- kstlib/resilience/__init__.py +101 -0
- kstlib/resilience/circuit_breaker.py +440 -0
- kstlib/resilience/exceptions.py +95 -0
- kstlib/resilience/heartbeat.py +491 -0
- kstlib/resilience/rate_limiter.py +506 -0
- kstlib/resilience/shutdown.py +417 -0
- kstlib/resilience/watchdog.py +637 -0
- kstlib/secrets/__init__.py +29 -0
- kstlib/secrets/exceptions.py +19 -0
- kstlib/secrets/models.py +62 -0
- kstlib/secrets/providers/__init__.py +79 -0
- kstlib/secrets/providers/base.py +58 -0
- kstlib/secrets/providers/environment.py +66 -0
- kstlib/secrets/providers/keyring.py +107 -0
- kstlib/secrets/providers/kms.py +223 -0
- kstlib/secrets/providers/kwargs.py +101 -0
- kstlib/secrets/providers/sops.py +209 -0
- kstlib/secrets/resolver.py +221 -0
- kstlib/secrets/sensitive.py +130 -0
- kstlib/secure/__init__.py +23 -0
- kstlib/secure/fs.py +194 -0
- kstlib/secure/permissions.py +70 -0
- kstlib/ssl.py +347 -0
- kstlib/ui/__init__.py +23 -0
- kstlib/ui/exceptions.py +26 -0
- kstlib/ui/panels.py +484 -0
- kstlib/ui/spinner.py +864 -0
- kstlib/ui/tables.py +382 -0
- kstlib/utils/__init__.py +48 -0
- kstlib/utils/dict.py +36 -0
- kstlib/utils/formatting.py +338 -0
- kstlib/utils/http_trace.py +237 -0
- kstlib/utils/lazy.py +49 -0
- kstlib/utils/secure_delete.py +205 -0
- kstlib/utils/serialization.py +247 -0
- kstlib/utils/text.py +56 -0
- kstlib/utils/validators.py +124 -0
- kstlib/websocket/__init__.py +97 -0
- kstlib/websocket/exceptions.py +214 -0
- kstlib/websocket/manager.py +1102 -0
- kstlib/websocket/models.py +361 -0
- kstlib-1.0.1.dist-info/METADATA +201 -0
- kstlib-1.0.1.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/WHEEL +1 -1
- kstlib-1.0.1.dist-info/entry_points.txt +2 -0
- kstlib-1.0.1.dist-info/licenses/LICENSE.md +9 -0
- kstlib-0.0.1a0.dist-info/METADATA +0 -29
- kstlib-0.0.1a0.dist-info/RECORD +0 -6
- kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Encrypt command for secrets subsystem."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from kstlib.cli.common import (
|
|
11
|
+
CommandResult,
|
|
12
|
+
CommandStatus,
|
|
13
|
+
console,
|
|
14
|
+
exit_with_result,
|
|
15
|
+
render_result,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .common import (
|
|
19
|
+
AGE_RECIPIENT_OPTION,
|
|
20
|
+
CONFIG_OPTION,
|
|
21
|
+
DATA_KEY_OPTION,
|
|
22
|
+
ENCRYPT_SOURCE_ARG,
|
|
23
|
+
FORCE_OPTION,
|
|
24
|
+
FORMATS_OPTION,
|
|
25
|
+
KMS_KEY_OPTION,
|
|
26
|
+
OUT_OPTION,
|
|
27
|
+
QUIET_OPTION,
|
|
28
|
+
SHRED_CHUNK_SIZE_OPTION,
|
|
29
|
+
SHRED_METHOD_OPTION,
|
|
30
|
+
SHRED_OPTION,
|
|
31
|
+
SHRED_PASSES_OPTION,
|
|
32
|
+
SHRED_ZERO_LAST_OPTION,
|
|
33
|
+
EncryptCommandOptions,
|
|
34
|
+
SecureDeleteCLIOptions,
|
|
35
|
+
format_arguments,
|
|
36
|
+
resolve_sops_binary,
|
|
37
|
+
run_sops_command,
|
|
38
|
+
shred_file,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from subprocess import CompletedProcess
|
|
43
|
+
|
|
44
|
+
from kstlib.utils.secure_delete import SecureDeleteReport
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _ensure_encrypt_destination(options: EncryptCommandOptions) -> None:
|
|
48
|
+
"""Abort when the target output is not writable."""
|
|
49
|
+
if options.out is None or not options.out.exists() or options.force:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
result = CommandResult(
|
|
53
|
+
status=CommandStatus.ERROR,
|
|
54
|
+
message=f"Refuse to overwrite existing file: {options.out} (use --force).",
|
|
55
|
+
)
|
|
56
|
+
exit_with_result(result, options.quiet, exit_code=1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _run_encrypt_command(source: Path, options: EncryptCommandOptions) -> CompletedProcess[str]:
|
|
60
|
+
"""Execute the ``sops`` encryption command and handle failures."""
|
|
61
|
+
try:
|
|
62
|
+
completed = run_sops_command(
|
|
63
|
+
options.binary,
|
|
64
|
+
_build_encrypt_args(source, options),
|
|
65
|
+
)
|
|
66
|
+
except FileNotFoundError as exc:
|
|
67
|
+
result = CommandResult(
|
|
68
|
+
status=CommandStatus.ERROR,
|
|
69
|
+
message=(f"SOPS binary '{options.binary}' not found. Install it or set secrets.sops.binary in the config."),
|
|
70
|
+
)
|
|
71
|
+
exit_with_result(result, options.quiet, exit_code=1, cause=exc)
|
|
72
|
+
|
|
73
|
+
if completed.returncode != 0:
|
|
74
|
+
message = completed.stderr.strip() or completed.stdout.strip() or "sops command failed"
|
|
75
|
+
result = CommandResult(status=CommandStatus.ERROR, message=message)
|
|
76
|
+
exit_with_result(result, options.quiet, exit_code=1)
|
|
77
|
+
|
|
78
|
+
return completed
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _maybe_print_encrypt_output(completed: CompletedProcess[str], options: EncryptCommandOptions) -> None:
|
|
82
|
+
"""Display stdout emitted by ``sops`` when appropriate."""
|
|
83
|
+
if options.out is not None:
|
|
84
|
+
return
|
|
85
|
+
if options.quiet or not completed.stdout:
|
|
86
|
+
return
|
|
87
|
+
console.print(completed.stdout.rstrip("\n"))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _handle_shred_request(source: Path, options: EncryptCommandOptions) -> SecureDeleteReport | None:
|
|
91
|
+
"""Perform secure delete when requested, aborting on failure."""
|
|
92
|
+
if not options.shred.enabled:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
shred_opts = options.shred
|
|
96
|
+
report = shred_file(
|
|
97
|
+
source,
|
|
98
|
+
method=shred_opts.method,
|
|
99
|
+
passes=shred_opts.passes,
|
|
100
|
+
zero_last_pass=shred_opts.zero_last_pass,
|
|
101
|
+
chunk_size=shred_opts.chunk_size,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if report.success:
|
|
105
|
+
return report
|
|
106
|
+
|
|
107
|
+
reason = f" {report.message}" if report.message else ""
|
|
108
|
+
result = CommandResult(
|
|
109
|
+
status=CommandStatus.ERROR,
|
|
110
|
+
message=f"Failed to remove cleartext source '{source}'.{reason}",
|
|
111
|
+
)
|
|
112
|
+
return exit_with_result(result, options.quiet, exit_code=1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _compose_encrypt_success_message(
|
|
116
|
+
source: Path,
|
|
117
|
+
options: EncryptCommandOptions,
|
|
118
|
+
report: SecureDeleteReport | None,
|
|
119
|
+
) -> str:
|
|
120
|
+
"""Return the final success message for ``encrypt``."""
|
|
121
|
+
target_info = str(options.out) if options.out else "stdout"
|
|
122
|
+
message = f"Encrypted secrets written to {target_info}."
|
|
123
|
+
|
|
124
|
+
if report is not None:
|
|
125
|
+
detail = f"{report.method.value} ({report.passes} passes)"
|
|
126
|
+
if report.command:
|
|
127
|
+
detail += f" via {' '.join(report.command)}"
|
|
128
|
+
return message + f" Cleartext source '{source}' removed using {detail}."
|
|
129
|
+
|
|
130
|
+
if not options.shred.enabled:
|
|
131
|
+
message += f" Cleartext source '{source}' still exists; run 'kstlib secrets shred {source}' to remove it."
|
|
132
|
+
|
|
133
|
+
return message
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _build_encrypt_args(
|
|
137
|
+
source: Path,
|
|
138
|
+
options: EncryptCommandOptions,
|
|
139
|
+
) -> list[str]:
|
|
140
|
+
"""Build arguments for ``sops --encrypt``."""
|
|
141
|
+
arguments = _base_encrypt_args(options)
|
|
142
|
+
arguments.extend(format_arguments(*options.formats))
|
|
143
|
+
arguments.extend(_recipient_flags(options))
|
|
144
|
+
arguments.extend(_key_flags(options))
|
|
145
|
+
arguments.append(str(source))
|
|
146
|
+
return arguments
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _base_encrypt_args(options: EncryptCommandOptions) -> list[str]:
|
|
150
|
+
"""Return base arguments shared by every encrypt invocation."""
|
|
151
|
+
arguments: list[str] = []
|
|
152
|
+
if options.config is not None:
|
|
153
|
+
arguments.extend(["--config", str(options.config)])
|
|
154
|
+
arguments.append("--encrypt")
|
|
155
|
+
if options.out is not None:
|
|
156
|
+
arguments.extend(["--output", str(options.out)])
|
|
157
|
+
return arguments
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _recipient_flags(options: EncryptCommandOptions) -> list[str]:
|
|
161
|
+
"""Return age or KMS recipient flag arguments."""
|
|
162
|
+
arguments: list[str] = []
|
|
163
|
+
if options.age_recipients:
|
|
164
|
+
for recipient in options.age_recipients:
|
|
165
|
+
arguments.extend(["--age", recipient])
|
|
166
|
+
if options.kms_keys:
|
|
167
|
+
for kms_key in options.kms_keys:
|
|
168
|
+
arguments.extend(["--kms", kms_key])
|
|
169
|
+
return arguments
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _key_flags(options: EncryptCommandOptions) -> list[str]:
|
|
173
|
+
"""Return raw data key flag arguments."""
|
|
174
|
+
arguments: list[str] = []
|
|
175
|
+
if options.data_keys:
|
|
176
|
+
for data_key in options.data_keys:
|
|
177
|
+
arguments.extend(["--key", data_key])
|
|
178
|
+
return arguments
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _execute_encrypt(source: Path, options: EncryptCommandOptions) -> None:
|
|
182
|
+
"""Perform the encryption workflow using the provided options."""
|
|
183
|
+
_ensure_encrypt_destination(options)
|
|
184
|
+
completed = _run_encrypt_command(source, options)
|
|
185
|
+
_maybe_print_encrypt_output(completed, options)
|
|
186
|
+
shred_report = _handle_shred_request(source, options)
|
|
187
|
+
message = _compose_encrypt_success_message(source, options, shred_report)
|
|
188
|
+
result = CommandResult(status=CommandStatus.OK, message=message)
|
|
189
|
+
if options.quiet:
|
|
190
|
+
raise typer.Exit(code=0)
|
|
191
|
+
render_result(result)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
|
|
195
|
+
def encrypt(
|
|
196
|
+
source: Path = ENCRYPT_SOURCE_ARG,
|
|
197
|
+
*,
|
|
198
|
+
out: Path | None = OUT_OPTION,
|
|
199
|
+
config: Path | None = CONFIG_OPTION,
|
|
200
|
+
formats: tuple[str, str] = FORMATS_OPTION,
|
|
201
|
+
force: bool = FORCE_OPTION,
|
|
202
|
+
quiet: bool = QUIET_OPTION,
|
|
203
|
+
shred_enabled: bool = SHRED_OPTION,
|
|
204
|
+
shred_method: str | None = SHRED_METHOD_OPTION,
|
|
205
|
+
shred_passes: int | None = SHRED_PASSES_OPTION,
|
|
206
|
+
shred_zero_last_pass: bool | None = SHRED_ZERO_LAST_OPTION,
|
|
207
|
+
shred_chunk_size: int | None = SHRED_CHUNK_SIZE_OPTION,
|
|
208
|
+
age_recipient: list[str] | None = AGE_RECIPIENT_OPTION,
|
|
209
|
+
kms_key: list[str] | None = KMS_KEY_OPTION,
|
|
210
|
+
key: list[str] | None = DATA_KEY_OPTION,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Encrypt a cleartext file using sops."""
|
|
213
|
+
binary = resolve_sops_binary()
|
|
214
|
+
effective_config = config
|
|
215
|
+
if effective_config is None:
|
|
216
|
+
default_config = Path.home() / ".sops.yaml"
|
|
217
|
+
if default_config.exists():
|
|
218
|
+
effective_config = default_config
|
|
219
|
+
|
|
220
|
+
options = EncryptCommandOptions(
|
|
221
|
+
out=out,
|
|
222
|
+
binary=binary,
|
|
223
|
+
config=effective_config,
|
|
224
|
+
formats=formats,
|
|
225
|
+
force=force,
|
|
226
|
+
quiet=quiet,
|
|
227
|
+
shred=SecureDeleteCLIOptions(
|
|
228
|
+
enabled=shred_enabled,
|
|
229
|
+
method=shred_method,
|
|
230
|
+
passes=shred_passes,
|
|
231
|
+
zero_last_pass=shred_zero_last_pass,
|
|
232
|
+
chunk_size=shred_chunk_size,
|
|
233
|
+
),
|
|
234
|
+
age_recipients=tuple(age_recipient or []),
|
|
235
|
+
kms_keys=tuple(kms_key or []),
|
|
236
|
+
data_keys=tuple(key or []),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
_execute_encrypt(source, options)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
__all__ = ["encrypt"]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Shred command for secrets subsystem."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from kstlib.cli.common import (
|
|
10
|
+
CommandResult,
|
|
11
|
+
CommandStatus,
|
|
12
|
+
exit_with_result,
|
|
13
|
+
render_result,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .common import (
|
|
17
|
+
SHRED_CMD_CHUNK_SIZE_OPTION,
|
|
18
|
+
SHRED_CMD_METHOD_OPTION,
|
|
19
|
+
SHRED_CMD_PASSES_OPTION,
|
|
20
|
+
SHRED_CMD_QUIET_OPTION,
|
|
21
|
+
SHRED_CMD_ZERO_LAST_OPTION,
|
|
22
|
+
SHRED_FORCE_OPTION,
|
|
23
|
+
SHRED_TARGET_ARG,
|
|
24
|
+
ShredCommandOptions,
|
|
25
|
+
shred_file,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
else: # pragma: no cover - runtime alias for Typer conversions
|
|
31
|
+
import pathlib
|
|
32
|
+
|
|
33
|
+
Path = pathlib.Path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _execute_shred(target: Path, options: ShredCommandOptions) -> None:
|
|
37
|
+
"""Perform the shredding workflow using the provided options."""
|
|
38
|
+
if not options.force:
|
|
39
|
+
confirmed = typer.confirm(f"Remove '{target}' permanently?", default=False)
|
|
40
|
+
if not confirmed:
|
|
41
|
+
result = CommandResult(
|
|
42
|
+
status=CommandStatus.WARNING,
|
|
43
|
+
message=f"Shred aborted; file '{target}' not removed.",
|
|
44
|
+
)
|
|
45
|
+
exit_with_result(result, options.quiet, exit_code=1)
|
|
46
|
+
|
|
47
|
+
report = shred_file(
|
|
48
|
+
target,
|
|
49
|
+
method=options.method,
|
|
50
|
+
passes=options.passes,
|
|
51
|
+
zero_last_pass=options.zero_last_pass,
|
|
52
|
+
chunk_size=options.chunk_size,
|
|
53
|
+
)
|
|
54
|
+
if not report.success:
|
|
55
|
+
result = CommandResult(
|
|
56
|
+
status=CommandStatus.ERROR,
|
|
57
|
+
message=report.message or f"Failed to remove '{target}'. Check permissions and try again.",
|
|
58
|
+
)
|
|
59
|
+
exit_with_result(result, options.quiet, exit_code=1)
|
|
60
|
+
|
|
61
|
+
detail = f"{report.method.value} ({report.passes} passes)"
|
|
62
|
+
if report.command:
|
|
63
|
+
detail += f" via {' '.join(report.command)}"
|
|
64
|
+
result = CommandResult(
|
|
65
|
+
status=CommandStatus.OK,
|
|
66
|
+
message=f"Secret file '{target}' removed using {detail}.",
|
|
67
|
+
)
|
|
68
|
+
if options.quiet:
|
|
69
|
+
raise typer.Exit(code=0)
|
|
70
|
+
render_result(result)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
74
|
+
def shred(
|
|
75
|
+
target: Path = SHRED_TARGET_ARG,
|
|
76
|
+
*,
|
|
77
|
+
force: bool = SHRED_FORCE_OPTION,
|
|
78
|
+
method: str | None = SHRED_CMD_METHOD_OPTION,
|
|
79
|
+
passes: int | None = SHRED_CMD_PASSES_OPTION,
|
|
80
|
+
zero_last_pass: bool | None = SHRED_CMD_ZERO_LAST_OPTION,
|
|
81
|
+
chunk_size: int | None = SHRED_CMD_CHUNK_SIZE_OPTION,
|
|
82
|
+
quiet: bool = SHRED_CMD_QUIET_OPTION,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Remove a secrets file from disk."""
|
|
85
|
+
options = ShredCommandOptions(
|
|
86
|
+
force=force,
|
|
87
|
+
method=method,
|
|
88
|
+
passes=passes,
|
|
89
|
+
zero_last_pass=zero_last_pass,
|
|
90
|
+
chunk_size=chunk_size,
|
|
91
|
+
quiet=quiet,
|
|
92
|
+
)
|
|
93
|
+
_execute_shred(target, options)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = ["shred"]
|
kstlib/cli/common.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Shared CLI utilities for kstlib commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, NoReturn
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.pretty import Pretty
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CommandStatus(str, Enum):
|
|
18
|
+
"""Represents the outcome of a CLI command."""
|
|
19
|
+
|
|
20
|
+
OK = "ok"
|
|
21
|
+
WARNING = "warning"
|
|
22
|
+
ERROR = "error"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(slots=True)
|
|
26
|
+
class CommandResult:
|
|
27
|
+
"""Summary payload produced by a command handler."""
|
|
28
|
+
|
|
29
|
+
status: CommandStatus
|
|
30
|
+
message: str
|
|
31
|
+
payload: dict[str, Any] | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
STYLE_MAP = {
|
|
35
|
+
CommandStatus.OK: "green",
|
|
36
|
+
CommandStatus.WARNING: "yellow",
|
|
37
|
+
CommandStatus.ERROR: "red",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render_result(result: CommandResult) -> None:
|
|
42
|
+
"""Render a command result using Rich components."""
|
|
43
|
+
style = STYLE_MAP[result.status]
|
|
44
|
+
console.print(Panel(result.message, title=result.status.value.upper(), style=style, border_style=style))
|
|
45
|
+
if result.payload is not None:
|
|
46
|
+
console.print(Pretty(result.payload))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def emit_result(result: CommandResult, quiet: bool) -> None:
|
|
50
|
+
"""Output a command result honoring the quiet flag."""
|
|
51
|
+
if quiet:
|
|
52
|
+
console.print(result.message, style=STYLE_MAP[result.status])
|
|
53
|
+
else:
|
|
54
|
+
render_result(result)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def exit_with_result(
|
|
58
|
+
result: CommandResult,
|
|
59
|
+
quiet: bool,
|
|
60
|
+
exit_code: int,
|
|
61
|
+
*,
|
|
62
|
+
cause: Exception | None = None,
|
|
63
|
+
) -> NoReturn:
|
|
64
|
+
"""Render ``result`` and raise ``typer.Exit`` with ``exit_code``."""
|
|
65
|
+
emit_result(result, quiet)
|
|
66
|
+
if cause is None:
|
|
67
|
+
raise typer.Exit(code=exit_code)
|
|
68
|
+
raise typer.Exit(code=exit_code) from cause
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def exit_error(message: str) -> NoReturn:
|
|
72
|
+
"""Render an error result and exit with code 1."""
|
|
73
|
+
render_result(CommandResult(status=CommandStatus.ERROR, message=message))
|
|
74
|
+
raise typer.Exit(code=1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"STYLE_MAP",
|
|
79
|
+
"CommandResult",
|
|
80
|
+
"CommandStatus",
|
|
81
|
+
"console",
|
|
82
|
+
"emit_result",
|
|
83
|
+
"exit_error",
|
|
84
|
+
"exit_with_result",
|
|
85
|
+
"render_result",
|
|
86
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Configuration management module.
|
|
2
|
+
|
|
3
|
+
This module provides flexible configuration loading from various file formats
|
|
4
|
+
with support for includes, environment variables, and cascading search.
|
|
5
|
+
|
|
6
|
+
SOPS Integration:
|
|
7
|
+
Files with .sops.yml, .sops.yaml, .sops.json, or .sops.toml extensions
|
|
8
|
+
are automatically decrypted via the SOPS binary when loaded.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# pylint: disable=duplicate-code
|
|
12
|
+
from kstlib.config.exceptions import (
|
|
13
|
+
ConfigCircularIncludeError,
|
|
14
|
+
ConfigError,
|
|
15
|
+
ConfigFileNotFoundError,
|
|
16
|
+
ConfigFormatError,
|
|
17
|
+
ConfigIncludeDepthError,
|
|
18
|
+
ConfigNotLoadedError,
|
|
19
|
+
ConfigSopsError,
|
|
20
|
+
ConfigSopsNotAvailableError,
|
|
21
|
+
KstlibError,
|
|
22
|
+
)
|
|
23
|
+
from kstlib.config.export import (
|
|
24
|
+
ConfigExportError,
|
|
25
|
+
ConfigExportOptions,
|
|
26
|
+
ConfigExportResult,
|
|
27
|
+
export_configuration,
|
|
28
|
+
)
|
|
29
|
+
from kstlib.config.loader import (
|
|
30
|
+
CONFIG_FILENAME,
|
|
31
|
+
ConfigLoader,
|
|
32
|
+
clear_config,
|
|
33
|
+
get_config,
|
|
34
|
+
load_config,
|
|
35
|
+
load_from_env,
|
|
36
|
+
load_from_file,
|
|
37
|
+
require_config,
|
|
38
|
+
)
|
|
39
|
+
from kstlib.config.sops import (
|
|
40
|
+
SopsDecryptor,
|
|
41
|
+
get_decryptor,
|
|
42
|
+
get_real_extension,
|
|
43
|
+
has_encrypted_values,
|
|
44
|
+
is_sops_file,
|
|
45
|
+
reset_decryptor,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"CONFIG_FILENAME",
|
|
50
|
+
"ConfigCircularIncludeError",
|
|
51
|
+
"ConfigError",
|
|
52
|
+
"ConfigExportError",
|
|
53
|
+
"ConfigExportOptions",
|
|
54
|
+
"ConfigExportResult",
|
|
55
|
+
"ConfigFileNotFoundError",
|
|
56
|
+
"ConfigFormatError",
|
|
57
|
+
"ConfigIncludeDepthError",
|
|
58
|
+
"ConfigLoader",
|
|
59
|
+
"ConfigNotLoadedError",
|
|
60
|
+
"ConfigSopsError",
|
|
61
|
+
"ConfigSopsNotAvailableError",
|
|
62
|
+
"KstlibError",
|
|
63
|
+
"SopsDecryptor",
|
|
64
|
+
"clear_config",
|
|
65
|
+
"export_configuration",
|
|
66
|
+
"get_config",
|
|
67
|
+
"get_decryptor",
|
|
68
|
+
"get_real_extension",
|
|
69
|
+
"has_encrypted_values",
|
|
70
|
+
"is_sops_file",
|
|
71
|
+
"load_config",
|
|
72
|
+
"load_from_env",
|
|
73
|
+
"load_from_file",
|
|
74
|
+
"require_config",
|
|
75
|
+
"reset_decryptor",
|
|
76
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception hierarchy for kstlib.
|
|
3
|
+
|
|
4
|
+
All kstlib exceptions inherit from KstlibError, allowing users to catch
|
|
5
|
+
all kstlib-specific errors with a single except clause.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
try:
|
|
9
|
+
config = load_config()
|
|
10
|
+
except KstlibError as e:
|
|
11
|
+
# Catch any kstlib error
|
|
12
|
+
print(f"Error: {e}")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KstlibError(Exception):
|
|
17
|
+
"""
|
|
18
|
+
Base exception for all kstlib errors.
|
|
19
|
+
|
|
20
|
+
All kstlib-specific exceptions inherit from this class,
|
|
21
|
+
allowing for easy catching of any kstlib error.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Configuration module exceptions
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConfigError(KstlibError):
|
|
31
|
+
"""
|
|
32
|
+
Base exception for configuration-related errors.
|
|
33
|
+
|
|
34
|
+
All config module exceptions inherit from this class.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ConfigFileNotFoundError(ConfigError, FileNotFoundError):
|
|
39
|
+
"""
|
|
40
|
+
Configuration file not found at specified location.
|
|
41
|
+
|
|
42
|
+
Raised when attempting to load a config file that doesn't exist.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ConfigFormatError(ConfigError, ValueError):
|
|
47
|
+
"""
|
|
48
|
+
Invalid configuration format or unsupported file type.
|
|
49
|
+
|
|
50
|
+
Raised when:
|
|
51
|
+
- File extension is not supported (.xml, etc.)
|
|
52
|
+
- Format mismatch in strict mode (YAML including JSON)
|
|
53
|
+
- Invalid content that cannot be parsed
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ConfigCircularIncludeError(ConfigError, ValueError):
|
|
58
|
+
"""
|
|
59
|
+
Circular include detected in configuration files.
|
|
60
|
+
|
|
61
|
+
Raised when an include chain creates a cycle (A includes B, B includes A).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ConfigIncludeDepthError(ConfigError, ValueError):
|
|
66
|
+
"""
|
|
67
|
+
Include depth limit exceeded.
|
|
68
|
+
|
|
69
|
+
Raised when config includes are nested too deeply, which may indicate
|
|
70
|
+
a misconfiguration or an attempt to exhaust resources.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ConfigNotLoadedError(ConfigError, RuntimeError):
|
|
75
|
+
"""
|
|
76
|
+
Configuration not loaded yet.
|
|
77
|
+
|
|
78
|
+
Raised by require_config() when attempting to access config
|
|
79
|
+
before it has been loaded via get_config() or load_config().
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ConfigSopsError(ConfigError):
|
|
84
|
+
"""
|
|
85
|
+
SOPS decryption failed for a configuration file.
|
|
86
|
+
|
|
87
|
+
Raised when SOPS binary fails to decrypt a .sops.* file.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ConfigSopsNotAvailableError(ConfigSopsError):
|
|
92
|
+
"""
|
|
93
|
+
SOPS binary not installed or not found in PATH.
|
|
94
|
+
|
|
95
|
+
Raised when attempting to decrypt a .sops.* file but the SOPS
|
|
96
|
+
binary is not available. Install from https://github.com/getsops/sops
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"ConfigCircularIncludeError",
|
|
102
|
+
"ConfigError",
|
|
103
|
+
"ConfigFileNotFoundError",
|
|
104
|
+
"ConfigFormatError",
|
|
105
|
+
"ConfigIncludeDepthError",
|
|
106
|
+
"ConfigNotLoadedError",
|
|
107
|
+
"ConfigSopsError",
|
|
108
|
+
"ConfigSopsNotAvailableError",
|
|
109
|
+
"KstlibError",
|
|
110
|
+
]
|