Coverage for docs / client-examples / python_boto3_example.py: 100.00%
93 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
1#!/usr/bin/env python3
2"""
3Example: Submit Kubernetes manifests to GCO API Gateway using Python boto3
5This example demonstrates how to authenticate with AWS IAM and submit manifests
6to the GCO API Gateway using AWS SigV4 request signing.
8Requirements:
9 pip install boto3 requests aws-requests-auth
11Usage:
12 python python_boto3_example.py
13"""
15import json
16from pathlib import Path
17from typing import Any
19import boto3
20import requests
21from aws_requests_auth.aws_auth import AWSRequestsAuth
24def get_api_endpoint(region: str, project_name: str = "gco") -> str:
25 """
26 Get the API Gateway endpoint URL from CloudFormation stack outputs.
28 Args:
29 region: AWS region where API Gateway stack is deployed.
30 project_name: Deployment project prefix (defaults to ``gco``).
32 Returns:
33 API Gateway invoke URL
34 """
35 cfn = boto3.client("cloudformation", region_name=region)
37 stack_name = f"{project_name}-api-gateway"
38 response = cfn.describe_stacks(StackName=stack_name)
39 outputs = response["Stacks"][0]["Outputs"]
41 for output in outputs:
42 if output["OutputKey"] == "ApiEndpoint":
43 # Remove trailing slash if present
44 endpoint: str = output["OutputValue"]
45 return endpoint.rstrip("/")
47 raise ValueError(f"ApiEndpoint not found in stack {stack_name}")
50def create_aws_auth(api_host: str, region: str) -> AWSRequestsAuth:
51 """
52 Create AWS SigV4 authentication for API Gateway requests.
54 Args:
55 api_host: API Gateway host (e.g., 'abc123.execute-api.us-east-2.amazonaws.com')
56 region: AWS region
58 Returns:
59 AWSRequestsAuth object for request signing
60 """
61 session = boto3.Session()
62 credentials = session.get_credentials()
63 if credentials is None:
64 raise RuntimeError(
65 "No AWS credentials are available. Configure a profile, environment credentials, "
66 "or an IAM role before running this example."
67 )
68 frozen = credentials.get_frozen_credentials()
70 return AWSRequestsAuth(
71 aws_access_key=frozen.access_key,
72 aws_secret_access_key=frozen.secret_key,
73 aws_token=frozen.token, # For temporary credentials (STS, IAM roles)
74 aws_host=api_host,
75 aws_region=region,
76 aws_service="execute-api",
77 )
80def submit_manifests(
81 api_endpoint: str,
82 auth: AWSRequestsAuth,
83 manifests: list[dict[str, Any]],
84 namespace: str | None = None,
85 dry_run: bool = False,
86) -> dict[str, Any]:
87 """
88 Submit Kubernetes manifests to the API Gateway.
90 Args:
91 api_endpoint: API Gateway base URL
92 auth: AWS authentication object
93 manifests: List of Kubernetes manifests as dictionaries
94 namespace: Default namespace for manifests without one specified
95 dry_run: If True, validate without applying
97 Returns:
98 API response as dictionary
99 """
100 url = f"{api_endpoint}/api/v1/manifests"
102 payload = {"manifests": manifests, "dry_run": dry_run}
104 if namespace:
105 payload["namespace"] = namespace
107 response = requests.post(
108 url, json=payload, auth=auth, headers={"Content-Type": "application/json"}, timeout=30
109 )
111 response.raise_for_status()
112 body: dict[str, Any] = response.json()
113 return body
116def get_health(api_endpoint: str, auth: AWSRequestsAuth) -> dict[str, Any]:
117 """
118 Get cluster health status.
120 Args:
121 api_endpoint: API Gateway base URL
122 auth: AWS authentication object
124 Returns:
125 API response as dictionary
126 """
127 url = f"{api_endpoint}/api/v1/health"
129 response = requests.get(url, auth=auth, timeout=30)
130 response.raise_for_status()
131 body: dict[str, Any] = response.json()
132 return body
135def get_deployment_config() -> tuple[str, str]:
136 """Return ``(project_name, API Gateway region)`` from ``cdk.json``.
138 The stock values are used when the file is unavailable or malformed. AWS
139 credentials still come from boto3's normal provider chain, including
140 ``AWS_PROFILE``, SSO, web identity, containers, and instance roles.
141 """
142 cdk_json_path = Path(__file__).resolve().parents[2] / "cdk.json"
143 try:
144 with cdk_json_path.open(encoding="utf-8") as file:
145 context = json.load(file).get("context", {})
146 project_name = str(context.get("project_name") or "gco")
147 deployment_regions = context.get("deployment_regions", {})
148 api_region = str(deployment_regions.get("api_gateway") or "us-east-2")
149 return project_name, api_region
150 except OSError, AttributeError, ValueError:
151 # OSError: no readable cdk.json; ValueError: not JSON; AttributeError:
152 # valid JSON of the wrong shape (a list or scalar where a mapping was
153 # expected has no ``.get``).
154 return "gco", "us-east-2"
157def main() -> None:
158 project_name, api_region = get_deployment_config()
159 stack_name = f"{project_name}-api-gateway"
160 print(f"Using API Gateway region: {api_region}")
162 # Get API Gateway endpoint from CloudFormation
163 print(f"Getting API Gateway endpoint from stack {stack_name}...")
164 api_endpoint = get_api_endpoint(api_region, project_name)
165 print(f"API Endpoint: {api_endpoint}")
167 # Extract host from endpoint URL
168 api_host = api_endpoint.replace("https://", "").replace("http://", "").split("/")[0]
170 # Create AWS authentication
171 print("Creating AWS SigV4 authentication...")
172 auth = create_aws_auth(api_host, api_region)
174 # Example 1: Simple Kubernetes Job
175 print("\n=== Example 1: Submit a simple Job ===")
176 simple_job = {
177 "apiVersion": "batch/v1",
178 "kind": "Job",
179 "metadata": {"name": "python-example-job", "namespace": "gco-jobs"},
180 "spec": {
181 "template": {
182 "spec": {
183 "containers": [
184 {
185 "name": "example",
186 "image": "busybox:1.38.0",
187 "command": ["echo", "Hello from GCO Python client!"],
188 }
189 ],
190 "restartPolicy": "Never",
191 }
192 },
193 "backoffLimit": 3,
194 },
195 }
197 try:
198 result = submit_manifests(api_endpoint, auth, [simple_job])
199 print(f"Success: {json.dumps(result, indent=2)}")
200 except requests.exceptions.HTTPError as e:
201 print(f"Error: {e}")
202 print(f"Response: {e.response.text}")
204 # Example 2: GPU Job with node selector for on-demand capacity
205 print("\n=== Example 2: Submit a GPU Job (on-demand) ===")
206 gpu_job = {
207 "apiVersion": "batch/v1",
208 "kind": "Job",
209 "metadata": {"name": "gpu-python-job", "namespace": "gco-jobs"},
210 "spec": {
211 "template": {
212 "spec": {
213 "containers": [
214 {
215 "name": "gpu-example",
216 "image": "nvidia/cuda:12.0-base",
217 "command": ["nvidia-smi"],
218 "resources": {"limits": {"nvidia.com/gpu": "1"}},
219 }
220 ],
221 "restartPolicy": "Never",
222 "nodeSelector": {"karpenter.sh/capacity-type": "on-demand"},
223 "tolerations": [
224 {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}
225 ],
226 }
227 },
228 "backoffLimit": 3,
229 },
230 }
232 print(f"GPU Job manifest: {json.dumps(gpu_job, indent=2)}")
233 print("(Not submitting - uncomment to test with GPU nodes)")
234 # Uncomment to submit:
235 # try:
236 # result = submit_manifests(api_endpoint, auth, [gpu_job])
237 # print(f"Success: {json.dumps(result, indent=2)}")
238 # except requests.exceptions.HTTPError as e:
239 # print(f"Error: {e}")
240 # print(f"Response: {e.response.text}")
242 # Example 3: Multiple manifests at once
243 print("\n=== Example 3: Submit multiple manifests ===")
244 config_map = {
245 "apiVersion": "v1",
246 "kind": "ConfigMap",
247 "metadata": {"name": "python-example-config", "namespace": "gco-jobs"},
248 "data": {"config.yaml": "key: value\nother: setting"},
249 }
251 config_reader_job = {
252 "apiVersion": "batch/v1",
253 "kind": "Job",
254 "metadata": {"name": "config-reader-python-job", "namespace": "gco-jobs"},
255 "spec": {
256 "template": {
257 "spec": {
258 "containers": [
259 {
260 "name": "reader",
261 "image": "busybox:1.38.0",
262 "command": ["cat", "/config/config.yaml"],
263 "volumeMounts": [{"name": "config-volume", "mountPath": "/config"}],
264 }
265 ],
266 "volumes": [
267 {"name": "config-volume", "configMap": {"name": "python-example-config"}}
268 ],
269 "restartPolicy": "Never",
270 }
271 },
272 "backoffLimit": 3,
273 },
274 }
276 try:
277 result = submit_manifests(api_endpoint, auth, [config_map, config_reader_job])
278 print(f"Success: {json.dumps(result, indent=2)}")
279 except requests.exceptions.HTTPError as e:
280 print(f"Error: {e}")
281 print(f"Response: {e.response.text}")
283 # Example 4: Dry run validation
284 print("\n=== Example 4: Dry run validation ===")
285 try:
286 result = submit_manifests(api_endpoint, auth, [simple_job], dry_run=True)
287 print(f"Dry run result: {json.dumps(result, indent=2)}")
288 except requests.exceptions.HTTPError as e:
289 print(f"Error: {e}")
290 print(f"Response: {e.response.text}")
292 print("\n=== Examples Complete ===")
293 print("\nKey points:")
294 print("1. The API expects 'manifests' as a list of JSON objects (not YAML strings)")
295 print("2. Each manifest must include apiVersion, kind, and metadata with name")
296 print("3. Use aws-requests-auth for SigV4 signing with requests library")
297 print("4. Use nodeSelector 'karpenter.sh/capacity-type' to control spot vs on-demand")
300if __name__ == "__main__":
301 main()