Podman API: Difference between revisions

From EDURange
Jump to navigationJump to search
Created page with "<nowiki>##</nowiki> Configuration <nowiki>###</nowiki> Config Configuration class that loads settings from environment variables. - MAX_WORKERS: Thread pool size (default: 10) - DEBUG: Enable debug mode (default: False) - REQUEST_TIMEOUT: Operation timeout in seconds (default: 30) <nowiki>##</nowiki> Custom Exceptions <nowiki>**</nowiki>PodmanAPIError(message, status_code=500)** Custom exception for API errors with HTTP status codes. <nowiki>##</nowiki> Decorato..."
 
No edit summary
 
(11 intermediate revisions by the same user not shown)
Line 1: Line 1:
<nowiki>##</nowiki> Configuration
[[Category:Demonstrations]]
In reference to:
https://github.com/edurange/demo-podman-api-extra


<nowiki>###</nowiki> Config
More design thoughts and justifications to be added later, these are just the protocol specs.


Configuration class that loads settings from environment variables.
This design is not meant to be followed, it's just my first sketch at listing everything we might need.


- MAX_WORKERS: Thread pool size (default: 10)
Testing instructions can be found [https://github.com/edurange/demo-podman-api-extra/blob/main/TESTING.md here], or run the demo script.


- DEBUG: Enable debug mode (default: False)
== Overview ==


- REQUEST_TIMEOUT: Operation timeout in seconds (default: 30)
* Container lifecycle management (create, start, stop, remove)
* Container listing and monitoring
* Log retrieval
* User management within containers
* File operations
* Command execution
* Health checks and metrics


<nowiki>##</nowiki> Custom Exceptions
=== Prerequisites ===


<nowiki>**</nowiki>PodmanAPIError(message, status_code=500)**
<ul>
<li><p>Python 3.7+</p></li>
<li><p>Podman installed and configured</p></li>
<li><p>Required Python packages:</p>
<p>pip install flask flask-cors podman</p></li></ul>


Custom exception for API errors with HTTP status codes.
=== Running the API ===


<nowiki>##</nowiki> Decorators
<pre>python3 demo-podman-api.py</pre>
The API will start on http://localhost:5000 by default.


<nowiki>**</nowiki>@handle_errors**
== Configuration ==


Decorator that catches exceptions and converts them to PodmanAPIError:
Configure the API using environment variables:


- Catches asyncio.TimeoutError → 408 Timeout
Example:


- Catches "not found" errors → 404 Not Found
<pre>export MAX_WORKERS=20
export DEBUG=true
export REQUEST_TIMEOUT=60
python3 demo-podman-api.py</pre>
== API Endpoints ==


- Catches other exceptions → 500 Internal Server Error
=== Base URL ===


<nowiki>**</nowiki>@validate_json(required_fields=None)**
All endpoints are prefixed with /api/v1


Decorator that validates JSON request data:
=== Container Management ===


- Ensures request contains valid JSON
==== Create Container ====


- Checks for required fields
'''POST''' /containers


- Returns 400 Bad Request if validation fails
Creates and starts a new container.


<nowiki>###</nowiki> Core API Class
'''Request Body:'''


<nowiki>**</nowiki>PodmanAPI.__init__()**
<pre>{
  &quot;image&quot;: &quot;alpine:latest&quot;,
  &quot;name&quot;: &quot;my-container&quot;,
  &quot;command&quot;: &quot;sleep 300&quot;,
  &quot;environment&quot;: {&quot;ENV_VAR&quot;: &quot;value&quot;},
  &quot;ports&quot;: {&quot;8080&quot;: &quot;80&quot;},
  &quot;volumes&quot;: {&quot;/host/path&quot;: &quot;/container/path&quot;},
  &quot;user&quot;: &quot;root&quot;
}</pre>
'''Required Fields:''' image, name


Initializes the API with a thread pool executor.
'''Response:'''


<nowiki>**</nowiki>PodmanAPI._get_client()**
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;id&quot;: &quot;abc123def456&quot;,
    &quot;name&quot;: &quot;my-container&quot;,
    &quot;status&quot;: &quot;running&quot;
  }
}</pre>
==== List Containers ====


