Skip to content

Commit 85f290e

Browse files
Rr 15 fix containerappclient (#17)
* container app client fix * remove None option * fix docstrings * fix credential * updated docs for containerappclient * fix documentation * updating start_job * add stop_job function * update container client and docs * Update cfa/cloudops/_containerappclient.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * readme and changelog updates * fixed docs --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9c94057 commit 85f290e

7 files changed

Lines changed: 170 additions & 13 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
![Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FCDCgov%2Fcfa-cloudops%2Frefs%2Fheads%2Fmaster%2Fpyproject.toml&query=%24.tool.poetry.version&style=plastic&label=version&color=lightgray)
1+
![Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FCDCgov%2Fcfa-cloudops%2Frefs%2Fheads%2Fmain%2Fpyproject.toml&query=project.version&style=plastic&logoColor=lightGray&label=version)
2+
23
![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&style=plastic&link=https://raw.githubusercontent.com/CDCgov/cfa_azure/refs/heads/master/.pre-commit-config.yaml)
34
![pre-commit](https://github.com/CDCgov/cfa_azure/workflows/pre-commit/badge.svg?style=plastic&link=https://github.com/CDCgov/cfa-cloudops/actions/workflows/pre-commit.yaml)
45
![CI](https://github.com/CDCgov/cfa_azure/workflows/Python%20Unit%20Tests%20with%20Coverage/badge.svg?style=plastic&link=https://github.com/CDCgov/cfa-cloudops/actions/workflows/pre-commit.yaml&link=https://github.com/CDCgov/cfa-cloudops/actions/workflows/ci.yaml)

cfa/cloudops/_containerappclient.py

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import logging
22
import os
33

4+
import dotenv
45
from azure.identity import ManagedIdentityCredential
56
from azure.mgmt.appcontainers import ContainerAppsAPIClient
67
from azure.mgmt.appcontainers.models import (
78
JobExecutionContainer,
89
JobExecutionTemplate,
910
)
11+
from azure.mgmt.resource import SubscriptionClient
1012

1113
logger = logging.getLogger(__name__)
1214

@@ -22,6 +24,7 @@ class ContainerAppClient:
2224

2325
def __init__(
2426
self,
27+
dotenv_path=None,
2528
resource_group=None,
2629
subscription_id=None,
2730
job_name=None,
@@ -30,13 +33,22 @@ def __init__(
3033
Initialize a ContainerAppClient for Azure Container Apps jobs.
3134
3235
Args:
36+
dotenv_path (str, optional): Path to a .env file to load environment variables.
3337
resource_group (str, optional): Azure resource group name. If None, uses env var AZURE_RESOURCE_GROUP_NAME.
3438
subscription_id (str, optional): Azure subscription ID. If None, uses env var AZURE_SUBSCRIPTION_ID.
35-
job_name (str, optional): Default job name for operations.
39+
job_name (str, optional): Job name for Container App Job.
3640
3741
Raises:
3842
ValueError: If required parameters are missing and not set in environment variables.
3943
"""
44+
self.credential = ManagedIdentityCredential()
45+
dotenv.load_dotenv(dotenv_path)
46+
sub_c = SubscriptionClient(self.credential)
47+
# pull in account info and save to environment vars
48+
account_info = list(sub_c.subscriptions.list())[0]
49+
os.environ["AZURE_SUBSCRIPTION_ID"] = account_info.subscription_id
50+
os.environ["AZURE_TENANT_ID"] = account_info.tenant_id
51+
os.environ["AZURE_RESOURCE_GROUP_NAME"] = account_info.display_name
4052
if resource_group is None:
4153
resource_group = os.getenv("AZURE_RESOURCE_GROUP_NAME")
4254
if resource_group is None:
@@ -52,14 +64,13 @@ def __init__(
5264
)
5365
self.subscription_id = subscription_id
5466
self.job_name = job_name
55-
self.credential = ManagedIdentityCredential()
5667

5768
self.client = ContainerAppsAPIClient(
5869
credential=self.credential, subscription_id=subscription_id
5970
)
6071
logger.debug("client initialized.")
6172

62-
def get_job_info(self, job_name):
73+
def get_job_info(self, job_name: str | None = None):
6374
"""
6475
Retrieve detailed information about a specific Container App job.
6576
@@ -69,12 +80,18 @@ def get_job_info(self, job_name):
6980
Returns:
7081
dict: Dictionary containing job details.
7182
"""
83+
if job_name is None:
84+
if self.job_name is None:
85+
raise ValueError("Please specify a job name.")
86+
else:
87+
job_name = self.job_name
88+
7289
for i in self.client.jobs.list_by_resource_group(self.resource_group):
7390
if i.name == job_name:
7491
job_info = i
7592
return job_info.as_dict()
7693

77-
def get_command_info(self, job_name):
94+
def get_command_info(self, job_name: str | None = None):
7895
"""
7996
Get command, image, and environment details for containers in a job.
8097
@@ -84,6 +101,12 @@ def get_command_info(self, job_name):
84101
Returns:
85102
list[dict]: List of container info dicts (name, image, command, args, env).
86103
"""
104+
if job_name is None:
105+
if self.job_name is None:
106+
raise ValueError("Please specify a job name.")
107+
else:
108+
job_name = self.job_name
109+
87110
for i in self.client.jobs.list_by_resource_group(self.resource_group):
88111
if i.name == job_name:
89112
job_info = i
@@ -115,7 +138,7 @@ def list_jobs(self):
115138
]
116139
return job_list
117140

118-
def check_job_exists(self, job_name):
141+
def check_job_exists(self, job_name: str):
119142
"""
120143
Check if a Container App job exists in the resource group.
121144
@@ -133,10 +156,10 @@ def check_job_exists(self, job_name):
133156

134157
def start_job(
135158
self,
136-
job_name: str = None,
137-
command: list[str] = None,
138-
args: list[str] = None,
139-
env: list[str] = None,
159+
job_name: str | None = None,
160+
command: list[str] | None = None,
161+
args: list[str] | None = None,
162+
env: list[str] | None = None,
140163
):
141164
"""
142165
Start a Container App job, optionally overriding command, args, or environment.
@@ -189,8 +212,41 @@ def start_job(
189212
new_containers.append(container)
190213
t = JobExecutionTemplate(containers=new_containers)
191214
logger.debug("submitting job start request.")
192-
self.client.jobs.begin_start(
215+
try:
216+
self.client.jobs.begin_start(
217+
resource_group_name=self.resource_group,
218+
job_name=job_name,
219+
template=t,
220+
)
221+
print(f"Started job {job_name}.")
222+
except Exception as e:
223+
logger.error(f"Failed to start job {job_name}: {e}")
224+
raise
225+
226+
def stop_job(self, job_name: str, job_execution_name: str):
227+
"""
228+
Stop a specific execution of an Azure Container App Job.
229+
230+
Args:
231+
job_name (str): Name of the Container App Job.
232+
job_execution_name (str): Name of the job execution to stop.
233+
234+
Returns:
235+
Any: Response object from the Azure SDK if successful, or None if an error occurs.
236+
237+
Raises:
238+
Exception: If the stop operation fails.
239+
"""
240+
try:
241+
response = self.client.jobs.begin_stop_execution(
193242
resource_group_name=self.resource_group,
194243
job_name=job_name,
195-
template=t,
244+
job_execution_name=job_execution_name,
245+
).result()
246+
logger.info(
247+
f"Job execution '{job_execution_name}' for job '{job_name}' stopped successfully."
196248
)
249+
return response
250+
except Exception as e:
251+
logger.error(f"Error stopping job execution: {e}")
252+
return None

changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ The versioning pattern is `major.minor.patch`.
88

99
---
1010

11+
## v0.0.2
12+
13+
- Updated ContainerAppClient
14+
- New documentation for ContainerAppClient
15+
1116
## v0.0.1
1217

1318
- Added CloudClient, ContainerAppClient, and other modules

docs/ContainerAppClient/index.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# ContainerAppClient Overview
2+
3+
The `ContainerAppClient` class provides a Python interface for managing Azure Container Apps jobs using the Azure SDK. It supports inspection, listing, existence checks, and starting jobs with custom commands and environment variables.
4+
5+
## Features
6+
- Authenticate using Azure Managed Identity and environment variables
7+
- List all jobs in a resource group
8+
- Retrieve detailed job information
9+
- Inspect container commands, images, and environment variables
10+
- Check if a job exists
11+
- Start jobs with custom commands, arguments, and environment variables
12+
- Stop a running container app job
13+
14+
## Setup
15+
16+
1. **Install dependencies**
17+
- The ContainerAppClient is available in the `cfa-cloudops` package. Install the package by executing the following code in your terminal.
18+
19+
```bash
20+
pip install git+https://github.com/CDCgov/cfa-cloudops.git
21+
```
22+
23+
2. **Environment variables**
24+
- The client uses Managed Identity credentialing by default, which can then pull in the subscription ID, resource group name, and tenant Id.
25+
- It's possible to pass in specific variables or use environment variables for Azure authentication:
26+
- `AZURE_SUBSCRIPTION_ID`
27+
- `AZURE_RESOURCE_GROUP_NAME`
28+
- `AZURE_TENANT_ID`
29+
- You can use a `.env` file and pass its path to the client, or set these variables manually.
30+
31+
## Usage Example
32+
33+
```python
34+
from cfa.cloudops import ContainerAppClient
35+
36+
# Simplest instantiation using Managed Identity
37+
client = ContainerAppClient()
38+
39+
# Initialize the client (dotenv_path is optional)
40+
client = ContainerAppClient(dotenv_path=".env", resource_group="my-rg", subscription_id="xxxx-xxxx", job_name="my-job")
41+
42+
# List all jobs in the resource group
43+
jobs = client.list_jobs()
44+
print("Jobs:", jobs)
45+
46+
# Check if a job exists
47+
exists = client.check_job_exists("my-job")
48+
print("Job exists:", exists)
49+
50+
# Get job information
51+
info = client.get_job_info("my-job")
52+
print("Job info:", info)
53+
54+
# Get command and environment info for containers in a job
55+
cmd_info = client.get_command_info("my-job")
56+
print("Command info:", cmd_info)
57+
58+
# Start a job (optionally override command, args, env)
59+
client.start_job(
60+
job_name="my-job",
61+
command=["python", "main.py"],
62+
args=["--input", "data.csv"],
63+
env=[{"name": "ENV_VAR", "value": "value"}]
64+
)
65+
66+
# Stop a job
67+
client.stop_job(
68+
job_name="my-job",
69+
job_execution_name="my-job-xxxxxxx"
70+
)
71+
```
72+
73+
## Method Reference
74+
75+
- `__init__(dotenv_path, resource_group, subscription_id, job_name)`
76+
- Initializes the client and loads environment variables.
77+
- `list_jobs()`
78+
- Returns a list of job names in the resource group.
79+
- `check_job_exists(job_name)`
80+
- Returns `True` if the job exists, `False` otherwise.
81+
- `get_job_info(job_name)`
82+
- Returns a dictionary of job details.
83+
- `get_command_info(job_name)`
84+
- Returns a list of container info dicts (name, image, command, args, env).
85+
- `start_job(job_name, command, args, env)`
86+
- Starts a job, optionally overriding command, args, and environment variables.
87+
- `stop_job(job_name, job_execution_name)`
88+
- Stops the specified job execution.
89+
90+
## Notes
91+
- The client uses Azure Managed Identity for authentication. Ensure your environment supports this (e.g., Azure VM, App Service, or configure credentials).
92+
- If you do not provide `resource_group`, `subscription_id`, or `job_name`, the client will attempt to use environment variables or values from the `.env` file.
93+
- All operations are logged using Python's `logging` module for easier debugging.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- [Overview](./overview.md)
44
- [CloudClient](./CloudClient/index.md)
5+
- [ContainerAppClient](./ContainerAppClient/index.md)
56
- [Automation](./automation.md)
67
- [Local Execution](./local.md)
78
- [Modules](./modules.md)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ nav:
55
- Overview: overview.md
66
- Automation: automation.md
77
- CloudClient: CloudClient/index.md
8+
- ContainerAppClient: ContainerAppClient/index.md
89
- Local Execution: local.md
910
- Modules: modules.md
1011
- Sample Files: files/index.md

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "cfa.cloudops"
3-
version = "0.0.1"
3+
version = "0.0.2"
44
description = "Cloud storage, batch, functions, MLOps assistance"
55
authors = [
66
{name = "Ryan Raasch", email = "xng3@cdc.gov"}

0 commit comments

Comments
 (0)