Skip to content

Commit 4b6ae6c

Browse files
committed
feat: add log streaming and environment management utilities; implement asynchronous log streaming and context retrieval functions
1 parent 528b635 commit 4b6ae6c

File tree

5 files changed

+247
-79
lines changed

5 files changed

+247
-79
lines changed

examples/execute_command.py

Lines changed: 36 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,60 @@
11
#!/usr/bin/env python
22

33
from gitpod import Gitpod
4-
from utils import get_context_url, get_authenticated_user, get_environment_class_id, get_environment_class, create_environment, check_environment_status, stream_logs
5-
import asyncio
4+
from utils.context import get_context_url, get_authenticated_user
5+
from utils.environment import get_environment_class_id, get_environment_class, create_environment
6+
from utils.task import create_command, run_command, delete_command, stream_command_logs
67

78
def main() -> None:
9+
"""
10+
Main function to create an environment, start a task, and stream its logs.
11+
"""
12+
# Initialize Gitpod client
813
client: Gitpod = Gitpod()
14+
15+
# Get context URL and authenticated user
916
context_url: str = get_context_url()
1017
user = get_authenticated_user(client)
18+
19+
# Get environment class ID and details
1120
environment_class_id: str = get_environment_class_id(client, user)
12-
1321
if not environment_class_id:
14-
raise ValueError("no environment class found")
15-
22+
raise ValueError("no environment class found, please set the ENVIRONMENT_CLASS_ID environment variable")
1623
environment_class = get_environment_class(client, environment_class_id)
17-
1824
if not environment_class:
19-
raise ValueError("no environment class found")
25+
raise ValueError("no environment class found, please set the ENVIRONMENT_CLASS_ID environment variable")
2026

2127
print(f"Using repository: {context_url}")
22-
print(f"Using environment class: {environment_class.display_name} (ID: {environment_class.id})")
28+
print(f"Using environment class: {environment_class.display_name} ({environment_class.description})")
2329

30+
# Create environment
2431
environment_id: str = create_environment(client, context_url, environment_class)
25-
print(f"Created environment: {environment_id}")
32+
print(f"\nCreated environment: {environment_id}")
2633

2734
try:
35+
# Define command
2836
reference = "hello-world"
29-
task = client.environments.automations.tasks.create(
30-
spec={
31-
"command": "echo 'Hello, World!' && false",
32-
},
33-
environment_id=environment_id,
34-
metadata={
35-
"name": "Hello, World!",
36-
"description": "Prints 'Hello, World!' to the console.",
37-
"reference": reference,
38-
}
39-
).task
40-
print(f"Created task: {task.id}")
41-
42-
execution = client.environments.automations.tasks.start(
43-
id=task.id
44-
).task_execution
45-
print(f"Started task execution: {execution.id}")
46-
47-
logs_access_token = client.environments.create_logs_token(environment_id=environment_id).access_token
48-
print(f"Waiting for task logs to be available...")
49-
50-
event_stream = client.events.watch(
51-
environment_id=environment_id,
52-
timeout=None
53-
)
54-
55-
log_url = None
56-
for event in event_stream:
57-
if event.resource_type == "RESOURCE_TYPE_ENVIRONMENT" and event.resource_id == environment_id:
58-
check_environment_status(client, environment_id)
59-
elif event.resource_type == "RESOURCE_TYPE_TASK_EXECUTION" and event.resource_id == execution.id:
60-
execution = client.environments.automations.tasks.executions.retrieve(id=execution.id).task_execution
61-
print(f"Task execution status: {execution.status.phase}")
62-
if execution.status.log_url:
63-
log_url = execution.status.log_url
64-
break
65-
66-
event_stream.http_response.close()
67-
68-
if not log_url:
69-
raise RuntimeError("Task logs are not available.")
70-
71-
print(f"Task logs are available at: {log_url}")
72-
print("")
73-
print("Streaming logs:")
74-
75-
asyncio.run(stream_logs(log_url, logs_access_token))
76-
77-
execution = client.environments.automations.tasks.executions.retrieve(id=execution.id).task_execution
78-
print(f"Task execution status: {execution.status.phase}")
79-
if execution.status.phase == "TASK_EXECUTION_PHASE_FAILED":
80-
print(f"Task execution failed: {execution.status.failure_message}")
81-
37+
name = "Hello, World!"
38+
description = "Prints 'Hello, World!' to the console."
39+
task_id = create_command(client, environment_id, reference, name, description)
40+
try:
41+
# Run command
42+
task_execution = run_command(client, task_id)
43+
44+
# Stream logs
45+
stream_command_logs(client, environment_id, task_execution)
46+
47+
# Check command status
48+
execution = client.environments.automations.tasks.executions.retrieve(id=execution_id).task_execution
49+
print(f"\nTask execution status: {execution.status.phase}")
50+
if execution.status.phase == "TASK_EXECUTION_PHASE_FAILED":
51+
print(f"Task execution failed: {execution.status.failure_message}")
52+
finally:
53+
delete_command(client, task_id)
8254
finally:
55+
# Clean up environment
8356
client.environments.delete(environment_id=environment_id)
84-
print(f"Deleted environment: {environment_id}")
57+
print(f"\nDeleted environment: {environment_id}")
8558

