Podman API: Difference between revisions

From EDURange
Jump to navigationJump to search
No edit summary
No edit summary
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
[[Category:Demonstrations]]
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.
More design thoughts and justifications to be added later, these are just the protocol specs.
Line 5: 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.
 
== 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 ===
 
<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>
 
=== Running the API ===
 
<pre>python3 demo-podman-api.py</pre>
The API will start on http://localhost:5000 by default.
 
== Configuration ==
== Configuration ==


<span id="config"></span>
Configure the API using environment variables:
=== 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)
Example:


<span id="custom-exceptions"></span>
<pre>export MAX_WORKERS=20
== Custom Exceptions ==
export DEBUG=true
export REQUEST_TIMEOUT=60
python3 demo-podman-api.py</pre>
== API Endpoints ==


'''PodmanAPIError(message, status_code=500)''' Custom exception for API errors with HTTP status codes.
=== Base URL ===


<span id="decorators"></span>
All endpoints are prefixed with /api/v1
== Decorators ==


'''@handle_errors''' Decorator that catches exceptions and converts them to PodmanAPIError: - Catches asyncio.TimeoutError → 408 Timeout - Catches “not found” errors → 404 Not Found - Catches other exceptions → 500 Internal Server Error
=== Container Management ===


'''@validate_json(required_fields=None)''' Decorator that validates JSON request data: - Ensures request contains valid JSON - Checks for required fields - Returns 400 Bad Request if validation fails
==== Create Container ====


<span id="core-api-class"></span>
'''POST''' /containers
=== Core API Class ===


**PodmanAPI.__init__()** Initializes the API with a thread pool executor.
Creates and starts a new container.


'''PodmanAPI._get_client()''' Returns a new Podman client instance.
'''Request Body:'''


