Skip to content

Commit f9fdda2

Browse files
committed
add execute command example
1 parent deaa8ed commit f9fdda2

File tree

6 files changed

+342
-3
lines changed

6 files changed

+342
-3
lines changed

.devcontainer/Dockerfile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
ARG VARIANT="3.9"
2-
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
1+
FROM mcr.microsoft.com/devcontainers/python:3.12
32

43
USER vscode
54

65
RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash
76
ENV PATH=/home/vscode/.rye/shims:$PATH
87

9-
RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc
8+
RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc

examples/run_command.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env python
2+
3+
import sys
4+
import asyncio
5+
from gitpod import Gitpod
6+
import gitpod.lib as util
7+
from gitpod.types.environment_create_params import SpecContentInitializerSpecContextURL
8+
9+
# Example: ./examples/run_command.py https://github.com/gitpod-io/empty 'echo "Hello World!"'
10+
async def main() -> None:
11+
client = Gitpod()
12+
13+
if len(sys.argv) < 3:
14+
print("Usage: ./examples/run_command.py <CONTEXT_URL> <COMMAND>")
15+
sys.exit(1)
16+
context_url = sys.argv[1]
17+
command = " ".join(sys.argv[2:])
18+
19+
env_class = util.find_most_used_environment_class(client)
20+
if not env_class:
21+
print("Error: No environment class found. Please create one first.")
22+
sys.exit(1)
23+
24+
environment_id = client.environments.create(
25+
spec={
26+
"desired_phase": "ENVIRONMENT_PHASE_RUNNING",
27+
"content": {
28+
"initializer": {"specs": [SpecContentInitializerSpecContextURL(context_url={"url": context_url})]}
29+
},
30+
"machine": {"class": env_class.id},
31+
}
32+
).environment.id
33+
34+
try:
35+
util.wait_for_environment_ready(client, environment_id)
36+
37+
lines = util.run_command(client, environment_id, command)
38+
async for line in lines:
39+
print(line)
40+
finally:
41+
client.environments.delete(environment_id=environment_id)
42+
43+
if __name__ == "__main__":
44+
asyncio.run(main())

examples/run_service.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python
2+
3+
import sys
4+
import asyncio
5+
from gitpod import Gitpod
6+
import gitpod.lib as util
7+
from gitpod.types.environment_create_params import SpecContentInitializerSpecContextURL
8+
9+
# Example: ./examples/run_service.py https://github.com/gitpod-io/empty
10+
async def main() -> None:
11+
client = Gitpod()
12+
13+
if len(sys.argv) < 2:
14+
print("Usage: ./examples/run_service.py <CONTEXT_URL>")
15+
sys.exit(1)
16+
context_url = sys.argv[1]
17+
18+
env_class = util.find_most_used_environment_class(client)
19+
if not env_class:
20+
print("Error: No environment class found. Please create one first.")
21+
sys.exit(1)
22+
23+
port = 8888
24+
25+
environment_id = client.environments.create(
26+
spec={
27+
"desired_phase": "ENVIRONMENT_PHASE_RUNNING",
28+
"content": {
29+
"initializer": {"specs": [SpecContentInitializerSpecContextURL(context_url={"url": context_url})]}
30+
},
31+
"machine": {"class": env_class.id},
32+
"ports": [
33+
{
34+
"name": "Lama Service",
35+
"port": port,
36+
"admission": "ADMISSION_LEVEL_EVERYONE"
37+
}
38+
]
39+
}
40+
).environment.id
41+
42+
try:
43+
util.wait_for_environment_ready(client, environment_id)
44+
45+
lines = util.run_service(client, environment_id, {
46+
"name":"Lama Service",
47+
"description":"Lama Service",
48+
"reference":"lama-service"
49+
}, {
50+
"commands": {
51+
"start":f"curl lama.sh | LAMA_PORT={port} sh",
52+
"ready":f"curl -s http://localhost:{port}"
53+
}
54+
})
55+
56+
port_url = util.wait_for_port_url(client, environment_id, port)
57+
print(f"Lama Service is running at {port_url}")
58+
59+
async for line in lines:
60+
print(line)
61+
finally:
62+
client.environments.delete(environment_id=environment_id)
63+
64+
if __name__ == "__main__":
65+
asyncio.run(main())