Returns a new Podman client instance.
'''GET''' /containers


<nowiki>**</nowiki>PodmanAPI._run_async(func, timeout=30)**
Lists all containers (running and stopped).
 
Executes a function asynchronously in the thread pool with timeout.
 
<nowiki>**</nowiki>PodmanAPI._handle_exec_result(result)**
 
Helper that normalizes exec_run results to (exit_code, output_string).
 
<nowiki>##</nowiki> Container Management
 
<nowiki>**</nowiki>create_container(config)**


Creates and starts a new container.
'''Response:'''


- Input: {image, name, command?, environment?, ports?, volumes?, user?}
<pre>{
  &quot;success&quot;: true,
  &quot;containers&quot;: [
    {
      &quot;id&quot;: &quot;abc123def456&quot;,
      &quot;name&quot;: &quot;my-container&quot;,
      &quot;status&quot;: &quot;running&quot;,
      &quot;image&quot;: &quot;alpine:latest&quot;
    }
  ]
}</pre>
==== Start Container ====


- Returns: {id, name, status}
'''POST''' /containers/{name}/start


<nowiki>**</nowiki>start_container(name)**
Starts a stopped container.


Starts an existing container.
'''Response:'''


- Input: Container name
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;name&quot;: &quot;my-container&quot;,
    &quot;status&quot;: &quot;started&quot;
  }
}</pre>
==== Stop Container ====


- Returns: {name, status}
'''POST''' /containers/{name}/stop
 
<nowiki>**</nowiki>stop_container(name)**


Stops a running container.
Stops a running container.


- Input: Container name
'''Response:'''


- Returns: {name, status}
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;name&quot;: &quot;my-container&quot;,
    &quot;status&quot;: &quot;stopped&quot;
  }
}</pre>
==== Remove Container ====


<nowiki>**</nowiki>remove_container(name, force=False)**
'''DELETE''' /containers/{name}


Removes a container.
Removes a container.


- Input: Container name, force flag
'''Query Parameters:''' - force (boolean): Force removal of running container
 
- Returns: {name, removed}
 
<nowiki>**</nowiki>list_containers()**


Lists all containers (running and stopped).
'''Response:'''


- Returns: Array of {id, name, status, image}
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;name&quot;: &quot;my-container&quot;,
    &quot;removed&quot;: true
  }
}</pre>
==== Get Container Logs ====


<nowiki>**</nowiki>get_container_logs(name, tail=100)**
'''GET''' /containers/{name}/logs


Retrieves container logs.
Retrieves container logs.


- Input: Container name, number of lines
'''Query Parameters:''' - tail (integer): Number of lines to retrieve (default: 100)


- Returns: {logs}
'''Response:'''


Host Operations
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;logs&quot;: &quot;Container log output here...&quot;
  }
}</pre>
=== Container Operations ===


<nowiki>**</nowiki>add_user(container_name, user_config)**
==== Execute Command ====


Creates a user inside a container.
'''POST''' /containers/{name}/exec
 
- Input: Container name, {username, password?, shell?}
 
- Returns: {username, created}
 
<nowiki>**</nowiki>add_file(container_name, file_config)**
 
Creates a file inside a container.
 
- Input: Container name, {dest_path, content}
 
- Returns: {dest_path, size}
 
<nowiki>**</nowiki>execute_command(container_name, command, user=None)**


Executes a command inside a container.
Executes a command inside a container.


- Input: Container name, command string, optional user
'''Request Body:'''


- Returns: {exit_code, output, success}
<pre>{
  &quot;command&quot;: &quot;ls -la /tmp&quot;,
  &quot;user&quot;: &quot;root&quot;
}</pre>
'''Required Fields:''' command


<nowiki>**</nowiki>shutdown()**
'''Response:'''


Gracefully shuts down the thread pool executor.
<pre>{
  &quot;success&quot;: true,
  &quot;execution&quot;: {
    &quot;exit_code&quot;: 0,
    &quot;output&quot;: &quot;total 4\ndrwxrwxrwt 2 root root 4096 Jan 1 00:00 .\n&quot;,
    &quot;success&quot;: true
  }
}</pre>
==== Add User ====


