octopize.deploy_tool 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.
- octopize_avatar_deploy/__init__.py +26 -0
- octopize_avatar_deploy/cli_test_harness.py +217 -0
- octopize_avatar_deploy/configure.py +705 -0
- octopize_avatar_deploy/defaults.yaml +63 -0
- octopize_avatar_deploy/download_templates.py +251 -0
- octopize_avatar_deploy/input_gatherer.py +324 -0
- octopize_avatar_deploy/printer.py +216 -0
- octopize_avatar_deploy/state_manager.py +136 -0
- octopize_avatar_deploy/steps/__init__.py +25 -0
- octopize_avatar_deploy/steps/authentik.py +46 -0
- octopize_avatar_deploy/steps/authentik_blueprint.py +79 -0
- octopize_avatar_deploy/steps/base.py +199 -0
- octopize_avatar_deploy/steps/database.py +37 -0
- octopize_avatar_deploy/steps/email.py +88 -0
- octopize_avatar_deploy/steps/logging.py +32 -0
- octopize_avatar_deploy/steps/required.py +81 -0
- octopize_avatar_deploy/steps/storage.py +32 -0
- octopize_avatar_deploy/steps/telemetry.py +58 -0
- octopize_avatar_deploy/steps/user.py +89 -0
- octopize_avatar_deploy/version_compat.py +344 -0
- octopize_deploy_tool-0.1.0.dist-info/METADATA +346 -0
- octopize_deploy_tool-0.1.0.dist-info/RECORD +24 -0
- octopize_deploy_tool-0.1.0.dist-info/WHEEL +4 -0
- octopize_deploy_tool-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Output printer abstraction for deployment tool.
|
|
4
|
+
|
|
5
|
+
Provides a pluggable interface for customizing output behavior
|
|
6
|
+
(console, file, GUI, etc.) without modifying core logic.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Protocol
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Printer(Protocol):
|
|
18
|
+
"""Protocol for output printing."""
|
|
19
|
+
|
|
20
|
+
def print(self, message: str = "") -> None:
|
|
21
|
+
"""Print a regular message."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def print_header(self, title: str, width: int = 60) -> None:
|
|
25
|
+
"""Print a section header."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def print_success(self, message: str) -> None:
|
|
29
|
+
"""Print a success message."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def print_error(self, message: str) -> None:
|
|
33
|
+
"""Print an error message."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def print_warning(self, message: str) -> None:
|
|
37
|
+
"""Print a warning message."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def print_step(self, description: str, skipped: bool = False) -> None:
|
|
41
|
+
"""Print a step header."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConsolePrinter:
|
|
46
|
+
"""Default console-based printer implementation."""
|
|
47
|
+
|
|
48
|
+
def print(self, message: str = "") -> None:
|
|
49
|
+
"""Print a regular message to console."""
|
|
50
|
+
print(message)
|
|
51
|
+
|
|
52
|
+
def print_header(self, title: str, width: int = 60) -> None:
|
|
53
|
+
"""Print a formatted section header."""
|
|
54
|
+
self.print("\n" + "=" * width)
|
|
55
|
+
self.print(title)
|
|
56
|
+
self.print("=" * width)
|
|
57
|
+
|
|
58
|
+
def print_success(self, message: str) -> None:
|
|
59
|
+
"""Print a success message with checkmark."""
|
|
60
|
+
self.print(f"✓ {message}")
|
|
61
|
+
|
|
62
|
+
def print_error(self, message: str) -> None:
|
|
63
|
+
"""Print an error message with X symbol."""
|
|
64
|
+
self.print(f"✗ {message}")
|
|
65
|
+
|
|
66
|
+
def print_warning(self, message: str) -> None:
|
|
67
|
+
"""Print a warning message with warning symbol."""
|
|
68
|
+
self.print(f"⚠ {message}")
|
|
69
|
+
|
|
70
|
+
def print_step(self, description: str, skipped: bool = False) -> None:
|
|
71
|
+
"""Print a step header with optional skipped indicator."""
|
|
72
|
+
if skipped:
|
|
73
|
+
self.print(f"\n--- {description} [SKIPPED - already completed] ---")
|
|
74
|
+
else:
|
|
75
|
+
self.print(f"\n--- {description} ---")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SilentPrinter:
|
|
79
|
+
"""Silent printer that suppresses all output."""
|
|
80
|
+
|
|
81
|
+
def print(self, message: str = "") -> None:
|
|
82
|
+
"""Do nothing."""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def print_header(self, title: str, width: int = 60) -> None:
|
|
86
|
+
"""Do nothing."""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
def print_success(self, message: str) -> None:
|
|
90
|
+
"""Do nothing."""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
def print_error(self, message: str) -> None:
|
|
94
|
+
"""Do nothing."""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
def print_warning(self, message: str) -> None:
|
|
98
|
+
"""Do nothing."""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
def print_step(self, description: str, skipped: bool = False) -> None:
|
|
102
|
+
"""Do nothing."""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RichPrinter:
|
|
107
|
+
"""
|
|
108
|
+
Rich-based printer with enhanced formatting and colors.
|
|
109
|
+
|
|
110
|
+
Uses the 'rich' library to provide beautiful console output with:
|
|
111
|
+
- Colors and styles
|
|
112
|
+
- Panels and boxes
|
|
113
|
+
- Better formatting
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self):
|
|
117
|
+
"""Initialize RichPrinter with a Console instance."""
|
|
118
|
+
self.console = Console()
|
|
119
|
+
|
|
120
|
+
def print(self, message: str = "") -> None:
|
|
121
|
+
"""Print a regular message to console."""
|
|
122
|
+
self.console.print(message)
|
|
123
|
+
|
|
124
|
+
def print_header(self, title: str, width: int = 60) -> None:
|
|
125
|
+
"""Print a formatted section header with panel."""
|
|
126
|
+
if title:
|
|
127
|
+
panel = Panel(
|
|
128
|
+
Text(title, style="bold cyan", justify="center"),
|
|
129
|
+
border_style="cyan",
|
|
130
|
+
padding=(0, 2),
|
|
131
|
+
)
|
|
132
|
+
self.console.print()
|
|
133
|
+
self.console.print(panel)
|
|
134
|
+
else:
|
|
135
|
+
self.console.print()
|
|
136
|
+
|
|
137
|
+
def print_success(self, message: str) -> None:
|
|
138
|
+
"""Print a success message with green checkmark."""
|
|
139
|
+
self.console.print(f"[green]✓[/green] {message}")
|
|
140
|
+
|
|
141
|
+
def print_error(self, message: str) -> None:
|
|
142
|
+
"""Print an error message with red X symbol."""
|
|
143
|
+
self.console.print(f"[red]✗[/red] {message}")
|
|
144
|
+
|
|
145
|
+
def print_warning(self, message: str) -> None:
|
|
146
|
+
"""Print a warning message with yellow warning symbol."""
|
|
147
|
+
self.console.print(f"[yellow]⚠[/yellow] {message}")
|
|
148
|
+
|
|
149
|
+
def print_step(self, description: str, skipped: bool = False) -> None:
|
|
150
|
+
"""Print a step header with optional skipped indicator."""
|
|
151
|
+
self.console.print()
|
|
152
|
+
if skipped:
|
|
153
|
+
self.console.print(f"[dim]--- {description} [SKIPPED - already completed] ---[/dim]")
|
|
154
|
+
else:
|
|
155
|
+
self.console.print(f"[bold blue]--- {description} ---[/bold blue]")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class FilePrinter:
|
|
159
|
+
"""
|
|
160
|
+
File-based printer that writes all output to a log file.
|
|
161
|
+
|
|
162
|
+
Useful for test debugging - when tests fail, you can inspect
|
|
163
|
+
the log file to see what happened.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(self, log_file: Path | str, append: bool = False):
|
|
167
|
+
"""
|
|
168
|
+
Initialize FilePrinter.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
log_file: Path to log file
|
|
172
|
+
append: Whether to append to existing file (default: False, overwrite)
|
|
173
|
+
"""
|
|
174
|
+
self.log_file = Path(log_file)
|
|
175
|
+
self.mode = "a" if append else "w"
|
|
176
|
+
|
|
177
|
+
# Create parent directory if needed
|
|
178
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
|
|
180
|
+
# Clear file if not appending
|
|
181
|
+
if not append:
|
|
182
|
+
self.log_file.write_text("")
|
|
183
|
+
|
|
184
|
+
def _write(self, message: str) -> None:
|
|
185
|
+
"""Write message to log file."""
|
|
186
|
+
with open(self.log_file, "a") as f:
|
|
187
|
+
f.write(message + "\n")
|
|
188
|
+
|
|
189
|
+
def print(self, message: str = "") -> None:
|
|
190
|
+
"""Print a regular message to file."""
|
|
191
|
+
self._write(message)
|
|
192
|
+
|
|
193
|
+
def print_header(self, title: str, width: int = 60) -> None:
|
|
194
|
+
"""Print a formatted section header to file."""
|
|
195
|
+
self._write("\n" + "=" * width)
|
|
196
|
+
self._write(title)
|
|
197
|
+
self._write("=" * width)
|
|
198
|
+
|
|
199
|
+
def print_success(self, message: str) -> None:
|
|
200
|
+
"""Print a success message to file."""
|
|
201
|
+
self._write(f"✓ {message}")
|
|
202
|
+
|
|
203
|
+
def print_error(self, message: str) -> None:
|
|
204
|
+
"""Print an error message to file."""
|
|
205
|
+
self._write(f"✗ {message}")
|
|
206
|
+
|
|
207
|
+
def print_warning(self, message: str) -> None:
|
|
208
|
+
"""Print a warning message to file."""
|
|
209
|
+
self._write(f"⚠ {message}")
|
|
210
|
+
|
|
211
|
+
def print_step(self, description: str, skipped: bool = False) -> None:
|
|
212
|
+
"""Print a step header to file."""
|
|
213
|
+
if skipped:
|
|
214
|
+
self._write(f"\n--- {description} [SKIPPED - already completed] ---")
|
|
215
|
+
else:
|
|
216
|
+
self._write(f"\n--- {description} ---")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
State Manager for Avatar Deployment Configuration
|
|
4
|
+
|
|
5
|
+
Manages deployment configuration state to support resuming interrupted configurations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeploymentState:
|
|
15
|
+
"""Manages the state of a deployment configuration."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, state_file: Path, steps: list[str]):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the state manager.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
state_file: Path to the state file
|
|
23
|
+
steps: List of step names to track. If None, uses DEFAULT_STEPS
|
|
24
|
+
"""
|
|
25
|
+
self.state_file = state_file
|
|
26
|
+
self.steps = steps
|
|
27
|
+
self.state = self._load_state()
|
|
28
|
+
|
|
29
|
+
def _load_state(self) -> dict[str, Any]:
|
|
30
|
+
"""Load state from file or create new state."""
|
|
31
|
+
if self.state_file.exists():
|
|
32
|
+
with open(self.state_file) as f:
|
|
33
|
+
return yaml.safe_load(f) or self._create_initial_state()
|
|
34
|
+
return self._create_initial_state()
|
|
35
|
+
|
|
36
|
+
def _create_initial_state(self) -> dict[str, Any]:
|
|
37
|
+
"""Create initial state structure."""
|
|
38
|
+
return {
|
|
39
|
+
"version": "1.0",
|
|
40
|
+
"steps": dict.fromkeys(self.steps, "not-started"),
|
|
41
|
+
"config": {},
|
|
42
|
+
"step_data": {}, # Store step-specific data
|
|
43
|
+
"user_secrets_provided": {}, # Track which user secrets were provided
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def save(self) -> None:
|
|
47
|
+
"""Save current state to file."""
|
|
48
|
+
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
with open(self.state_file, "w") as f:
|
|
50
|
+
yaml.dump(self.state, f, default_flow_style=False, sort_keys=False)
|
|
51
|
+
|
|
52
|
+
def get_step_status(self, step: str) -> str:
|
|
53
|
+
"""Get the status of a step."""
|
|
54
|
+
return self.state["steps"].get(step, "not-started")
|
|
55
|
+
|
|
56
|
+
def mark_step_started(self, step: str) -> None:
|
|
57
|
+
"""Mark a step as in-progress."""
|
|
58
|
+
self.state["steps"][step] = "in-progress"
|
|
59
|
+
self.save()
|
|
60
|
+
|
|
61
|
+
def mark_step_completed(self, step: str) -> None:
|
|
62
|
+
"""Mark a step as completed."""
|
|
63
|
+
self.state["steps"][step] = "completed"
|
|
64
|
+
self.save()
|
|
65
|
+
|
|
66
|
+
def is_step_completed(self, step: str) -> bool:
|
|
67
|
+
"""Check if a step is completed."""
|
|
68
|
+
return self.get_step_status(step) == "completed"
|
|
69
|
+
|
|
70
|
+
def get_next_step(self) -> str | None:
|
|
71
|
+
"""Get the next step that needs to be executed."""
|
|
72
|
+
for step in self.steps:
|
|
73
|
+
if not self.is_step_completed(step):
|
|
74
|
+
return step
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def update_config(self, config: dict[str, Any]) -> None:
|
|
78
|
+
"""Update configuration in state."""
|
|
79
|
+
self.state["config"].update(config)
|
|
80
|
+
self.save()
|
|
81
|
+
|
|
82
|
+
def get_config(self) -> dict[str, Any]:
|
|
83
|
+
"""Get current configuration from state."""
|
|
84
|
+
return self.state["config"].copy()
|
|
85
|
+
|
|
86
|
+
def mark_user_secret_provided(self, secret_name: str, provided: bool = True) -> None:
|
|
87
|
+
"""Mark whether a user secret was provided."""
|
|
88
|
+
self.state["user_secrets_provided"][secret_name] = provided
|
|
89
|
+
self.save()
|
|
90
|
+
|
|
91
|
+
def is_user_secret_provided(self, secret_name: str) -> bool:
|
|
92
|
+
"""Check if a user secret was provided."""
|
|
93
|
+
return self.state["user_secrets_provided"].get(secret_name, False)
|
|
94
|
+
|
|
95
|
+
def reset(self) -> None:
|
|
96
|
+
"""Reset state to initial values."""
|
|
97
|
+
self.state = self._create_initial_state()
|
|
98
|
+
self.save()
|
|
99
|
+
|
|
100
|
+
def delete(self) -> None:
|
|
101
|
+
"""Delete the state file."""
|
|
102
|
+
if self.state_file.exists():
|
|
103
|
+
self.state_file.unlink()
|
|
104
|
+
|
|
105
|
+
def has_started(self) -> bool:
|
|
106
|
+
"""Check if any configuration has been started."""
|
|
107
|
+
return any(status != "not-started" for status in self.state["steps"].values())
|
|
108
|
+
|
|
109
|
+
def is_complete(self) -> bool:
|
|
110
|
+
"""Check if all steps are completed."""
|
|
111
|
+
return all(status == "completed" for status in self.state["steps"].values())
|
|
112
|
+
|
|
113
|
+
def get_progress_summary(self) -> str:
|
|
114
|
+
"""Get a human-readable progress summary."""
|
|
115
|
+
completed = sum(1 for s in self.state["steps"].values() if s == "completed")
|
|
116
|
+
total = len(self.steps)
|
|
117
|
+
return f"{completed}/{total} steps completed"
|
|
118
|
+
|
|
119
|
+
def print_status(self) -> None:
|
|
120
|
+
"""Print current status."""
|
|
121
|
+
print("\n" + "=" * 60)
|
|
122
|
+
print("Deployment Configuration Status")
|
|
123
|
+
print("=" * 60)
|
|
124
|
+
print(f"\n{self.get_progress_summary()}\n")
|
|
125
|
+
|
|
126
|
+
for step in self.steps:
|
|
127
|
+
status = self.get_step_status(step)
|
|
128
|
+
icon = "✓" if status == "completed" else "○" if status == "not-started" else "◐"
|
|
129
|
+
print(f" {icon} {step.replace('_', ' ').title()}: {status}")
|
|
130
|
+
|
|
131
|
+
if self.is_complete():
|
|
132
|
+
print("\n✓ Configuration is complete!")
|
|
133
|
+
elif self.has_started():
|
|
134
|
+
next_step = self.get_next_step()
|
|
135
|
+
if next_step:
|
|
136
|
+
print(f"\n→ Next step: {next_step.replace('_', ' ').title()}")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Deployment configuration steps."""
|
|
2
|
+
|
|
3
|
+
from .authentik import AuthentikStep
|
|
4
|
+
from .authentik_blueprint import AuthentikBlueprintStep
|
|
5
|
+
from .base import DeploymentStep
|
|
6
|
+
from .database import DatabaseStep
|
|
7
|
+
from .email import EmailStep
|
|
8
|
+
from .logging import LoggingStep
|
|
9
|
+
from .required import RequiredConfigStep
|
|
10
|
+
from .storage import StorageStep
|
|
11
|
+
from .telemetry import TelemetryStep
|
|
12
|
+
from .user import UserStep
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DeploymentStep",
|
|
16
|
+
"RequiredConfigStep",
|
|
17
|
+
"EmailStep",
|
|
18
|
+
"TelemetryStep",
|
|
19
|
+
"LoggingStep",
|
|
20
|
+
"DatabaseStep",
|
|
21
|
+
"StorageStep",
|
|
22
|
+
"AuthentikStep",
|
|
23
|
+
"AuthentikBlueprintStep",
|
|
24
|
+
"UserStep",
|
|
25
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Authentik configuration step."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .base import DeploymentStep
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthentikStep(DeploymentStep):
|
|
10
|
+
"""Handles Authentik SSO configuration and credentials."""
|
|
11
|
+
|
|
12
|
+
name = "authentik"
|
|
13
|
+
description = "Configure Authentik SSO authentication credentials"
|
|
14
|
+
required = True
|
|
15
|
+
|
|
16
|
+
def collect_config(self) -> dict[str, Any]:
|
|
17
|
+
"""Collect Authentik configuration."""
|
|
18
|
+
config = {}
|
|
19
|
+
|
|
20
|
+
# Authentik database name and user
|
|
21
|
+
# These are typically set to default values unless customized
|
|
22
|
+
authentik_db_name = self.config.get(
|
|
23
|
+
"AUTHENTIK_DATABASE_NAME",
|
|
24
|
+
"authentik",
|
|
25
|
+
)
|
|
26
|
+
authentik_db_user = self.config.get(
|
|
27
|
+
"AUTHENTIK_DATABASE_USER",
|
|
28
|
+
"authentik",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
config["AUTHENTIK_DATABASE_NAME"] = authentik_db_name
|
|
32
|
+
config["AUTHENTIK_DATABASE_USER"] = authentik_db_user
|
|
33
|
+
|
|
34
|
+
# Update self.config so generate_secrets() can access these values
|
|
35
|
+
self.config.update(config)
|
|
36
|
+
|
|
37
|
+
return config
|
|
38
|
+
|
|
39
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
40
|
+
"""Generate Authentik-related secrets."""
|
|
41
|
+
return {
|
|
42
|
+
"authentik_database_name": self.config["AUTHENTIK_DATABASE_NAME"],
|
|
43
|
+
"authentik_database_user": self.config["AUTHENTIK_DATABASE_USER"],
|
|
44
|
+
"authentik_database_password": secrets.token_hex(),
|
|
45
|
+
"authentik_secret_key": secrets.token_hex(),
|
|
46
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Authentik blueprint configuration step."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .base import DeploymentStep
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthentikBlueprintStep(DeploymentStep):
|
|
10
|
+
"""Handles Authentik blueprint configuration for SSO setup.
|
|
11
|
+
|
|
12
|
+
The blueprint template (octopize-avatar-blueprint.yaml) is stored in
|
|
13
|
+
docker/templates/authentik/ and will be populated with these values.
|
|
14
|
+
|
|
15
|
+
This step derives all values from existing configuration without prompting:
|
|
16
|
+
- Domain from PUBLIC_URL
|
|
17
|
+
- Random OAuth2 client ID
|
|
18
|
+
- Redirect URI from domain
|
|
19
|
+
- Default license type
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name = "authentik-blueprint"
|
|
23
|
+
description = "Configure Authentik SSO blueprint settings"
|
|
24
|
+
required = True
|
|
25
|
+
|
|
26
|
+
def collect_config(self) -> dict[str, Any]:
|
|
27
|
+
"""Collect Authentik blueprint configuration.
|
|
28
|
+
|
|
29
|
+
All values are derived from existing config without user prompts.
|
|
30
|
+
"""
|
|
31
|
+
config = {}
|
|
32
|
+
|
|
33
|
+
# Extract domain from PUBLIC_URL (required config value)
|
|
34
|
+
public_url = self.config.get("PUBLIC_URL", "")
|
|
35
|
+
# Remove protocol and trailing slashes
|
|
36
|
+
domain = public_url.replace("https://", "").replace("http://", "").rstrip("/")
|
|
37
|
+
|
|
38
|
+
# Ensure domain is not empty
|
|
39
|
+
if not domain:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"PUBLIC_URL '{public_url}' is not set or invalid; cannot derive BLUEPRINT_DOMAIN."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
config["BLUEPRINT_DOMAIN"] = domain
|
|
45
|
+
|
|
46
|
+
# Generate random OAuth2 client ID (or use existing if provided)
|
|
47
|
+
client_id = self.config.get("BLUEPRINT_CLIENT_ID", secrets.token_hex(32))
|
|
48
|
+
config["BLUEPRINT_CLIENT_ID"] = client_id
|
|
49
|
+
|
|
50
|
+
# Generate OAuth2 client secret (or use existing if provided)
|
|
51
|
+
# This is a config value because it goes directly into the blueprint template
|
|
52
|
+
client_secret = self.config.get("BLUEPRINT_CLIENT_SECRET", secrets.token_hex(32))
|
|
53
|
+
config["BLUEPRINT_CLIENT_SECRET"] = client_secret
|
|
54
|
+
|
|
55
|
+
# Build API redirect URI from domain
|
|
56
|
+
redirect_uri = self.config.get(
|
|
57
|
+
"BLUEPRINT_API_REDIRECT_URI", f"https://{domain}/api/login/sso/auth"
|
|
58
|
+
)
|
|
59
|
+
config["BLUEPRINT_API_REDIRECT_URI"] = redirect_uri
|
|
60
|
+
|
|
61
|
+
# Use default license type (or from config)
|
|
62
|
+
license_type = self.config.get(
|
|
63
|
+
"BLUEPRINT_SELF_SERVICE_LICENSE",
|
|
64
|
+
"demo", # Default to demo license
|
|
65
|
+
)
|
|
66
|
+
config["BLUEPRINT_SELF_SERVICE_LICENSE"] = license_type
|
|
67
|
+
|
|
68
|
+
# Update self.config for generate_secrets()
|
|
69
|
+
self.config.update(config)
|
|
70
|
+
|
|
71
|
+
return config
|
|
72
|
+
|
|
73
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
74
|
+
"""Generate Authentik blueprint secrets.
|
|
75
|
+
|
|
76
|
+
The blueprint step doesn't generate any docker secrets.
|
|
77
|
+
All values are stored as config values instead.
|
|
78
|
+
"""
|
|
79
|
+
return {}
|