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,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for deployment configuration steps.
|
|
3
|
+
|
|
4
|
+
Each step is a modular component that handles configuration and secrets
|
|
5
|
+
for a specific part of the deployment (email, telemetry, storage, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import secrets
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from octopize_avatar_deploy.input_gatherer import InputGatherer
|
|
17
|
+
from octopize_avatar_deploy.printer import Printer
|
|
18
|
+
from octopize_avatar_deploy.input_gatherer import ConsoleInputGatherer
|
|
19
|
+
from octopize_avatar_deploy.printer import ConsolePrinter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DeploymentStep(ABC):
|
|
23
|
+
"""
|
|
24
|
+
Base class for all deployment configuration steps.
|
|
25
|
+
|
|
26
|
+
Each step handles a specific aspect of the deployment configuration.
|
|
27
|
+
Subclasses should implement collect_config() and generate_secrets().
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Step metadata
|
|
31
|
+
name: str = "base_step"
|
|
32
|
+
description: str = "Base configuration step"
|
|
33
|
+
required: bool = True # Whether this step is required
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
output_dir: Path,
|
|
38
|
+
defaults: dict[str, Any],
|
|
39
|
+
config: dict[str, Any] | None = None,
|
|
40
|
+
interactive: bool = True,
|
|
41
|
+
printer: "Printer | None" = None,
|
|
42
|
+
input_gatherer: "InputGatherer | None" = None,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the step.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
output_dir: Directory where files will be generated
|
|
49
|
+
defaults: Default configuration from defaults.yaml
|
|
50
|
+
config: Pre-loaded configuration (for non-interactive mode)
|
|
51
|
+
interactive: Whether to prompt user interactively
|
|
52
|
+
printer: Optional printer for output
|
|
53
|
+
"""
|
|
54
|
+
self.output_dir = Path(output_dir)
|
|
55
|
+
self.defaults = defaults
|
|
56
|
+
# Use the provided config dict directly (by reference) so all steps share the same dict
|
|
57
|
+
# This allows steps to access config values set by previous steps
|
|
58
|
+
if config is None:
|
|
59
|
+
config = {}
|
|
60
|
+
self.config: dict[str, Any] = config
|
|
61
|
+
self.secrets: dict[str, str] = {}
|
|
62
|
+
self.interactive = interactive
|
|
63
|
+
|
|
64
|
+
# Import here to avoid circular dependency
|
|
65
|
+
if printer is None:
|
|
66
|
+
printer = ConsolePrinter()
|
|
67
|
+
|
|
68
|
+
self.printer: Printer = printer
|
|
69
|
+
|
|
70
|
+
if input_gatherer is None:
|
|
71
|
+
input_gatherer = ConsoleInputGatherer()
|
|
72
|
+
|
|
73
|
+
self.input_gatherer: InputGatherer = input_gatherer
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def collect_config(self) -> dict[str, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Collect configuration for this step.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
dictionary of configuration values
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
87
|
+
"""
|
|
88
|
+
Generate secrets for this step.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
dictionary of {filename: secret_value} for .secrets/ directory
|
|
92
|
+
"""
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def can_skip(self) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Determine if this step can be skipped.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if step can be skipped (optional and user chooses to skip)
|
|
101
|
+
"""
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def validate(self) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Validate configuration before proceeding.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if validation passes
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValueError: If validation fails
|
|
113
|
+
"""
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def get_summary(self) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Get a summary of the configuration for review.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Human-readable summary string
|
|
122
|
+
"""
|
|
123
|
+
return f"{self.name}: Configured"
|
|
124
|
+
|
|
125
|
+
# Helper methods for prompting user input
|
|
126
|
+
|
|
127
|
+
def prompt(
|
|
128
|
+
self,
|
|
129
|
+
message: str,
|
|
130
|
+
default: str | None = None,
|
|
131
|
+
validate: "Callable[[str], tuple[bool, str]] | None" = None,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Prompt user for input with optional default value and validation.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
message: The prompt message
|
|
138
|
+
default: Default value to use if user presses Enter (None = required field)
|
|
139
|
+
validate: Optional validation function that returns (is_valid, error_message)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
User's input or default value
|
|
143
|
+
"""
|
|
144
|
+
return self.input_gatherer.prompt(message, default, validate)
|
|
145
|
+
|
|
146
|
+
def prompt_yes_no(self, message: str, default: bool = True) -> bool:
|
|
147
|
+
"""
|
|
148
|
+
Prompt user for yes/no input.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
message: The prompt message
|
|
152
|
+
default: Default value if user presses Enter
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
True for yes, False for no
|
|
156
|
+
"""
|
|
157
|
+
return self.input_gatherer.prompt_yes_no(message, default)
|
|
158
|
+
|
|
159
|
+
def prompt_choice(self, message: str, choices: list, default: str | None = None) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Prompt user to choose from list of options.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
message: The prompt message
|
|
165
|
+
choices: List of valid choices
|
|
166
|
+
default: Default choice if user presses Enter
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Selected choice
|
|
170
|
+
"""
|
|
171
|
+
return self.input_gatherer.prompt_choice(message, choices, default)
|
|
172
|
+
|
|
173
|
+
def get_config_value(self, key: str, default: Any = None) -> Any:
|
|
174
|
+
ret = self.config.get(key, default)
|
|
175
|
+
if ret is None:
|
|
176
|
+
raise ValueError(f"Configuration key '{key}' is required but not set.")
|
|
177
|
+
return ret
|
|
178
|
+
|
|
179
|
+
# Utility methods for generating secrets
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def generate_secret_token() -> str:
|
|
183
|
+
"""Generate a secure random token (hex-encoded)."""
|
|
184
|
+
return secrets.token_hex()
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def generate_secret_urlsafe(nbytes: int = 32) -> str:
|
|
188
|
+
"""Generate a URL-safe random token."""
|
|
189
|
+
return secrets.token_urlsafe(nbytes)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def generate_encryption_key() -> str:
|
|
193
|
+
"""Generate a URL-safe base64-encoded encryption key."""
|
|
194
|
+
return base64.urlsafe_b64encode(secrets.token_bytes(32)).decode("utf-8")
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def generate_base64_key(nbytes: int = 32) -> str:
|
|
198
|
+
"""Generate a base64-encoded random key."""
|
|
199
|
+
return base64.b64encode(secrets.token_bytes(nbytes)).decode("utf-8")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Database configuration step."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .base import DeploymentStep
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatabaseStep(DeploymentStep):
|
|
10
|
+
"""Handles database configuration and credentials."""
|
|
11
|
+
|
|
12
|
+
name = "database"
|
|
13
|
+
description = "Configure PostgreSQL database credentials"
|
|
14
|
+
required = True
|
|
15
|
+
|
|
16
|
+
def collect_config(self) -> dict[str, Any]:
|
|
17
|
+
"""Collect database configuration."""
|
|
18
|
+
config = {}
|
|
19
|
+
|
|
20
|
+
config["DB_NAME"] = self.get_config_value("DB_NAME", "avatar")
|
|
21
|
+
config["DB_USER"] = self.get_config_value("DB_USER", "avatar")
|
|
22
|
+
config["DB_ADMIN_USER"] = self.get_config_value("DB_ADMIN_USER", "avatar_dba")
|
|
23
|
+
|
|
24
|
+
# Update self.config so generate_secrets() can access these values
|
|
25
|
+
self.config.update(config)
|
|
26
|
+
|
|
27
|
+
return config
|
|
28
|
+
|
|
29
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
30
|
+
"""Generate database passwords and configuration."""
|
|
31
|
+
return {
|
|
32
|
+
"db_password": secrets.token_hex(),
|
|
33
|
+
"db_admin_password": secrets.token_hex(),
|
|
34
|
+
"db_admin_user": self.config["DB_ADMIN_USER"],
|
|
35
|
+
"db_user": self.config["DB_USER"],
|
|
36
|
+
"db_name": self.config["DB_NAME"],
|
|
37
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Email configuration step."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .base import DeploymentStep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EmailStep(DeploymentStep):
|
|
9
|
+
"""Handles email configuration and credentials."""
|
|
10
|
+
|
|
11
|
+
name = "email"
|
|
12
|
+
description = "Configure email provider (AWS SES or SMTP) and credentials"
|
|
13
|
+
required = True
|
|
14
|
+
|
|
15
|
+
def collect_config(self) -> dict[str, Any]:
|
|
16
|
+
"""Collect email configuration."""
|
|
17
|
+
config = {}
|
|
18
|
+
|
|
19
|
+
print("\n--- Email Configuration ---")
|
|
20
|
+
|
|
21
|
+
# Email provider
|
|
22
|
+
default_provider = self.defaults["email"]["provider"]
|
|
23
|
+
provider = self.config.get(
|
|
24
|
+
"MAIL_PROVIDER",
|
|
25
|
+
self.prompt("Mail provider (aws or smtp)", default_provider)
|
|
26
|
+
if self.interactive
|
|
27
|
+
else default_provider,
|
|
28
|
+
).lower()
|
|
29
|
+
|
|
30
|
+
config["MAIL_PROVIDER"] = provider
|
|
31
|
+
|
|
32
|
+
# SMTP configuration
|
|
33
|
+
if provider == "smtp":
|
|
34
|
+
smtp_defaults = self.defaults["email"]["smtp"]
|
|
35
|
+
|
|
36
|
+
config["SMTP_HOST"] = self.config.get(
|
|
37
|
+
"SMTP_HOST",
|
|
38
|
+
self.prompt("SMTP host", smtp_defaults["host"])
|
|
39
|
+
if self.interactive
|
|
40
|
+
else smtp_defaults["host"],
|
|
41
|
+
)
|
|
42
|
+
config["SMTP_PORT"] = self.config.get(
|
|
43
|
+
"SMTP_PORT",
|
|
44
|
+
self.prompt("SMTP port", str(smtp_defaults["port"]))
|
|
45
|
+
if self.interactive
|
|
46
|
+
else str(smtp_defaults["port"]),
|
|
47
|
+
)
|
|
48
|
+
config["SMTP_USE_TLS"] = self.config.get("SMTP_USE_TLS", smtp_defaults["use_tls"])
|
|
49
|
+
config["SMTP_START_TLS"] = self.config.get("SMTP_START_TLS", smtp_defaults["start_tls"])
|
|
50
|
+
config["SMTP_VERIFY"] = self.config.get("SMTP_VERIFY", smtp_defaults["verify"])
|
|
51
|
+
config["SMTP_SENDER_EMAIL"] = self.config.get(
|
|
52
|
+
"SMTP_SENDER_EMAIL",
|
|
53
|
+
self.prompt("SMTP sender email", smtp_defaults["sender_email"])
|
|
54
|
+
if self.interactive
|
|
55
|
+
else smtp_defaults["sender_email"],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Email authentication
|
|
59
|
+
config["USE_EMAIL_AUTHENTICATION"] = self.config.get(
|
|
60
|
+
"USE_EMAIL_AUTHENTICATION",
|
|
61
|
+
self.defaults["application"]["email_authentication"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self.config.update(config)
|
|
65
|
+
|
|
66
|
+
return config
|
|
67
|
+
|
|
68
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
69
|
+
"""Generate email-related secrets."""
|
|
70
|
+
secrets_dict = {}
|
|
71
|
+
|
|
72
|
+
# SMTP password (if using SMTP)
|
|
73
|
+
if self.config["MAIL_PROVIDER"] == "smtp":
|
|
74
|
+
if self.interactive:
|
|
75
|
+
smtp_password = self.input_gatherer.prompt(
|
|
76
|
+
"SMTP password (press Enter to skip)", default=""
|
|
77
|
+
)
|
|
78
|
+
if smtp_password:
|
|
79
|
+
secrets_dict["smtp_password"] = smtp_password
|
|
80
|
+
|
|
81
|
+
# AWS SES credentials (if using AWS)
|
|
82
|
+
elif self.config["MAIL_PROVIDER"] == "aws":
|
|
83
|
+
# Generate empty placeholder files for AWS credentials
|
|
84
|
+
# These will be provided by Octopize to the user
|
|
85
|
+
secrets_dict["aws_mail_account_access_key_id"] = ""
|
|
86
|
+
secrets_dict["aws_mail_account_secret_access_key"] = ""
|
|
87
|
+
|
|
88
|
+
return secrets_dict
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Logging configuration step."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .base import DeploymentStep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LoggingStep(DeploymentStep):
|
|
9
|
+
"""Handles application logging configuration."""
|
|
10
|
+
|
|
11
|
+
name = "logging"
|
|
12
|
+
description = "Configure application logging settings"
|
|
13
|
+
required = False
|
|
14
|
+
|
|
15
|
+
def collect_config(self) -> dict[str, Any]:
|
|
16
|
+
"""Collect logging configuration."""
|
|
17
|
+
config = {}
|
|
18
|
+
|
|
19
|
+
# Console logging
|
|
20
|
+
if "USE_CONSOLE_LOGGING" not in self.config:
|
|
21
|
+
config["USE_CONSOLE_LOGGING"] = self.defaults["application"]["use_console_logging"]
|
|
22
|
+
|
|
23
|
+
# Log level
|
|
24
|
+
config["LOG_LEVEL"] = self.config.get(
|
|
25
|
+
"LOG_LEVEL", self.defaults["application"]["log_level"]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return config
|
|
29
|
+
|
|
30
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
31
|
+
"""No secrets needed for logging."""
|
|
32
|
+
return {}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Required configuration step - collects mandatory deployment settings."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .base import DeploymentStep
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RequiredConfigStep(DeploymentStep):
|
|
10
|
+
"""Collects required configuration that must be provided."""
|
|
11
|
+
|
|
12
|
+
name = "required_config"
|
|
13
|
+
description = "Collect required deployment settings (PUBLIC_URL, ENV_NAME, etc.)"
|
|
14
|
+
required = True
|
|
15
|
+
|
|
16
|
+
def collect_config(self) -> dict[str, Any]:
|
|
17
|
+
"""Collect required configuration."""
|
|
18
|
+
config = {}
|
|
19
|
+
|
|
20
|
+
# Public URL - Required
|
|
21
|
+
if "PUBLIC_URL" in self.config:
|
|
22
|
+
config["PUBLIC_URL"] = self.config["PUBLIC_URL"]
|
|
23
|
+
elif self.interactive:
|
|
24
|
+
config["PUBLIC_URL"] = self.prompt("Public URL (domain name, e.g., avatar.example.com)")
|
|
25
|
+
else:
|
|
26
|
+
config["PUBLIC_URL"] = ""
|
|
27
|
+
|
|
28
|
+
# Environment name - Required
|
|
29
|
+
if "ENV_NAME" in self.config:
|
|
30
|
+
config["ENV_NAME"] = self.config["ENV_NAME"]
|
|
31
|
+
elif self.interactive:
|
|
32
|
+
config["ENV_NAME"] = self.prompt("Environment name (e.g., mycompany-prod)")
|
|
33
|
+
else:
|
|
34
|
+
config["ENV_NAME"] = ""
|
|
35
|
+
|
|
36
|
+
# Organization name - Required
|
|
37
|
+
if "ORGANIZATION_NAME" in self.config:
|
|
38
|
+
config["ORGANIZATION_NAME"] = self.config["ORGANIZATION_NAME"]
|
|
39
|
+
elif self.interactive:
|
|
40
|
+
while True:
|
|
41
|
+
org_name = self.prompt("Organization name (e.g., MyCompany)")
|
|
42
|
+
if org_name.strip():
|
|
43
|
+
config["ORGANIZATION_NAME"] = org_name
|
|
44
|
+
break
|
|
45
|
+
self.printer.print_error("Organization name is required and cannot be empty")
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"ORGANIZATION_NAME is required but not provided in configuration file. "
|
|
49
|
+
"Please add ORGANIZATION_NAME to your config or run in interactive mode."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Service versions
|
|
53
|
+
config["AVATAR_API_VERSION"] = self.config.get(
|
|
54
|
+
"AVATAR_API_VERSION", self.defaults["images"]["api"]
|
|
55
|
+
)
|
|
56
|
+
config["AVATAR_WEB_VERSION"] = self.config.get(
|
|
57
|
+
"AVATAR_WEB_VERSION", self.defaults["images"]["web"]
|
|
58
|
+
)
|
|
59
|
+
config["AVATAR_PDFGENERATOR_VERSION"] = self.config.get(
|
|
60
|
+
"AVATAR_PDFGENERATOR_VERSION", self.defaults["images"]["pdfgenerator"]
|
|
61
|
+
)
|
|
62
|
+
config["AVATAR_SEAWEEDFS_VERSION"] = self.config.get(
|
|
63
|
+
"AVATAR_SEAWEEDFS_VERSION", self.defaults["images"]["seaweedfs"]
|
|
64
|
+
)
|
|
65
|
+
config["AVATAR_AUTHENTIK_VERSION"] = self.config.get(
|
|
66
|
+
"AVATAR_AUTHENTIK_VERSION", self.defaults["images"]["authentik"]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Update self.config so generate_secrets() can access these values
|
|
70
|
+
self.config.update(config)
|
|
71
|
+
|
|
72
|
+
return config
|
|
73
|
+
|
|
74
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
75
|
+
"""Generate required API secrets."""
|
|
76
|
+
return {
|
|
77
|
+
"pepper": secrets.token_hex(),
|
|
78
|
+
"authjwt_secret_key": secrets.token_hex(),
|
|
79
|
+
"organization_name": self.config["ORGANIZATION_NAME"],
|
|
80
|
+
"clevercloud_sso_salt": secrets.token_hex(),
|
|
81
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Storage configuration step."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .base import DeploymentStep
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StorageStep(DeploymentStep):
|
|
10
|
+
"""Handles storage configuration and credentials."""
|
|
11
|
+
|
|
12
|
+
name = "storage"
|
|
13
|
+
description = "Configure S3-compatible storage (SeaweedFS) credentials"
|
|
14
|
+
required = True
|
|
15
|
+
|
|
16
|
+
def collect_config(self) -> dict[str, Any]:
|
|
17
|
+
"""Collect storage configuration."""
|
|
18
|
+
config: dict[str, Any] = {}
|
|
19
|
+
|
|
20
|
+
# Storage configuration is mostly handled via secrets
|
|
21
|
+
# No interactive prompts needed for basic setup
|
|
22
|
+
|
|
23
|
+
return config
|
|
24
|
+
|
|
25
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
26
|
+
"""Generate storage-related secrets."""
|
|
27
|
+
return {
|
|
28
|
+
"file_jwt_secret_key": secrets.token_hex(),
|
|
29
|
+
"file_encryption_key": self.generate_encryption_key(),
|
|
30
|
+
"storage_admin_access_key_id": secrets.token_hex(),
|
|
31
|
+
"storage_admin_secret_access_key": secrets.token_hex(),
|
|
32
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Telemetry and monitoring configuration step."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .base import DeploymentStep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TelemetryStep(DeploymentStep):
|
|
9
|
+
"""Handles telemetry and Sentry monitoring configuration."""
|
|
10
|
+
|
|
11
|
+
name = "telemetry"
|
|
12
|
+
description = "Configure telemetry and monitoring (Sentry, usage analytics)"
|
|
13
|
+
required = False
|
|
14
|
+
|
|
15
|
+
def collect_config(self) -> dict[str, Any]:
|
|
16
|
+
"""Collect telemetry configuration."""
|
|
17
|
+
config = {}
|
|
18
|
+
|
|
19
|
+
sentry_enabled = (
|
|
20
|
+
self.prompt_yes_no(
|
|
21
|
+
"Enable Sentry error monitoring?",
|
|
22
|
+
default=self.defaults["application"]["sentry_enabled"] == "true",
|
|
23
|
+
)
|
|
24
|
+
if self.interactive
|
|
25
|
+
else self.defaults["application"]["sentry_enabled"] == "true"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
config["IS_SENTRY_ENABLED"] = "true" if sentry_enabled else "false"
|
|
29
|
+
|
|
30
|
+
enable_telemetry = (
|
|
31
|
+
self.prompt_yes_no(
|
|
32
|
+
"Enable usage telemetry?",
|
|
33
|
+
default=self.defaults["telemetry"]["enabled"],
|
|
34
|
+
)
|
|
35
|
+
if self.interactive
|
|
36
|
+
else self.defaults["telemetry"]["enabled"]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if enable_telemetry:
|
|
40
|
+
config["TELEMETRY_S3_ENDPOINT_URL"] = self.defaults["telemetry"]["endpoint_url"]
|
|
41
|
+
config["TELEMETRY_S3_REGION"] = self.defaults["telemetry"]["region"]
|
|
42
|
+
else:
|
|
43
|
+
config["TELEMETRY_S3_ENDPOINT_URL"] = ""
|
|
44
|
+
config["TELEMETRY_S3_REGION"] = ""
|
|
45
|
+
|
|
46
|
+
self.config.update(config)
|
|
47
|
+
|
|
48
|
+
return config
|
|
49
|
+
|
|
50
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
51
|
+
"""Generate telemetry-related secrets."""
|
|
52
|
+
# Only generate secrets if telemetry is enabled
|
|
53
|
+
if self.config["TELEMETRY_S3_ENDPOINT_URL"]:
|
|
54
|
+
return {
|
|
55
|
+
"telemetry_s3_access_key_id": "",
|
|
56
|
+
"telemetry_s3_secret_access_key": "",
|
|
57
|
+
}
|
|
58
|
+
return {}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""User authentication configuration step."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .base import DeploymentStep
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_comma_separated_emails(value: str) -> tuple[bool, str]:
|
|
10
|
+
"""
|
|
11
|
+
Validate comma-separated email addresses.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
value: String containing comma-separated email addresses
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Tuple of (is_valid, error_message)
|
|
18
|
+
"""
|
|
19
|
+
if not value.strip():
|
|
20
|
+
# Empty is allowed (optional field)
|
|
21
|
+
return True, ""
|
|
22
|
+
|
|
23
|
+
# Simple email regex pattern
|
|
24
|
+
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
25
|
+
|
|
26
|
+
emails = [email.strip() for email in value.split(",")]
|
|
27
|
+
|
|
28
|
+
for email in emails:
|
|
29
|
+
if not email:
|
|
30
|
+
return False, "Empty email address found in list"
|
|
31
|
+
if not re.match(email_pattern, email):
|
|
32
|
+
return False, f"Invalid email address: {email}"
|
|
33
|
+
|
|
34
|
+
return True, ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UserStep(DeploymentStep):
|
|
38
|
+
"""Handles user authentication configuration."""
|
|
39
|
+
|
|
40
|
+
name = "user"
|
|
41
|
+
description = "Configure user authentication settings"
|
|
42
|
+
required = True
|
|
43
|
+
|
|
44
|
+
def collect_config(self) -> dict[str, Any]:
|
|
45
|
+
"""Collect user authentication configuration."""
|
|
46
|
+
config = {}
|
|
47
|
+
|
|
48
|
+
# Check if email authentication is enabled
|
|
49
|
+
# Get from config, or fallback to defaults
|
|
50
|
+
use_email_auth = self.config.get(
|
|
51
|
+
"USE_EMAIL_AUTHENTICATION",
|
|
52
|
+
self.defaults.get("application", {}).get("email_authentication", True),
|
|
53
|
+
)
|
|
54
|
+
# Convert to string for comparison (defaults might be bool)
|
|
55
|
+
use_email_auth_str = str(use_email_auth).lower()
|
|
56
|
+
|
|
57
|
+
if use_email_auth_str == "true":
|
|
58
|
+
if self.interactive:
|
|
59
|
+
admin_emails = self.prompt(
|
|
60
|
+
"Admin email addresses (comma-separated)",
|
|
61
|
+
default="",
|
|
62
|
+
validate=validate_comma_separated_emails,
|
|
63
|
+
)
|
|
64
|
+
config["ADMIN_EMAILS"] = admin_emails
|
|
65
|
+
else:
|
|
66
|
+
config["ADMIN_EMAILS"] = self.config.get("ADMIN_EMAILS", "")
|
|
67
|
+
|
|
68
|
+
self.config.update(config)
|
|
69
|
+
|
|
70
|
+
return config
|
|
71
|
+
|
|
72
|
+
def generate_secrets(self) -> dict[str, str]:
|
|
73
|
+
"""Generate user-related secrets."""
|
|
74
|
+
secrets_dict = {}
|
|
75
|
+
|
|
76
|
+
# Admin emails for email-based authentication
|
|
77
|
+
# USE_EMAIL_AUTHENTICATION comes from EmailStep, so use .get() with fallback
|
|
78
|
+
use_email_auth = self.config.get(
|
|
79
|
+
"USE_EMAIL_AUTHENTICATION",
|
|
80
|
+
self.defaults.get("application", {}).get("email_authentication", "true"),
|
|
81
|
+
)
|
|
82
|
+
# Convert to string for comparison
|
|
83
|
+
use_email_auth_str = str(use_email_auth).lower()
|
|
84
|
+
|
|
85
|
+
if use_email_auth_str == "true":
|
|
86
|
+
admin_emails = self.config.get("ADMIN_EMAILS", "")
|
|
87
|
+
secrets_dict["admin_emails"] = admin_emails
|
|
88
|
+
|
|
89
|
+
return secrets_dict
|