<nowiki>##</nowiki> Error Handlers
'''POST''' /containers/{name}/users


<nowiki>**</nowiki>handle_podman_error(error)**
Creates a new user inside a container.


Flask error handler for PodmanAPIError exceptions.
'''Request Body:'''


- Returns structured JSON error response with timestamp
<pre>{
  &quot;username&quot;: &quot;newuser&quot;,
  &quot;password&quot;: &quot;password123&quot;,
  &quot;shell&quot;: &quot;/bin/bash&quot;
}</pre>
'''Required Fields:''' username


<nowiki>**</nowiki>handle_internal_error(error)**
'''Response:'''


Flask error handler for 500 Internal Server Error.
<pre>{
  &quot;success&quot;: true,
  &quot;user&quot;: {
    &quot;username&quot;: &quot;newuser&quot;,
    &quot;created&quot;: true
  }
}</pre>
==== Add File ====


- Logs error and returns generic error response
'''POST''' /containers/{name}/files


<nowiki>##</nowiki> API Routes
Creates a file inside a container.
 
<nowiki>**</nowiki>POST /api/v1/containers**
 
Creates a new container. Requires image and name fields.
 
<nowiki>**</nowiki>POST /api/v1/containers/<name>/start**
 
Starts the specified container.
 
<nowiki>**</nowiki>POST /api/v1/containers/<name>/stop**
 
Stops the specified container.
 
<nowiki>**</nowiki>DELETE /api/v1/containers/<name>?force=bool**
 
Removes the specified container. Optional force parameter.


<nowiki>**</nowiki>GET /api/v1/containers**
'''Request Body:'''


Lists all containers.
<pre>{
  &quot;dest_path&quot;: &quot;/tmp/myfile.txt&quot;,
  &quot;content&quot;: &quot;Hello, World!\nThis is file content.&quot;
}</pre>
'''Required Fields:''' dest_path, content


<nowiki>**</nowiki>GET /api/v1/containers/<name>/logs?tail=int**
'''Response:'''


Gets container logs. Optional tail parameter (default: 100).
<pre>{
  &quot;success&quot;: true,
  &quot;file&quot;: {
    &quot;dest_path&quot;: &quot;/tmp/myfile.txt&quot;,
    &quot;size&quot;: 32
  }
}</pre>
=== System Endpoints ===


<nowiki>**</nowiki>POST /api/v1/containers/<name>/users**
==== Health Check ====


Adds a user to the container. Requires username field.
'''GET''' /health


<nowiki>**</nowiki>POST /api/v1/containers/<name>/files**
Checks API and Podman connectivity.


Adds a file to the container. Requires dest_path and content fields.
'''Response:'''


<nowiki>**</nowiki>POST /api/v1/containers/<name>/exec**
<pre>{
  &quot;status&quot;: &quot;healthy&quot;,
  &quot;podman&quot;: &quot;connected&quot;,
  &quot;timestamp&quot;: &quot;2025-01-19T15:47:19.123456&quot;
}</pre>
==== Metrics ====


Executes a command in the container. Requires command field.
'''GET''' /metrics


<nowiki>**</nowiki>GET /api/v1/health**
Returns API performance metrics.


Health check endpoint. Returns Podman connection status.
'''Response:'''


<nowiki>**</nowiki>GET /api/v1/metrics**
<pre>{
  &quot;uptime_seconds&quot;: 3600,
  &quot;uptime_human&quot;: &quot;1:00:00&quot;,
  &quot;active_threads&quot;: 2,
  &quot;max_workers&quot;: 10,
  &quot;timestamp&quot;: &quot;2025-01-19T15:47:19.123456&quot;
}</pre>
== Error Handling ==


Returns API metrics including uptime and thread pool status.
=== Error Response Format ===


<nowiki>##</nowiki> Signal Handlers
All errors follow a consistent format:


<nowiki>**</nowiki>shutdown_handler(sig, frame)**
<pre>{
  &quot;success&quot;: false,
  &quot;error&quot;: {
    &quot;message&quot;: &quot;Error description&quot;,
    &quot;code&quot;: 404,
    &quot;type&quot;: &quot;PodmanAPIError&quot;
  },
  &quot;timestamp&quot;: &quot;2025-01-19T15:47:19.123456&quot;
}</pre>
=== HTTP Status Codes ===