8659
if __name__ == "__main__":
8760
main()

examples/utils/context.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import os
2+
from typing import Optional
3+
from gitpod import Gitpod
4+
from gitpod.types.user_get_authenticated_user_response import User
5+
6+
def get_context_url() -> str:
7+
"""
8+
Retrieve the context URL from the environment variable or use a default value.
9+
10+
Returns:
11+
str: The context URL.
12+
"""
13+
return os.environ.get("CONTEXT_URL", "https://github.com/gitpod-io/empty")
14+
15+
def get_authenticated_user(client: Gitpod) -> User:
16+
"""
17+
Get the authenticated user using the Gitpod client.
18+
19+
Args:
20+
client (Gitpod): The Gitpod client instance.
21+
22+
Returns:
23+
User: The authenticated user.
24+
"""
25+
return client.users.get_authenticated_user(body={}).user

examples/utils.py renamed to examples/utils/environment.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import os
22
from typing import Optional
3-
from gitpod.types.environment_create_params import SpecContentInitializerSpecContextURL
43
from gitpod import Gitpod
54
from gitpod.types.user_get_authenticated_user_response import User
65
from gitpod.types.environments.class_list_response import ClassListResponse
7-
import httpx
6+
from gitpod.types.environment_create_params import SpecContentInitializerSpecContextURL
87

9-
def get_context_url() -> str:
10-
return os.environ.get("CONTEXT_URL", "https://github.com/gitpod-io/empty")
8+
def get_environment_class_id(client: Gitpod, user: User) -> Optional[str]:
9+
"""
10+
Get the most used environment class ID for the given user.
1111
12-
def get_authenticated_user(client: Gitpod) -> User:
13-
return client.users.get_authenticated_user(body={}).user
12+
Args:
13+
client (Gitpod): The Gitpod client instance.
14+
user (User): The authenticated user.
1415
15-
def get_environment_class_id(client: Gitpod, user: User) -> Optional[str]:
16+
Returns:
17+
Optional[str]: The environment class ID or None if not found.
18+
"""
1619
environment_class_id = os.environ.get("ENVIRONMENT_CLASS_ID")
1720
if environment_class_id:
1821
return environment_class_id
@@ -34,6 +37,16 @@ def get_environment_class_id(client: Gitpod, user: User) -> Optional[str]:
3437
return sorted_classes[0][0] if sorted_classes else None
3538