src/gitpod/lib/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .automation import run_command, run_service
2+
from .environment import find_most_used_environment_class, wait_for_environment_ready, wait_for_port_url
3+
4+
__all__ = [
5+
'find_most_used_environment_class',
6+
'wait_for_environment_ready',
7+
'wait_for_port_url',
8+
'run_command',
9+
'run_service'
10+
]

src/gitpod/lib/automation.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import httpx
2+
from typing import AsyncIterator
3+
4+
from gitpod import Gitpod
5+
from gitpod.types.environments.automations import service_create_params
6+
TASK_REFERENCE = "gitpod-python-sdk"
7+
8+
def run_service(
9+
client: Gitpod,
10+
environment_id: str,
11+
metadata: service_create_params.Metadata,
12+
spec: service_create_params.Spec
13+
) -> AsyncIterator[str]:
14+
reference = metadata["reference"]
15+
if not reference:
16+
raise ValueError("metadata.reference is required")
17+
18+
services = client.environments.automations.services.list(
19+
filter={
20+
"references": [reference],
21+
"environment_ids": [environment_id]
22+
}
23+
).services
24+
25+
service_id = ""
26+
if not services:
27+
service_id = client.environments.automations.services.create(
28+
environment_id=environment_id,
29+
spec=spec,
30+
metadata=metadata
31+
).service.id
32+
else:
33+
service_id = services[0].id
34+
35+
client.environments.automations.services.start(id=service_id)
36+
log_url = wait_for_service_log_url(client, environment_id, service_id)
37+
return stream_logs(client, environment_id, log_url)
38+
39+
async def run_command(client: Gitpod, environment_id: str, command: str) -> AsyncIterator[str]:
40+
tasks = client.environments.automations.tasks.list(
41+
filter={
42+
"references": [TASK_REFERENCE],
43+
"environment_ids": [environment_id]
44+
}
45+
).tasks
46+
47+
task_id = ""
48+
if not tasks:
49+
task_id = client.environments.automations.tasks.create(
50+
spec={
51+
"command": command,
52+
},
53+
environment_id=environment_id,
54+
metadata={
55+
"name": "Gitpod Python SDK Task",
56+
"description": "Gitpod Python SDK Task",
57+
"reference": TASK_REFERENCE,
58+
},
59+
).task.id
60+
else:
61+
task_id = tasks[0].id
62+
client.environments.automations.tasks.update(
63+
id=task_id,
64+
spec={
65+
"command": command,
66+
},
67+
)
68+
69+
task_execution_id = client.environments.automations.tasks.start(id=task_id).task_execution.id
70+
log_url = wait_for_task_log_url(client, environment_id, task_execution_id)
71+
return stream_logs(client, environment_id, log_url)
72+
73+
async def stream_logs(client: Gitpod, environment_id: str, log_url: str) -> AsyncIterator[str]:
74+
logs_access_token = client.environments.create_logs_token(environment_id=environment_id).access_token
75+
async with httpx.AsyncClient() as http_client:
76+
async with http_client.stream("GET", log_url, headers={"Authorization": f"Bearer {logs_access_token}"}, timeout=None) as response:
77+
buffer = ""
78+
async for chunk in response.aiter_text():
79+
buffer += chunk
80+
while "\n" in buffer:
81+
line, buffer = buffer.split("\n", 1)
82+
if line:
83+
yield line
84+
if buffer:
85+
yield buffer
86+
87+
def wait_for_task_log_url(client: Gitpod, environment_id: str, task_execution_id: str) -> str:
88+
def get_log_url():
89+
execution = client.environments.automations.tasks.executions.retrieve(id=task_execution_id).task_execution
90+
return execution.status.log_url
91+
92+
return wait_for_log_url(client, environment_id, task_execution_id, get_log_url, "RESOURCE_TYPE_TASK_EXECUTION")
93+
94+
def wait_for_service_log_url(client: Gitpod, environment_id: str, service_id: str) -> str:
95+
def get_log_url():
96+
service = client.environments.automations.services.retrieve(id=service_id).service
97+
if service.status.phase != "SERVICE_PHASE_RUNNING":
98+
return None
99+
return service.status.log_url
100+
101+
return wait_for_log_url(client, environment_id, service_id, get_log_url, "RESOURCE_TYPE_SERVICE")
102+
103+
def wait_for_log_url(client: Gitpod, environment_id: str, resource_id: str, get_log_url_fn, resource_type: str) -> str:
104+
log_url = get_log_url_fn()
105+
if log_url:
106+
return log_url
107+
108+
event_stream = client.events.watch(environment_id=environment_id, timeout=None)
109+
try:
110+
log_url = get_log_url_fn()
111+
if log_url:
112+
return log_url
113+
114+
for event in event_stream:
115+
if event.resource_type == resource_type and event.resource_id == resource_id:
116+
log_url = get_log_url_fn()
117+
if log_url is not None:
118+
return log_url
119+
finally:
120+
event_stream.http_response.close()

