Podman API: Difference between revisions

From EDURange
Jump to navigationJump to search
No edit summary
No edit summary
 
(3 intermediate revisions by the same user not shown)
Line 7: Line 7:
This design is not meant to be followed, it's just my first sketch at listing everything we might need.
This design is not meant to be followed, it's just my first sketch at listing everything we might need.


[TODO: Add the curl scripts I used for testing]
Testing instructions can be found [https://github.com/edurange/demo-podman-api-extra/blob/main/TESTING.md here], or run the demo script.
== Configuration ==


<span id="config"></span>
== Overview ==
=== Config ===


Configuration class that loads settings from environment variables.
* 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


* - MAX_WORKERS: Thread pool size (default: 10)
=== Prerequisites ===
* - DEBUG: Enable debug mode (default: False)
* - REQUEST_TIMEOUT: Operation timeout in seconds (default: 30)


<span id="custom-exceptions"></span>
<ul>
== Custom Exceptions ==
<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>


'''PodmanAPIError(message, status_code=500)'''
=== Running the API ===


Custom exception for API errors with HTTP status codes.
<pre>python3 demo-podman-api.py</pre>
The API will start on http://localhost:5000 by default.


<span id="decorators"></span>
== Configuration ==
== Decorators ==


'''@handle_errors'''
Configure the API using environment variables:


* Decorator that catches exceptions and converts them to PodmanAPIError:  
Example:
* - Catches asyncio.TimeoutError → 408 Timeout
* - Catches “not found” errors → 404 Not Found
* - Catches other exceptions → 500 Internal Server Error


'''@validate_json(required_fields=None)'''
<pre>export MAX_WORKERS=20
export DEBUG=true
export REQUEST_TIMEOUT=60
python3 demo-podman-api.py</pre>
== API Endpoints ==


Decorator that validates JSON request data:
=== Base URL ===


* - Ensures request contains valid JSON
All endpoints are prefixed with /api/v1
* - Checks for required fields
* - Returns 400 Bad Request if validation fails


<span id="core-api-class"></span>
=== Container Management ===
=== Core API Class ===


===== PodmanAPI.__init__() =====
==== Create Container ====
Initializes the API with a thread pool executor.


'''POST''' /containers


'''PodmanAPI._get_client()'''
Creates and starts a new container.


Returns a new Podman client instance.
'''Request Body:'''


<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


'''PodmanAPI._run_async(func, timeout=30)'''  
'''Response:'''


Executes a function asynchronously in the thread pool with timeout.
<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 ====


'''GET''' /containers


'''PodmanAPI._handle_exec_result(result)'''
Lists all containers (running and stopped).


Helper that normalizes exec_run results to (exit_code, output_string).
'''Response:'''


<span id="container-management"></span>
<pre>{
== Container Management ==
  &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 ====


'''create_container(config)'''  
'''POST''' /containers/{name}/start


Creates and starts a new container.  
Starts a stopped container.


* - Input: {image, name, command?, environment?, ports?, volumes?, user?}
'''Response:'''
* - Returns: {id, name, status}


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


'''start_container(name)'''  
'''POST''' /containers/{name}/stop


Starts an existing 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 ====


'''stop_container(name)'''  
'''DELETE''' /containers/{name}


Stops a running container.  
Removes a container.


* - Input: Container name
'''Query Parameters:''' - force (boolean): Force removal of running container
* - Returns: {name, status}


'''Response:'''


'''remove_container(name, force=False)'''
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;name&quot;: &quot;my-container&quot;,
    &quot;removed&quot;: true
  }
}</pre>
==== Get Container Logs ====


Removes a container.
'''GET''' /containers/{name}/logs


* - Input: Container name, force flag
Retrieves container logs.
* - Returns: {name, removed}


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


'''list_containers()'''
'''Response:'''


Lists all containers (running and stopped).  
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;logs&quot;: &quot;Container log output here...&quot;
  }
}</pre>
=== Container Operations ===


* - Returns: Array of {id, name, status, image}
==== Execute Command ====


'''POST''' /containers/{name}/exec


'''get_container_logs(name, tail=100)'''
Executes a command inside a container.


Retrieves container logs.
'''Request Body:'''


* - Input: Container name, number of lines
<pre>{
* - Returns: {logs}
  &quot;command&quot;: &quot;ls -la /tmp&quot;,
  &quot;user&quot;: &quot;root&quot;
}</pre>
'''Required Fields:''' command


'''Response:'''


'''Host Operations'''
<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 ====


'''add_user(container_name, user_config)'''  
'''POST''' /containers/{name}/users


Creates a user inside a container.  
Creates a new user inside a container.


* - Input: Container name, {username, password?, shell?}
'''Request Body:'''
* - Returns: {username, created}