Handles SIGINT and SIGTERM signals for graceful shutdown.
{|
! Code
! Description
|-
| 200
| Success
|-
| 400
| Bad Request (missing required fields)
|-
| 404
| Resource not found (container, image, etc.)
|-
| 408
| Request timeout
|-
| 500
| Internal server error
|-
| 503
| Service unavailable (Podman connection issues)
|}


- Logs signal details including source file and line number
=== Common Error Types ===


- Shuts down thread pool and exits cleanly
* '''PodmanAPIError''': Podman-related errors
* '''InternalError''': Server-side errors
* '''ValidationError''': Request validation failures


<nowiki>##</nowiki> Response Format
== Examples ==


Successful requests will look like this:
=== Complete Container Workflow ===


```
<pre># 1. Check API health
curl -X GET http://localhost:5000/api/v1/health | jq


{
# 2. Create and start container
curl -X POST http://localhost:5000/api/v1/containers \
  -H &quot;Content-Type: application/json&quot; \
  -d '{
    &quot;image&quot;: &quot;alpine:latest&quot;,
    &quot;name&quot;: &quot;demo-container&quot;,
    &quot;command&quot;: &quot;sleep 300&quot;
  }' | jq


  "success": true,
# 3. Add a user
curl -X POST http://localhost:5000/api/v1/containers/demo-container/users \
  -H &quot;Content-Type: application/json&quot; \
  -d '{
    &quot;username&quot;: &quot;testuser&quot;,
    &quot;password&quot;: &quot;testpass&quot;,
    &quot;shell&quot;: &quot;/bin/sh&quot;
  }' | jq


  "container|user|file|execution": { ... },
# 4. Create a file
curl -X POST http://localhost:5000/api/v1/containers/demo-container/files \
  -H &quot;Content-Type: application/json&quot; \
  -d '{
    &quot;dest_path&quot;: &quot;/tmp/demo.txt&quot;,
    &quot;content&quot;: &quot;Hello from the API!&quot;
  }' | jq


  "timestamp": "ISO-8601-timestamp"
# 5. Execute commands
curl -X POST http://localhost:5000/api/v1/containers/demo-container/exec \
  -H &quot;Content-Type: application/json&quot; \
  -d '{&quot;command&quot;: &quot;cat /tmp/demo.txt&quot;}' | jq


}
# 6. Check logs
curl -X GET &quot;http://localhost:5000/api/v1/containers/demo-container/logs?tail=20&quot; | jq


```
# 7. Stop and remove container
curl -X POST http://localhost:5000/api/v1/containers/demo-container/stop | jq
curl -X DELETE &quot;http://localhost:5000/api/v1/containers/demo-container?force=true&quot; | jq</pre>
=== Python Client Example ===


Errors will look like this:
<pre>import requests
import json


```
class PodmanAPIClient:
    def __init__(self, base_url=&quot;http://localhost:5000/api/v1&quot;):
        self.base_url = base_url
   
    def create_container(self, image, name, **kwargs):
        data = {&quot;image&quot;: image, &quot;name&quot;: name, **kwargs}
        response = requests.post(f&quot;{self.base_url}/containers&quot;, json=data)
        return response.json()
   
    def execute_command(self, container_name, command, user=None):
        data = {&quot;command&quot;: command}
        if user:
            data[&quot;user&quot;] = user
        response = requests.post(
            f&quot;{self.base_url}/containers/{container_name}/exec&quot;,
            json=data
        )
        return response.json()


{
# Usage
client = PodmanAPIClient()
result = client.create_container(&quot;alpine:latest&quot;, &quot;test-container&quot;)
print(json.dumps(result, indent=2))</pre>
== Security Considerations ==


  "success": false,
* The API runs without authentication by default
* Container commands are executed with full privileges
* File operations can write anywhere in the container filesystem
* Consider implementing authentication and authorization for production use
* Validate and sanitize all user inputs
* Consider running the API behind a reverse proxy with rate limiting


  "error": {
== Troubleshooting ==


    "message": "Error description",
=== Common Issues ===


    "code": 400,
# '''“Container not found” errors''':  
Ensure the container name is correct and the container exists


    "type": "ErrorType"
# '''“Image not found” errors''':  
Pull the image with <code>podman pull &lt;image&gt;</code> first


  },
# '''Permission errors''':
Ensure Podman is properly configured for your user


  "timestamp": "ISO-8601-timestamp"
# '''Connection errors''':  
Check that Podman service is running


}
=== Debug Mode ===
Enable debug mode for detailed logging:
<pre>export DEBUG=true
python3 demo-podman-api.py</pre>


```
=== Logs ===
The API logs all operations. Check the console output for detailed error information.

