Skip to content
Draft
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
110 changes: 110 additions & 0 deletions .github/scripts/cloud_namespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Create and delete an isolated Temporal Cloud namespace for CI."""

import asyncio
import os
import sys
import time
from pathlib import Path

from temporalio.api.cloud.cloudservice.v1 import (
CreateNamespaceRequest,
DeleteNamespaceRequest,
GetAsyncOperationRequest,
GetNamespaceRequest,
)
from temporalio.api.cloud.namespace.v1 import MtlsAuthSpec, NamespaceSpec
from temporalio.api.cloud.operation.v1 import AsyncOperation
from temporalio.client import CloudOperationsClient


async def wait_for_operation(
client: CloudOperationsClient, operation: AsyncOperation
) -> None:
deadline = time.monotonic() + 10 * 60
while True:
operation = (
await client.cloud_service.get_async_operation(
GetAsyncOperationRequest(async_operation_id=operation.id)
)
).async_operation
if operation.state == AsyncOperation.STATE_FULFILLED:
return
if operation.state in {
AsyncOperation.STATE_FAILED,
AsyncOperation.STATE_CANCELLED,
AsyncOperation.STATE_REJECTED,
}:
raise RuntimeError(
"Cloud operation "
f"{operation.id} {AsyncOperation.State.Name(operation.state).lower()}: "
f"{operation.failure_reason}"
)
if time.monotonic() >= deadline:
raise TimeoutError(f"Timed out waiting for Cloud operation {operation.id}")
delay = max(
operation.check_duration.seconds
+ operation.check_duration.nanos / 1_000_000_000,
1,
)
await asyncio.sleep(min(delay, deadline - time.monotonic()))


async def create() -> None:
client = await cloud_client()
namespace_name = "sdk-python-ci-{}-{}".format(
os.environ["GITHUB_RUN_ID"], os.environ["GITHUB_RUN_ATTEMPT"]
)
result = await client.cloud_service.create_namespace(
CreateNamespaceRequest(
spec=NamespaceSpec(
name=namespace_name,
regions=["aws-ca-central-1"],
retention_days=1,
mtls_auth=MtlsAuthSpec(
accepted_client_ca=Path(
os.environ["TEMPORAL_CLOUD_CLIENT_CA_PATH"]
).read_bytes(),
enabled=True,
),
)
)
)
# Make cleanup possible even if provisioning fails after Cloud accepts the request.
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
output.write(f"namespace={result.namespace}\n")
await wait_for_operation(client, result.async_operation)


async def delete(namespace: str) -> None:
client = await cloud_client()
existing = await client.cloud_service.get_namespace(
GetNamespaceRequest(namespace=namespace)
)
result = await client.cloud_service.delete_namespace(
DeleteNamespaceRequest(
namespace=namespace,
resource_version=existing.namespace.resource_version,
)
)
await wait_for_operation(client, result.async_operation)


async def cloud_client() -> CloudOperationsClient:
return await CloudOperationsClient.connect(
api_key=os.environ["TEMPORAL_CLIENT_CLOUD_API_KEY"],
version=os.environ["TEMPORAL_CLIENT_CLOUD_API_VERSION"],
)


async def main() -> None:
match sys.argv[1:]:
case ["create"]:
await create()
case ["delete", namespace]:
await delete(namespace)
case _:
raise ValueError("Usage: cloud_namespace.py create|delete <namespace>")


if __name__ == "__main__":
asyncio.run(main())
41 changes: 34 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,46 @@ jobs:
- run: uv tool install poethepoet
- run: uv sync --all-extras
- run: poe build-develop
- name: Generate Cloud test certificates
run: |
cert_dir="$RUNNER_TEMP/cloud-test-certs"
mkdir "$cert_dir"
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-keyout "$cert_dir/ca.key" -out "$cert_dir/ca.pem" \
-subj '/CN=Temporal Python SDK Cloud CI CA'
openssl req -newkey rsa:2048 -nodes \
-keyout "$cert_dir/client.key" -out "$cert_dir/client.csr" \
-subj '/CN=Temporal Python SDK Cloud CI'
openssl x509 -req -days 1 -in "$cert_dir/client.csr" \
-CA "$cert_dir/ca.pem" -CAkey "$cert_dir/ca.key" -CAcreateserial \
-out "$cert_dir/client.pem" -extfile <(printf 'extendedKeyUsage=clientAuth')
{
echo "TEMPORAL_CLOUD_CLIENT_CA_PATH=$cert_dir/ca.pem"
echo "TEMPORAL_TLS_CLIENT_CERT_PATH=$cert_dir/client.pem"
echo "TEMPORAL_TLS_CLIENT_KEY_PATH=$cert_dir/client.key"
} >> "$GITHUB_ENV"
- name: Create Cloud namespace
id: create-cloud-namespace
run: uv run python .github/scripts/cloud_namespace.py create
env:
TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1
- run: mkdir junit-xml
- run: poe test -s --workflow-environment envconfig --junit-xml=junit-xml/cloud.xml
timeout-minutes: 15
env:
TEMPORAL_ADDRESS: sdk-ci.a2dd6.tmprl.cloud:7233
TEMPORAL_NAMESPACE: sdk-ci.a2dd6
TEMPORAL_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
TEMPORAL_TLS_CLIENT_CERT_DATA: ${{ secrets.TEMPORAL_CLIENT_CERT }}
TEMPORAL_TLS_CLIENT_KEY_DATA: ${{ secrets.TEMPORAL_CLIENT_KEY }}
TEMPORAL_ADDRESS: ${{ steps.create-cloud-namespace.outputs.namespace }}.tmprl.cloud:7233
TEMPORAL_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }}
TEMPORAL_IS_CLOUD_TESTS: true
TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00
TEMPORAL_CLIENT_CLOUD_NAMESPACE: sdk-ci.a2dd6
TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1
TEMPORAL_CLIENT_CLOUD_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }}
- name: Delete Cloud namespace
if: ${{ always() && steps.create-cloud-namespace.outputs.namespace != '' }}
run: uv run python .github/scripts/cloud_namespace.py delete "${{ steps.create-cloud-namespace.outputs.namespace }}"
env:
TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1
- name: "Upload junit-xml artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
Expand Down
Loading