Skip to content

Commit a3647c6

Browse files
authored
feat: setup development environment with cli installer (#508)
1 parent de27f30 commit a3647c6

File tree

12 files changed

+1198
-288
lines changed

12 files changed

+1198
-288
lines changed

api/Dockerfile.dev

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM golang:alpine
2+
ARG API_PORT=8443
3+
4+
# Install air for live reload
5+
RUN apk add --no-cache bash git curl make && \
6+
go install github.com/air-verse/air@latest
7+
8+
WORKDIR /app
9+
10+
# Pre-download dependencies
11+
COPY go.mod go.sum ./
12+
RUN go mod download
13+
COPY . .
14+
15+
# Source code is mounted via volume
16+
ENV PORT=${API_PORT}
17+
EXPOSE ${API_PORT}
18+
19+
# Use air with default config for live reload
20+
CMD ["air"]
21+

api/doc/openapi.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

cli/app/commands/install/base.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
import re
3+
4+
from app.utils.config import Config
5+
from app.utils.protocols import LoggerProtocol
6+
7+
from .messages import configuration_key_has_no_default_value
8+
9+
10+
class BaseInstall:
11+
"""Base class with shared logic for both production and development installations"""
12+
13+
def __init__(
14+
self,
15+
logger: LoggerProtocol = None,
16+
verbose: bool = False,
17+
timeout: int = 300,
18+
force: bool = False,
19+
dry_run: bool = False,
20+
config_file: str = None,
21+
repo: str = None,
22+
branch: str = None,
23+
):
24+
self.logger = logger
25+
self.verbose = verbose
26+
self.timeout = timeout
27+
self.force = force
28+
self.dry_run = dry_run
29+
self.config_file = config_file
30+
self.repo = repo
31+
self.branch = branch
32+
self._user_config = Config().load_user_config(self.config_file)
33+
self.progress = None
34+
self.main_task = None
35+
36+
def _get_config(self, key: str, user_config: dict = None, defaults: dict = None):
37+
"""Base config getter - override in subclasses for specific behavior"""
38+
config = Config()
39+
40+
# Override repo_url and branch_name if provided via command line
41+
if key == "repo_url" and self.repo is not None:
42+
return self.repo
43+
if key == "branch_name" and self.branch is not None:
44+
return self.branch
45+
46+
try:
47+
return config.get_config_value(key, user_config or self._user_config, defaults or {})
48+
except ValueError:
49+
raise ValueError(configuration_key_has_no_default_value.format(key=key))
50+
51+
def _validate_domains(self, api_domain: str = None, view_domain: str = None):
52+
"""Validate domain format"""
53+
if (api_domain is None) != (view_domain is None):
54+
raise ValueError("Both api_domain and view_domain must be provided together, or neither should be provided")
55+
56+
if api_domain and view_domain:
57+
domain_pattern = re.compile(
58+
r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?))*$"
59+
)
60+
if not domain_pattern.match(api_domain) or not domain_pattern.match(view_domain):
61+
raise ValueError("Invalid domain format. Domains must be valid hostnames")
62+
63+
def _validate_repo(self):
64+
"""Validate repository URL format"""
65+
if self.repo:
66+
if not (
67+
self.repo.startswith(("http://", "https://", "git://", "ssh://"))
68+
or (self.repo.endswith(".git") and not self.repo.startswith("github.com:"))
69+
or ("@" in self.repo and ":" in self.repo and self.repo.count("@") == 1)
70+
):
71+
raise ValueError("Invalid repository URL format")
72+
73+
def _is_custom_repo_or_branch(self, default_repo: str, default_branch: str):
74+
"""Check if custom repository or branch is provided"""
75+
repo_differs = self.repo is not None and self.repo != default_repo
76+
branch_differs = self.branch is not None and self.branch != default_branch
77+
return repo_differs or branch_differs
78+

cli/app/commands/install/command.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from .deps import install_all_deps
88
from .run import Install
9+
from .development import DevelopmentInstall
910
from .ssh import SSH, SSHConfig
1011

1112
install_app = typer.Typer(help="Install Nixopus", invoke_without_command=True)
@@ -40,7 +41,7 @@ def install_callback(
4041
None, "--branch", "-b", help="Git branch to clone (defaults to config value)"
4142
),
4243
):
43-
"""Install Nixopus"""
44+
"""Install Nixopus for production"""
4445
if ctx.invoked_subcommand is None:
4546
logger = Logger(verbose=verbose)
4647
install = Install(
@@ -54,6 +55,7 @@ def install_callback(
5455
view_domain=view_domain,
5556
repo=repo,
5657
branch=branch,
58+
development=False,
5759
)
5860
install.run()
5961

@@ -66,6 +68,39 @@ def main_install_callback(value: bool):
6668
raise typer.Exit()
6769

6870

71+
@install_app.command(name="development")
72+
def development(
73+
path: str = typer.Option(None, "--path", "-p", help="Installation directory (defaults to current directory)"),
74+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show more details while installing"),
75+
timeout: int = typer.Option(300, "--timeout", "-t", help="How long to wait for each step (in seconds)"),
76+
force: bool = typer.Option(False, "--force", "-f", help="Replace files if they already exist"),
77+
dry_run: bool = typer.Option(False, "--dry-run", "-d", help="See what would happen, but don't make changes"),
78+
config_file: str = typer.Option(
79+
None, "--config-file", "-c", help="Path to custom config file (defaults to config.dev.yaml)"
80+
),
81+
repo: str = typer.Option(
82+
None, "--repo", "-r", help="GitHub repository URL to clone (defaults to config value)"
83+
),
84+
branch: str = typer.Option(
85+
None, "--branch", "-b", help="Git branch to clone (defaults to config value)"
86+
),
87+
):
88+
"""Install Nixopus for local development in specified or current directory"""
89+
logger = Logger(verbose=verbose)
90+
install = DevelopmentInstall(
91+
logger=logger,
92+
verbose=verbose,
93+
timeout=timeout,
94+
force=force,
95+
dry_run=dry_run,
96+
config_file=config_file,
97+
repo=repo,
98+
branch=branch,
99+
install_path=path,
100+
)
101+
install.run()
102+
103+
69104
@install_app.command(name="ssh")
70105
def ssh(
71106
path: str = typer.Option("~/.ssh/nixopus_rsa", "--path", "-p", help="The SSH key path to generate"),

0 commit comments

Comments
 (0)