Latest revision as of 18:14, 19 July 2025

In reference to: https://github.com/edurange/demo-podman-api-extra

More design thoughts and justifications to be added later, these are just the protocol specs.

This design is not meant to be followed, it's just my first sketch at listing everything we might need.

Testing instructions can be found here, or run the demo script.

Overview

  • Container lifecycle management (create, start, stop, remove)
  • Container listing and monitoring
  • Log retrieval
  • User management within containers
  • File operations
  • Command execution
  • Health checks and metrics

Prerequisites

  • Python 3.7+

  • Podman installed and configured

  • Required Python packages:

    pip install flask flask-cors podman

Running the API

python3 demo-podman-api.py

The API will start on http://localhost:5000 by default.

Configuration

Configure the API using environment variables:

Example:

export MAX_WORKERS=20
export DEBUG=true
export REQUEST_TIMEOUT=60
python3 demo-podman-api.py

API Endpoints

Base URL

All endpoints are prefixed with /api/v1

Container Management

Create Container

POST /containers

Creates and starts a new container.

Request Body:

{
  "image": "alpine:latest",
  "name": "my-container",
  "command": "sleep 300",
  "environment": {"ENV_VAR": "value"},
  "ports": {"8080": "80"},
  "volumes": {"/host/path": "/container/path"},
  "user": "root"
}

Required Fields: image, name

Response:

{
  "success": true,
  "container": {
    "id": "abc123def456",
    "name": "my-container",
    "status": "running"
  }
}

List Containers

GET /containers

Lists all containers (running and stopped).

Response:

{
  "success": true,
  "containers": [
    {
      "id": "abc123def456",
      "name": "my-container",
      "status": "running",
      "image": "alpine:latest"
    }
  ]
}

Start Container

POST /containers/{name}/start

Starts a stopped container.

Response:

{
  "success": true,
  "container": {
    "name": "my-container",
    "status": "started"
  }
}

Stop Container

POST /containers/{name}/stop

Stops a running container.

Response:

{
  "success": true,
  "container": {
    "name": "my-container",
    "status": "stopped"
  }
}

Remove Container

DELETE /containers/{name}

Removes a container.

Query Parameters: - force (boolean): Force removal of running container

Response:

{
  "success": true,
  "container": {
    "name": "my-container",
    "removed": true
  }
}

Get Container Logs

GET /containers/{name}/logs

Retrieves container logs.

Query Parameters: - tail (integer): Number of lines to retrieve (default: 100)

Response:

{
  "success": true,
  "container": {
    "logs": "Container log output here..."
  }
}

Container Operations

Execute Command

POST /containers/{name}/exec

Executes a command inside a container.

Request Body:

{
  "command": "ls -la /tmp",
  "user": "root"
}

Required Fields: command

Response:

{
  "success": true,
  "execution": {
    "exit_code": 0,
    "output": "total 4\ndrwxrwxrwt 2 root root 4096 Jan 1 00:00 .\n",
    "success": true
  }
}

Add User

POST /containers/{name}/users

Creates a new user inside a container.

Request Body:

{
  "username": "newuser",
  "password": "password123",
  "shell": "/bin/bash"
}

Required Fields: username

Response:

{
  "success": true,
  "user": {
    "username": "newuser",
    "created": true
  }
}

Add File

POST /containers/{name}/files

Creates a file inside a container.

Request Body:

{
  "dest_path": "/tmp/myfile.txt",
  "content": "Hello, World!\nThis is file content."
}