<pre>{
  &quot;username&quot;: &quot;newuser&quot;,
  &quot;password&quot;: &quot;password123&quot;,
  &quot;shell&quot;: &quot;/bin/bash&quot;
}</pre>
'''Required Fields:''' username


'''add_file(container_name, file_config)'''  
'''Response:'''


Creates a file inside a container.
<pre>{
  &quot;success&quot;: true,
  &quot;user&quot;: {
    &quot;username&quot;: &quot;newuser&quot;,
    &quot;created&quot;: true
  }
}</pre>
==== Add File ====


* - Input: Container name, {dest_path, content}
'''POST''' /containers/{name}/files
* - Returns: {dest_path, size}


'''execute_command(container_name, command, user=None)'''
Creates a file inside a container.


Executes a command inside a container.
'''Request Body:'''


* - Input: Container name, command string, optional user
<pre>{
* - Returns: {exit_code, output, success}
  &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


'''Response:'''


'''shutdown()'''
<pre>{
  &quot;success&quot;: true,
  &quot;file&quot;: {
    &quot;dest_path&quot;: &quot;/tmp/myfile.txt&quot;,
    &quot;size&quot;: 32
  }
}</pre>
=== System Endpoints ===


Gracefully shuts down the thread pool executor.
==== Health Check ====


<span id="error-handlers"></span>
'''GET''' /health
== Error Handlers ==


'''handle_podman_error(error)'''
Checks API and Podman connectivity.


Flask error handler for PodmanAPIError exceptions.
'''Response:'''


* - Returns structured JSON error response with timestamp
<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 ====


'''GET''' /metrics


'''handle_internal_error(error)'''
Returns API performance metrics.


Flask error handler for 500 Internal Server Error.
'''Response:'''


* - Logs error and returns generic error response
<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 ==


<span id="api-routes"></span>
=== Error Response Format ===
== API Routes ==


'''POST /api/v1/containers'''
All errors follow a consistent format:


Creates a new container. Requires image and name fields.
<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 ===


{|
! 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)
|}


'''POST /api/v1/containers/<name>/start'''
=== Common Error Types ===


Starts the specified container.
* '''PodmanAPIError''': Podman-related errors
* '''InternalError''': Server-side errors
* '''ValidationError''': Request validation failures


== Examples ==


'''POST /api/v1/containers/<name>/stop'''
=== Complete Container Workflow ===


Stops the specified container.
<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


'''DELETE /api/v1/containers/<name>?force=bool'''  
# 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


Removes the specified container. Optional force parameter.
# 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


# 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


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


Lists all containers.
# 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 ===


<pre>import requests
import json


'''GET /api/v1/containers/<name>/logs?tail=int'''
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()


Gets container logs. Optional tail parameter (default: 100).
# Usage
client = PodmanAPIClient()
result = client.create_container(&quot;alpine:latest&quot;, &quot;test-container&quot;)
print(json.dumps(result, indent=2))</pre>
== 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


'''POST /api/v1/containers/<name>/users'''
== Troubleshooting ==


Adds a user to the container. Requires username field.
=== Common Issues ===


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


'''POST /api/v1/containers/<name>/files'''
# '''“Image not found” errors''':
Pull the image with <code>podman pull &lt;image&gt;</code> first


Adds a file to the container. Requires dest_path and content fields.
# '''Permission errors''':
Ensure Podman is properly configured for your user


# '''Connection errors''':
Check that Podman service is running


'''POST /api/v1/containers/<name>/exec'''
=== Debug Mode ===
Enable debug mode for detailed logging:
<pre>export DEBUG=true
python3 demo-podman-api.py</pre>


Executes a command in the container. Requires command field.
=== Logs ===
 
The API logs all operations. Check the console output for detailed error information.
 
'''GET /api/v1/health'''
 
Health check endpoint. Returns Podman connection status.
 
 
'''GET /api/v1/metrics'''
 
Returns API metrics including uptime and thread pool status.
 
<span id="signal-handlers"></span>
== Signal Handlers ==
 
'''shutdown_handler(sig, frame)'''
 
Handles SIGINT and SIGTERM signals for graceful shutdown.
 
* - Logs signal details including source file and line number
* - Shuts down thread pool and exits cleanly
 
<span id="response-format"></span>
== Response Format ==
 
Successful requests will look like this:
 
<pre>{
  &quot;success&quot;: true,
  &quot;container|user|file|execution&quot;: { ... },
  &quot;timestamp&quot;: &quot;ISO-8601-timestamp&quot;
}</pre>
Errors will look like this:
 
<pre>{
  &quot;success&quot;: false,
  &quot;error&quot;: {
    &quot;message&quot;: &quot;Error description&quot;,
    &quot;code&quot;: 400,
    &quot;type&quot;: &quot;ErrorType&quot;
  },
  &quot;timestamp&quot;: &quot;ISO-8601-timestamp&quot;
}</pre>

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.