'''PodmanAPI._run_async(func, timeout=30)''' Executes a function asynchronously in the thread pool with timeout.
<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._handle_exec_result(result)''' 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;container&quot;: {
    &quot;id&quot;: &quot;abc123def456&quot;,
    &quot;name&quot;: &quot;my-container&quot;,
    &quot;status&quot;: &quot;running&quot;
  }
}</pre>
==== List Containers ====


'''create_container(config)''' Creates and starts a new container. - Input: {image, name, command?, environment?, ports?, volumes?, user?} - Returns: {id, name, status}
'''GET''' /containers


'''start_container(name)''' Starts an existing container. - Input: Container name - Returns: {name, status}
Lists all containers (running and stopped).


'''stop_container(name)''' Stops a running container. - Input: Container name - Returns: {name, status}
'''Response:'''


'''remove_container(name, force=False)''' Removes a container. - Input: Container name, force flag - Returns: {name, removed}
<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 ====


'''list_containers()''' Lists all containers (running and stopped). - Returns: Array of {id, name, status, image}
'''POST''' /containers/{name}/start


'''get_container_logs(name, tail=100)''' Retrieves container logs. - Input: Container name, number of lines - Returns: {logs}
Starts a stopped container.


Host Operations
'''Response:'''


'''add_user(container_name, user_config)''' Creates a user inside a container. - Input: Container name, {username, password?, shell?} - Returns: {username, created}
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;name&quot;: &quot;my-container&quot;,
    &quot;status&quot;: &quot;started&quot;
  }
}</pre>
==== Stop Container ====


'''add_file(container_name, file_config)''' Creates a file inside a container. - Input: Container name, {dest_path, content} - Returns: {dest_path, size}
'''POST''' /containers/{name}/stop


'''execute_command(container_name, command, user=None)''' Executes a command inside a container. - Input: Container name, command string, optional user - Returns: {exit_code, output, success}
Stops a running container.


'''shutdown()''' Gracefully shuts down the thread pool executor.
'''Response:'''


<span id="error-handlers"></span>
<pre>{
== Error Handlers ==
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;name&quot;: &quot;my-container&quot;,
    &quot;status&quot;: &quot;stopped&quot;
  }
}</pre>
==== Remove Container ====


'''handle_podman_error(error)''' Flask error handler for PodmanAPIError exceptions. - Returns structured JSON error response with timestamp
'''DELETE''' /containers/{name}


'''handle_internal_error(error)''' Flask error handler for 500 Internal Server Error. - Logs error and returns generic error response
Removes a container.


<span id="api-routes"></span>
'''Query Parameters:''' - force (boolean): Force removal of running container
== API Routes ==


'''POST /api/v1/containers''' Creates a new container. Requires image and name fields.
'''Response:'''


'''POST /api/v1/containers/<name>/start''' Starts the specified container.
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;name&quot;: &quot;my-container&quot;,
    &quot;removed&quot;: true
  }
}</pre>
==== Get Container Logs ====
 
'''GET''' /containers/{name}/logs


'''POST /api/v1/containers/<name>/stop''' Stops the specified container.
Retrieves container logs.


'''DELETE /api/v1/containers/<name>?force=bool''' Removes the specified container. Optional force parameter.
'''Query Parameters:''' - tail (integer): Number of lines to retrieve (default: 100)


'''GET /api/v1/containers''' Lists all containers.
'''Response:'''


'''GET /api/v1/containers/<name>/logs?tail=int''' Gets container logs. Optional tail parameter (default: 100).
<pre>{
  &quot;success&quot;: true,
  &quot;container&quot;: {
    &quot;logs&quot;: &quot;Container log output here...&quot;
  }
}</pre>
=== Container Operations ===


'''POST /api/v1/containers/<name>/users''' Adds a user to the container. Requires username field.
==== Execute Command ====


'''POST /api/v1/containers/<name>/files''' Adds a file to the container. Requires dest_path and content fields.
'''POST''' /containers/{name}/exec
 
Executes a command inside a container.
 
'''Request Body:'''
 
<pre>{
  &quot;command&quot;: &quot;ls -la /tmp&quot;,
  &quot;user&quot;: &quot;root&quot;
}</pre>
'''Required Fields:''' command


'''POST /api/v1/containers/<name>/exec''' Executes a command in the container. Requires command field.
'''Response:'''


'''GET /api/v1/health''' Health check endpoint. Returns Podman connection status.
<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 ====


'''GET /api/v1/metrics''' Returns API metrics including uptime and thread pool status.
'''POST''' /containers/{name}/users


<span id="signal-handlers"></span>
Creates a new user inside a container.
== 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
'''Request Body:'''


<span id="response-format"></span>
<pre>{
== Response Format ==
  &quot;username&quot;: &quot;newuser&quot;,
  &quot;password&quot;: &quot;password123&quot;,
  &quot;shell&quot;: &quot;/bin/bash&quot;
}</pre>
'''Required Fields:''' username


Successful requests will look like this:
'''Response:'''


<pre>{
<pre>{
   &quot;success&quot;: true,
   &quot;success&quot;: true,
   &quot;container|user|file|execution&quot;: { ... },
   &quot;user&quot;: {
  &quot;timestamp&quot;: &quot;ISO-8601-timestamp&quot;
    &quot;username&quot;: &quot;newuser&quot;,
    &quot;created&quot;: true
  }
}</pre>
}</pre>
Errors will look like this:
==== Add File ====
 
'''POST''' /containers/{name}/files
 
Creates a file inside a container.
 
'''Request Body:'''
 
<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
 
'''Response:'''
 
<pre>{
  &quot;success&quot;: true,
  &quot;file&quot;: {
    &quot;dest_path&quot;: &quot;/tmp/myfile.txt&quot;,
    &quot;size&quot;: 32
  }
}</pre>
=== System Endpoints ===
 
==== Health Check ====
 
'''GET''' /health
 
Checks API and Podman connectivity.
 
'''Response:'''
 
<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
 
Returns API performance metrics.
 
'''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 ==
 
=== Error Response Format ===
 
All errors follow a consistent format:


<pre>{
<pre>{
Line 114: Line 274:
   &quot;error&quot;: {
   &quot;error&quot;: {
     &quot;message&quot;: &quot;Error description&quot;,
     &quot;message&quot;: &quot;Error description&quot;,
     &quot;code&quot;: 400,
     &quot;code&quot;: 404,
     &quot;type&quot;: &quot;ErrorType&quot;
     &quot;type&quot;: &quot;PodmanAPIError&quot;
   },
   },
   &quot;timestamp&quot;: &quot;ISO-8601-timestamp&quot;
   &quot;timestamp&quot;: &quot;2025-01-19T15:47:19.123456&quot;
}</pre>
}</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)
|}
=== Common Error Types ===
* '''PodmanAPIError''': Podman-related errors
* '''InternalError''': Server-side errors
* '''ValidationError''': Request validation failures
== Examples ==
=== 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
# 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
# 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
# 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 ===
<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 ==
* 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 ===
# '''“Container not found” errors''':
Ensure the container name is correct and the container exists
# '''“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
# '''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.