Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions examples/create_workspace_with_landscape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import asyncio
import time

from codesphere import CodesphereSDK
from codesphere.resources.workspace import WorkspaceCreate
from codesphere.resources.workspace.landscape import ProfileBuilder, ProfileConfig

TEAM_ID = 123 # Replace with your actual team ID


async def get_plan_id(sdk: CodesphereSDK, plan_name: str = "Micro") -> int:
plans = await sdk.metadata.list_plans()
plan = next((p for p in plans if p.title == plan_name and not p.deprecated), None)
if not plan:
raise ValueError(f"Plan '{plan_name}' not found")
return plan.id


def build_web_profile(plan_id: int) -> ProfileConfig:
"""Build a simple web service landscape profile."""
return (
ProfileBuilder()
.prepare()
.add_step("npm install", name="Install dependencies")
.done()
.add_reactive_service("web")
.plan(plan_id)
.add_step("npm start")
.add_port(3000, public=True)
.add_path("/", port=3000)
.replicas(1)
.env("NODE_ENV", "production")
.build()
)


async def create_workspace(sdk: CodesphereSDK, plan_id: int, name: str):
workspace = await sdk.workspaces.create(
WorkspaceCreate(plan_id=plan_id, team_id=TEAM_ID, name=name)
)
await workspace.wait_until_running(timeout=300.0, poll_interval=5.0)
return workspace


async def deploy_landscape(workspace, profile: dict, profile_name: str = "production"):
await workspace.landscape.save_profile(profile_name, profile)
await workspace.landscape.deploy(profile=profile_name)
print("Deployment started!")


async def main():
async with CodesphereSDK() as sdk:
plan_id = await get_plan_id(sdk)
workspace = await create_workspace(
sdk, plan_id, f"landscape-demo-{int(time.time())}"
)
profile = build_web_profile(plan_id)
await deploy_landscape(workspace, profile)


if __name__ == "__main__":
asyncio.run(main())
Empty file.
Empty file.
31 changes: 31 additions & 0 deletions src/codesphere/resources/workspace/landscape/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from .models import WorkspaceLandscapeManager
from .schemas import (
ManagedServiceBuilder,
ManagedServiceConfig,
NetworkConfig,
PathConfig,
PortConfig,
Profile,
ProfileBuilder,
ProfileConfig,
ReactiveServiceBuilder,
ReactiveServiceConfig,
StageConfig,
Step,
)

__all__ = [
"WorkspaceLandscapeManager",
"Profile",
"ProfileBuilder",
"ProfileConfig",
"Step",
"StageConfig",
"ReactiveServiceConfig",
"ReactiveServiceBuilder",
"ManagedServiceConfig",
"ManagedServiceBuilder",
"NetworkConfig",
"PortConfig",
"PathConfig",
]
97 changes: 97 additions & 0 deletions src/codesphere/resources/workspace/landscape/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from __future__ import annotations

import logging
import re
from typing import TYPE_CHECKING, Dict, List, Optional, Union

from ....core.base import ResourceList
from ....core.handler import _APIOperationExecutor
from ....http_client import APIHttpClient
from .operations import (
_DEPLOY_OP,
_DEPLOY_WITH_PROFILE_OP,
_SCALE_OP,
_TEARDOWN_OP,
)
from .schemas import Profile, ProfileConfig

if TYPE_CHECKING:
from ..schemas import CommandOutput

log = logging.getLogger(__name__)

# Regex pattern to match ci.<profile>.yml files
_PROFILE_FILE_PATTERN = re.compile(r"^ci\.([A-Za-z0-9_-]+)\.yml$")
# Pattern for valid profile names
_VALID_PROFILE_NAME = re.compile(r"^[A-Za-z0-9_-]+$")


def _validate_profile_name(name: str) -> None:
if not _VALID_PROFILE_NAME.match(name):
raise ValueError(
f"Invalid profile name '{name}'. Must match pattern ^[A-Za-z0-9_-]+$"
)


def _profile_filename(name: str) -> str:
_validate_profile_name(name)
return f"ci.{name}.yml"


class WorkspaceLandscapeManager(_APIOperationExecutor):
def __init__(self, http_client: APIHttpClient, workspace_id: int):
self._http_client = http_client
self._workspace_id = workspace_id
self.id = workspace_id

async def _run_command(self, command: str) -> "CommandOutput":
from ..operations import _EXECUTE_COMMAND_OP
from ..schemas import CommandInput

return await self._execute_operation(
_EXECUTE_COMMAND_OP, data=CommandInput(command=command)
)

async def list_profiles(self) -> ResourceList[Profile]:
result = await self._run_command("ls -1 *.yml 2>/dev/null || true")

profiles: List[Profile] = []
if result.output:
for line in result.output.strip().split("\n"):
if match := _PROFILE_FILE_PATTERN.match(line.strip()):
profiles.append(Profile(name=match.group(1)))

return ResourceList[Profile](root=profiles)

async def save_profile(self, name: str, config: Union[ProfileConfig, str]) -> None:
filename = _profile_filename(name)

if isinstance(config, ProfileConfig):
yaml_content = config.to_yaml()
else:
yaml_content = config

body = yaml_content if yaml_content.endswith("\n") else yaml_content + "\n"
await self._run_command(
f"cat > {filename} << 'PROFILE_EOF'\n{body}PROFILE_EOF\n"
)

async def get_profile(self, name: str) -> str:
result = await self._run_command(f"cat {_profile_filename(name)}")
return result.output

async def delete_profile(self, name: str) -> None:
await self._run_command(f"rm -f {_profile_filename(name)}")

async def deploy(self, profile: Optional[str] = None) -> None:
if profile is not None:
_validate_profile_name(profile)
await self._execute_operation(_DEPLOY_WITH_PROFILE_OP, profile=profile)
else:
await self._execute_operation(_DEPLOY_OP)

async def teardown(self) -> None:
await self._execute_operation(_TEARDOWN_OP)

async def scale(self, services: Dict[str, int]) -> None:
await self._execute_operation(_SCALE_OP, data=services)
25 changes: 25 additions & 0 deletions src/codesphere/resources/workspace/landscape/operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from ....core.operations import APIOperation

_DEPLOY_OP = APIOperation(
method="POST",
endpoint_template="/workspaces/{id}/landscape/deploy",
response_model=type(None),
)

_DEPLOY_WITH_PROFILE_OP = APIOperation(
method="POST",
endpoint_template="/workspaces/{id}/landscape/deploy/{profile}",
response_model=type(None),
)

_TEARDOWN_OP = APIOperation(
method="DELETE",
endpoint_template="/workspaces/{id}/landscape/teardown",
response_model=type(None),
)

_SCALE_OP = APIOperation(
method="PATCH",
endpoint_template="/workspaces/{id}/landscape/scale",
response_model=type(None),
)
Loading