Skip to content

Commit 15f000a

Browse files
author
Datata1
committed
feat(landscape): add landscape resource
1 parent cbce32e commit 15f000a

File tree

10 files changed

+2001
-4
lines changed

10 files changed

+2001
-4
lines changed

src/codesphere/resources/workspace/git/models.py

Whitespace-only changes.

src/codesphere/resources/workspace/git/schema.py

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from .models import WorkspaceLandscapeManager
2+
from .schemas import (
3+
ManagedServiceBuilder,
4+
ManagedServiceConfig,
5+
NetworkConfig,
6+
PathConfig,
7+
PortConfig,
8+
Profile,
9+
ProfileBuilder,
10+
ProfileConfig,
11+
ReactiveServiceBuilder,
12+
ReactiveServiceConfig,
13+
StageConfig,
14+
Step,
15+
)
16+
17+
__all__ = [
18+
"WorkspaceLandscapeManager",
19+
"Profile",
20+
"ProfileBuilder",
21+
"ProfileConfig",
22+
"Step",
23+
"StageConfig",
24+
"ReactiveServiceConfig",
25+
"ReactiveServiceBuilder",
26+
"ManagedServiceConfig",
27+
"ManagedServiceBuilder",
28+
"NetworkConfig",
29+
"PortConfig",
30+
"PathConfig",
31+
]
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import re
5+
from typing import TYPE_CHECKING, Dict, List, Optional, Union
6+
7+
from ....core.base import ResourceList
8+
from ....core.handler import _APIOperationExecutor
9+
from ....http_client import APIHttpClient
10+
from .operations import (
11+
_DEPLOY_OP,
12+
_DEPLOY_WITH_PROFILE_OP,
13+
_SCALE_OP,
14+
_TEARDOWN_OP,
15+
)
16+
from .schemas import Profile, ProfileConfig
17+
18+
if TYPE_CHECKING:
19+
from ..schemas import CommandOutput
20+
21+
log = logging.getLogger(__name__)
22+
23+
# Regex pattern to match ci.<profile>.yml files
24+
_PROFILE_FILE_PATTERN = re.compile(r"^ci\.([A-Za-z0-9_-]+)\.yml$")
25+
# Pattern for valid profile names
26+
_VALID_PROFILE_NAME = re.compile(r"^[A-Za-z0-9_-]+$")
27+
28+
29+
class WorkspaceLandscapeManager(_APIOperationExecutor):
30+
"""Manager for workspace landscape operations (Multi Server Deployments)."""
31+
32+
def __init__(self, http_client: APIHttpClient, workspace_id: int):
33+
self._http_client = http_client
34+
self._workspace_id = workspace_id
35+
self.id = workspace_id
36+
37+
async def list_profiles(self) -> ResourceList[Profile]:
38+
"""List all available deployment profiles in the workspace.
39+
40+
Profiles are discovered by listing files matching the pattern ci.<profile>.yml
41+
in the workspace root directory.
42+
43+
Returns:
44+
ResourceList of Profile objects.
45+
"""
46+
from ..operations import _EXECUTE_COMMAND_OP
47+
from ..schemas import CommandInput
48+
49+
command_data = CommandInput(command="ls -1 *.yml 2>/dev/null || true")
50+
result: CommandOutput = await self._execute_operation(
51+
_EXECUTE_COMMAND_OP, data=command_data
52+
)
53+
54+
profiles: List[Profile] = []
55+
if result.output:
56+
for line in result.output.strip().split("\n"):
57+
line = line.strip()
58+
if match := _PROFILE_FILE_PATTERN.match(line):
59+
profile_name = match.group(1)
60+
profiles.append(Profile(name=profile_name))
61+
62+
return ResourceList[Profile](root=profiles)
63+
64+
async def save_profile(self, name: str, config: Union[ProfileConfig, str]) -> None:
65+
"""Save a profile configuration to the workspace.
66+
67+
Args:
68+
name: Profile name (must match pattern ^[A-Za-z0-9_-]+$).
69+
config: ProfileConfig instance or YAML string.
70+
71+
Raises:
72+
ValueError: If the profile name is invalid.
73+
"""
74+
from ..operations import _EXECUTE_COMMAND_OP
75+
from ..schemas import CommandInput
76+
77+
if not _VALID_PROFILE_NAME.match(name):
78+
raise ValueError(
79+
f"Invalid profile name '{name}'. Must match pattern ^[A-Za-z0-9_-]+$"
80+
)
81+
82+
# Convert ProfileConfig to YAML if needed
83+
if isinstance(config, ProfileConfig):
84+
yaml_content = config.to_yaml()
85+
else:
86+
yaml_content = config
87+
88+
# Escape single quotes in YAML content for shell
89+
escaped_content = yaml_content.replace("'", "'\"'\"'")
90+
91+
# Write the profile file
92+
filename = f"ci.{name}.yml"
93+
command = f"cat > {filename} << 'PROFILE_EOF'\n{yaml_content}PROFILE_EOF"
94+
95+
command_data = CommandInput(command=command)
96+
await self._execute_operation(_EXECUTE_COMMAND_OP, data=command_data)
97+
98+
async def get_profile(self, name: str) -> str:
99+
"""Get the raw YAML content of a profile.
100+
101+
Args:
102+
name: Profile name.
103+
104+
Returns:
105+
YAML content of the profile as a string.
106+
107+
Raises:
108+
ValueError: If the profile name is invalid.
109+
"""
110+
from ..operations import _EXECUTE_COMMAND_OP
111+
from ..schemas import CommandInput
112+
113+
if not _VALID_PROFILE_NAME.match(name):
114+
raise ValueError(
115+
f"Invalid profile name '{name}'. Must match pattern ^[A-Za-z0-9_-]+$"
116+
)
117+
118+
filename = f"ci.{name}.yml"
119+
command_data = CommandInput(command=f"cat {filename}")
120+
result: CommandOutput = await self._execute_operation(
121+
_EXECUTE_COMMAND_OP, data=command_data
122+
)
123+
124+
return result.output
125+
126+
async def delete_profile(self, name: str) -> None:
127+
"""Delete a profile from the workspace.
128+
129+
Args:
130+
name: Profile name to delete.
131+
132+
Raises:
133+
ValueError: If the profile name is invalid.
134+
"""
135+
from ..operations import _EXECUTE_COMMAND_OP
136+
from ..schemas import CommandInput
137+
138+
if not _VALID_PROFILE_NAME.match(name):
139+
raise ValueError(
140+
f"Invalid profile name '{name}'. Must match pattern ^[A-Za-z0-9_-]+$"
141+
)
142+
143+
filename = f"ci.{name}.yml"
144+
command_data = CommandInput(command=f"rm -f {filename}")
145+
await self._execute_operation(_EXECUTE_COMMAND_OP, data=command_data)
146+
147+
async def deploy(self, profile: Optional[str] = None) -> None:
148+
"""Deploy the landscape.
149+
150+
Args:
151+
profile: Optional deployment profile name (must match pattern ^[A-Za-z0-9-_]+$).
152+
"""
153+
if profile is not None:
154+
await self._execute_operation(_DEPLOY_WITH_PROFILE_OP, profile=profile)
155+
else:
156+
await self._execute_operation(_DEPLOY_OP)
157+
158+
async def teardown(self) -> None:
159+
"""Teardown the landscape."""
160+
await self._execute_operation(_TEARDOWN_OP)
161+
162+
async def scale(self, services: Dict[str, int]) -> None:
163+
"""Scale landscape services.
164+
165+
Args:
166+
services: A dictionary mapping service names to replica counts (minimum 1).
167+
"""
168+
await self._execute_operation(_SCALE_OP, data=services)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from ....core.operations import APIOperation
2+
3+
_DEPLOY_OP = APIOperation(
4+
method="POST",
5+
endpoint_template="/workspaces/{id}/landscape/deploy",
6+
response_model=type(None),
7+
)
8+
9+
_DEPLOY_WITH_PROFILE_OP = APIOperation(
10+
method="POST",
11+
endpoint_template="/workspaces/{id}/landscape/deploy/{profile}",
12+
response_model=type(None),
13+
)
14+
15+
_TEARDOWN_OP = APIOperation(
16+
method="DELETE",
17+
endpoint_template="/workspaces/{id}/landscape/teardown",
18+
response_model=type(None),
19+
)
20+
21+
_SCALE_OP = APIOperation(
22+
method="PATCH",
23+
endpoint_template="/workspaces/{id}/landscape/scale",
24+
response_model=type(None),
25+
)

0 commit comments

Comments
 (0)