3639
def get_environment_class(client: Gitpod, environment_class_id: str) -> Optional[ClassListResponse]:
40+
"""
41+
Get the environment class details for the given environment class ID.
42+
43+
Args:
44+
client (Gitpod): The Gitpod client instance.
45+
environment_class_id (str): The environment class ID.
46+
47+
Returns:
48+
Optional[ClassListResponse]: The environment class details or None if not found.
49+
"""
3750
page = client.environments.classes.list(filter={"enabled": True})
3851
while page:
3952
for cls in page.environment_classes:
@@ -46,6 +59,17 @@ def get_environment_class(client: Gitpod, environment_class_id: str) -> Optional
4659
return None
4760

4861
def create_environment(client: Gitpod, context_url: str, environment_class: ClassListResponse) -> str:
62+
"""
63+
Create a new environment with the given context URL and environment class.
64+
65+
Args:
66+
client (Gitpod): The Gitpod client instance.
67+
context_url (str): The context URL.
68+
environment_class (ClassListResponse): The environment class details.
69+
70+
Returns:
71+
str: The created environment ID.
72+
"""
4973
resp = client.environments.create(
5074
spec={
5175
"desired_phase": "ENVIRONMENT_PHASE_RUNNING",
@@ -67,18 +91,20 @@ def create_environment(client: Gitpod, context_url: str, environment_class: Clas
6791
)
6892
return resp.environment.id
6993

70-
def check_environment_status(client: Gitpod, environment_id: str) -> None:
94+
def ensure_environment_healthy(client: Gitpod, environment_id: str) -> None:
95+
"""
96+
Ensure the environment is running or will be running and raise an error if it is in an unexpected phase.
97+
98+
Args:
99+
client (Gitpod): The Gitpod client instance.
100+
environment_id (str): The environment ID.
101+
102+
Raises:
103+
RuntimeError: If the environment is in an unexpected phase or has a failure message.
104+
"""
71105
environment = client.environments.retrieve(environment_id=environment_id).environment
72106
print(f"Environment status: {environment.status.phase}")
73107
if environment.status.phase in ["ENVIRONMENT_PHASE_STOPPING", "ENVIRONMENT_PHASE_STOPPED", "ENVIRONMENT_PHASE_DELETING", "ENVIRONMENT_PHASE_DELETED"]:
74108
raise RuntimeError(f"Environment {environment_id} is in an unexpected phase: {environment.status.phase}")
75109
elif environment.status.failure_message and len(environment.status.failure_message) > 0:
76110
raise RuntimeError(f"Environment {environment_id} failed: {'; '.join(environment.status.failure_message)}")
77-
78-
async def stream_logs(log_url: str, logs_access_token: str) -> None:
79-
async with httpx.AsyncClient() as client:
80-
async with client.stream("GET", log_url, headers={"Authorization": f"Bearer {logs_access_token}"}) as response:
81-
async for line in response.aiter_lines():
82-
if line:
83-
print(line)
84-

examples/utils/logs.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import httpx
2+
3+
async def stream_logs(log_url: str, logs_access_token: str) -> None:
4+
"""
5+
Stream logs from the given log URL using the provided access token.
6+
7+
Args:
8+
log_url (str): The log URL.
9+
logs_access_token (str): The access token for the logs.
10+
"""
11+
async with httpx.AsyncClient() as client:
12+
async with client.stream("GET", log_url, headers={"Authorization": f"Bearer {logs_access_token}"}) as response:
13+
async for line in response.aiter_lines():
14+
if line:
15+
print(line)

examples/utils/task.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from gitpod import Gitpod
2+
import asyncio
3+
from utils.logs import stream_logs
4+
from utils.environment import ensure_environment_healthy
5+
6+
7+
def create_command(client: Gitpod, environment_id: str, reference: str, name: str, description: str) -> str:
8+
"""
9+
Create a task in the given environment.
10+
11+
Args:
12+
client (Gitpod): The Gitpod client instance.
13+
environment_id (str): The environment ID.
14+
reference (str): The task reference.
15+
name (str): The task name.
16+
description (str): The task description.
17+
18+
Returns:
19+
str: The task ID.
20+
"""
21+
task = client.environments.automations.tasks.create(
22+
spec={
23+
"command": "echo 'Hello, World!'",
24+
},
25+
environment_id=environment_id,
26+
metadata={
27+
"name": name,
28+
"description": description,
29+
"reference": reference,
30+
}
31+
).task
32+
print(f"\nCreated task: {task.id}")
33+
return task.id
34+
35+
36+
def run_command(client: Gitpod, task_id: str) -> str:
37+
"""
38+
Start a task execution.
39+
40+
Args:
41+
client (Gitpod): The Gitpod client instance.
42+
task_id (str): The task ID.
43+
44+
Returns:
45+
str: The task execution ID.
46+
"""
47+
execution = client.environments.automations.tasks.start(
48+
id=task_id
49+
).task_execution
50+
print(f"Started task execution: {execution.id}")
51+
return execution.id
52+
53+
54+
def delete_command(client: Gitpod, task_id: str) -> None:
55+
"""
56+
Delete a task.
57+
58+
Args:
59+
client (Gitpod): The Gitpod client instance.
60+
task_id (str): The task ID.
61+
"""
62+
client.environments.automations.tasks.delete(id=task_id)
63+
print(f"Deleted task: {task_id}")
64+
65+
66+
def wait_for_task_logs(client: Gitpod, environment_id: str, execution_id: str) -> str:
67+
"""
68+
Wait for the task logs to be available.
69+
70+
Args:
71+
client (Gitpod): The Gitpod client instance.
72+
environment_id (str): The environment ID.
73+
execution_id (str): The task execution ID.
74+
75+
Returns:
76+
str: The log URL.
77+
78+
Raises:
79+
RuntimeError: If the task logs are not available.
80+
"""
81+
def get_log_url() -> str:
82+
execution = client.environments.automations.tasks.executions.retrieve(
83+
id=execution_id).task_execution
84+
return execution.status.log_url
85+
86+
print(f"Waiting for task logs to be available...")
87+
88+
# 1. open the stream before the first request
89+
event_stream = client.events.watch(
90+
environment_id=environment_id,
91+
timeout=None
92+
)
93+
94+
try:
95+
# 2. make the first request to ensure that we did not miss any events
96+
ensure_environment_healthy(client, environment_id)
97+
log_url = get_log_url()
98+
if log_url:
99+
return log_url
100+
101+
for event in event_stream:
102+
if event.resource_type == "RESOURCE_TYPE_ENVIRONMENT" and event.resource_id == environment_id:
103+
ensure_environment_healthy(client, environment_id)
104+
elif event.resource_type == "RESOURCE_TYPE_TASK_EXECUTION" and event.resource_id == execution_id:
105+
log_url = get_log_url()
106+
print(f"Task execution status: {client.environments.automations.tasks.executions.retrieve(id=execution_id).task_execution.status.phase}")
107+
if log_url:
108+
return log_url
109+
finally:
110+
event_stream.http_response.close()
111+
112+
raise RuntimeError("Task logs are not available.")
113+
114+
115+
def stream_command_logs(client: Gitpod, environment_id: str, execution_id: str) -> None:
116+
"""
117+
Stream the logs of a command execution.
118+
119+
Args:
120+
client (Gitpod): The Gitpod client instance.
121+
environment_id (str): The environment ID.
122+
execution_id (str): The task execution ID.
123+
"""
124+
log_url = wait_for_task_logs(client, environment_id, execution_id)
125+
print(f"\nTask logs are available at: {log_url}")
126+
print("\nStreaming logs:")
127+
128+
logsAccessToken = client.environments.create_logs_token(environment_id=environment_id).access_token
129+
asyncio.run(stream_logs(log_url, logsAccessToken))

0 commit comments

Comments
 (0)