Required Fields: dest_path, content

Response:

{
  "success": true,
  "file": {
    "dest_path": "/tmp/myfile.txt",
    "size": 32
  }
}

System Endpoints

Health Check

GET /health

Checks API and Podman connectivity.

Response:

{
  "status": "healthy",
  "podman": "connected",
  "timestamp": "2025-01-19T15:47:19.123456"
}

Metrics

GET /metrics

Returns API performance metrics.

Response:

{
  "uptime_seconds": 3600,
  "uptime_human": "1:00:00",
  "active_threads": 2,
  "max_workers": 10,
  "timestamp": "2025-01-19T15:47:19.123456"
}

Error Handling

Error Response Format

All errors follow a consistent format:

{
  "success": false,
  "error": {
    "message": "Error description",
    "code": 404,
    "type": "PodmanAPIError"
  },
  "timestamp": "2025-01-19T15:47:19.123456"
}

HTTP Status Codes

Code Description
200 Success
400 Bad Request (missing required fields)
404 Resource not found (container, image, etc.)
408 Request timeout
500 Internal server error
503 Service unavailable (Podman connection issues)

Common Error Types

  • PodmanAPIError: Podman-related errors
  • InternalError: Server-side errors
  • ValidationError: Request validation failures

Examples

Complete Container Workflow

# 1. Check API health
curl -X GET http://localhost:5000/api/v1/health | jq

# 2. Create and start container
curl -X POST http://localhost:5000/api/v1/containers \
  -H "Content-Type: application/json" \
  -d '{
    "image": "alpine:latest",
    "name": "demo-container",
    "command": "sleep 300"
  }' | jq

# 3. Add a user
curl -X POST http://localhost:5000/api/v1/containers/demo-container/users \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "password": "testpass",
    "shell": "/bin/sh"
  }' | jq

# 4. Create a file
curl -X POST http://localhost:5000/api/v1/containers/demo-container/files \
  -H "Content-Type: application/json" \
  -d '{
    "dest_path": "/tmp/demo.txt",
    "content": "Hello from the API!"
  }' | jq

# 5. Execute commands
curl -X POST http://localhost:5000/api/v1/containers/demo-container/exec \
  -H "Content-Type: application/json" \
  -d '{"command": "cat /tmp/demo.txt"}' | jq

# 6. Check logs
curl -X GET "http://localhost:5000/api/v1/containers/demo-container/logs?tail=20" | jq

# 7. Stop and remove container
curl -X POST http://localhost:5000/api/v1/containers/demo-container/stop | jq
curl -X DELETE "http://localhost:5000/api/v1/containers/demo-container?force=true" | jq

Python Client Example

import requests
import json

class PodmanAPIClient:
    def __init__(self, base_url="http://localhost:5000/api/v1"):
        self.base_url = base_url
    
    def create_container(self, image, name, **kwargs):
        data = {"image": image, "name": name, **kwargs}
        response = requests.post(f"{self.base_url}/containers", json=data)
        return response.json()
    
    def execute_command(self, container_name, command, user=None):
        data = {"command": command}
        if user:
            data["user"] = user
        response = requests.post(
            f"{self.base_url}/containers/{container_name}/exec", 
            json=data
        )
        return response.json()

# Usage
client = PodmanAPIClient()
result = client.create_container("alpine:latest", "test-container")
print(json.dumps(result, indent=2))

Security Considerations

  • The API runs without authentication by default
  • Container commands are executed with full privileges
  • File operations can write anywhere in the container filesystem
  • Consider implementing authentication and authorization for production use
  • Validate and sanitize all user inputs
  • Consider running the API behind a reverse proxy with rate limiting

Troubleshooting

Common Issues

  1. “Container not found” errors:

Ensure the container name is correct and the container exists

  1. “Image not found” errors:

Pull the image with podman pull <image> first

  1. Permission errors:

Ensure Podman is properly configured for your user

  1. Connection errors:

Check that Podman service is running

Debug Mode

Enable debug mode for detailed logging:

export DEBUG=true
python3 demo-podman-api.py

Logs

The API logs all operations. Check the console output for detailed error information.