src/gitpod/lib/environment.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from typing import Optional
2+
3+
from gitpod import Gitpod
4+
from gitpod.types.environments.class_list_response import ClassListResponse
5+
6+
def find_most_used_environment_class(client: Gitpod) -> Optional[ClassListResponse]:
7+
"""
8+
Find the most used environment class.
9+
"""
10+
user = client.users.get_authenticated_user(body={}).user
11+
12+
class_usage = {}
13+
page = client.environments.list(organization_id=user.organization_id)
14+
while page:
15+
for env in page.environments:
16+
env_class = env.spec.machine.class_
17+
if env_class not in class_usage:
18+
class_usage[env_class] = 0
19+
class_usage[env_class] += 1
20+
if page.pagination and page.pagination.next_token:
21+
page = client.environments.list(token=page.pagination.next_token, organization_id=user.organization_id)
22+
else:
23+
break
24+
25+
sorted_classes = sorted(class_usage.items(), key=lambda item: -item[1])
26+
environment_class_id = sorted_classes[0][0] if sorted_classes else None
27+
if not environment_class_id:
28+
return None
29+
30+
page = client.environments.classes.list(filter={"enabled": True})
31+
while page:
32+
for cls in page.environment_classes:
33+
if cls.id == environment_class_id:
34+
return cls
35+
if page.pagination and page.pagination.next_token:
36+
page = client.environments.classes.list(token=page.pagination.next_token)
37+
else:
38+
break
39+
return None
40+
41+
42+
def wait_for_environment_ready(client: Gitpod, environment_id: str) -> None:
43+
def is_ready() -> bool:
44+
environment = client.environments.retrieve(environment_id=environment_id).environment
45+
if environment.status.phase in [
46+
"ENVIRONMENT_PHASE_STOPPING",
47+
"ENVIRONMENT_PHASE_STOPPED",
48+
"ENVIRONMENT_PHASE_DELETING",
49+
"ENVIRONMENT_PHASE_DELETED",
50+
]:
51+
raise RuntimeError(f"Environment {environment_id} is in an unexpected phase: {environment.status.phase}")
52+
elif environment.status.failure_message and len(environment.status.failure_message) > 0:
53+
raise RuntimeError(f"Environment {environment_id} failed: {'; '.join(environment.status.failure_message)}")
54+
return environment.status.phase == "ENVIRONMENT_PHASE_RUNNING"
55+
56+
event_stream = client.events.watch(environment_id=environment_id, timeout=None)
57+
try:
58+
if is_ready():
59+
return
60+
61+
for event in event_stream:
62+
if event.resource_type == "RESOURCE_TYPE_ENVIRONMENT" and event.resource_id == environment_id:
63+
if is_ready():
64+
return
65+
finally:
66+
event_stream.http_response.close()
67+
68+
def wait_for_port_url(client: Gitpod, environment_id: str, port: int) -> str:
69+
def get_port_url() -> Optional[str]:
70+
environment = client.environments.retrieve(environment_id=environment_id).environment
71+
status = environment.status
72+
if not status:
73+
return None
74+
environment_urls = status.environment_urls
75+
if not environment_urls:
76+
return None
77+
ports = environment_urls.ports
78+
if not ports:
79+
return None
80+
for p in ports:
81+
if p.port == port:
82+
return p.url
83+
return None
84+
85+
port_url = get_port_url()
86+
if port_url:
87+
return port_url
88+
89+
event_stream = client.events.watch(environment_id=environment_id, timeout=None)
90+
try:
91+
port_url = get_port_url()
92+
if port_url:
93+
return port_url
94+
95+
for event in event_stream:
96+
if event.resource_type == "RESOURCE_TYPE_ENVIRONMENT" and event.resource_id == environment_id:
97+
port_url = get_port_url()
98+
if port_url:
99+
return port_url
100+
finally:
101+
event_stream.http_response.close()

0 commit comments

Comments
 (0)