|
26 | 26 | _VALID_PROFILE_NAME = re.compile(r"^[A-Za-z0-9_-]+$") |
27 | 27 |
|
28 | 28 |
|
29 | | -class WorkspaceLandscapeManager(_APIOperationExecutor): |
30 | | - """Manager for workspace landscape operations (Multi Server Deployments).""" |
| 29 | +def _validate_profile_name(name: str) -> None: |
| 30 | + if not _VALID_PROFILE_NAME.match(name): |
| 31 | + raise ValueError( |
| 32 | + f"Invalid profile name '{name}'. Must match pattern ^[A-Za-z0-9_-]+$" |
| 33 | + ) |
| 34 | + |
| 35 | + |
| 36 | +def _profile_filename(name: str) -> str: |
| 37 | + _validate_profile_name(name) |
| 38 | + return f"ci.{name}.yml" |
| 39 | + |
31 | 40 |
|
| 41 | +class WorkspaceLandscapeManager(_APIOperationExecutor): |
32 | 42 | def __init__(self, http_client: APIHttpClient, workspace_id: int): |
33 | 43 | self._http_client = http_client |
34 | 44 | self._workspace_id = workspace_id |
35 | 45 | self.id = workspace_id |
36 | 46 |
|
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 | | - """ |
| 47 | + async def _run_command(self, command: str) -> "CommandOutput": |
46 | 48 | from ..operations import _EXECUTE_COMMAND_OP |
47 | 49 | from ..schemas import CommandInput |
48 | 50 |
|
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 |
| 51 | + return await self._execute_operation( |
| 52 | + _EXECUTE_COMMAND_OP, data=CommandInput(command=command) |
52 | 53 | ) |
53 | 54 |
|
| 55 | + async def list_profiles(self) -> ResourceList[Profile]: |
| 56 | + result = await self._run_command("ls -1 *.yml 2>/dev/null || true") |
| 57 | + |
54 | 58 | profiles: List[Profile] = [] |
55 | 59 | if result.output: |
56 | 60 | 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 | + if match := _PROFILE_FILE_PATTERN.match(line.strip()): |
| 62 | + profiles.append(Profile(name=match.group(1))) |
61 | 63 |
|
62 | 64 | return ResourceList[Profile](root=profiles) |
63 | 65 |
|
64 | 66 | 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. |
| 67 | + filename = _profile_filename(name) |
70 | 68 |
|
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 | 69 | if isinstance(config, ProfileConfig): |
84 | 70 | yaml_content = config.to_yaml() |
85 | 71 | else: |
86 | 72 | yaml_content = config |
87 | 73 |
|
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 |
| 74 | + body = yaml_content if yaml_content.endswith("\n") else yaml_content + "\n" |
| 75 | + await self._run_command( |
| 76 | + f"cat > {filename} << 'PROFILE_EOF'\n{body}PROFILE_EOF\n" |
122 | 77 | ) |
123 | 78 |
|
| 79 | + async def get_profile(self, name: str) -> str: |
| 80 | + result = await self._run_command(f"cat {_profile_filename(name)}") |
124 | 81 | return result.output |
125 | 82 |
|
126 | 83 | 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) |
| 84 | + await self._run_command(f"rm -f {_profile_filename(name)}") |
146 | 85 |
|
147 | 86 | 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 | 87 | if profile is not None: |
| 88 | + _validate_profile_name(profile) |
154 | 89 | await self._execute_operation(_DEPLOY_WITH_PROFILE_OP, profile=profile) |
155 | 90 | else: |
156 | 91 | await self._execute_operation(_DEPLOY_OP) |
157 | 92 |
|
158 | 93 | async def teardown(self) -> None: |
159 | | - """Teardown the landscape.""" |
160 | 94 | await self._execute_operation(_TEARDOWN_OP) |
161 | 95 |
|
162 | 96 | 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 | 97 | await self._execute_operation(_SCALE_OP, data=services) |
0 commit comments