diff --git a/container-exec/Dockerfile b/container-exec/Dockerfile new file mode 100644 index 0000000..3d6593f --- /dev/null +++ b/container-exec/Dockerfile @@ -0,0 +1,157 @@ +# syntax=docker/dockerfile:1 + +FROM node:18-slim + +# Install common CLI tools that might be useful for exec commands +RUN apt-get update && apt-get install -y \ + curl \ + wget \ + git \ + vim \ + nano \ + htop \ + net-tools \ + iputils-ping \ + dnsutils \ + procps \ + coreutils \ + findutils \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Create the exec server application +RUN cat > server.js << 'EOF' +const http = require('http'); +const { spawn } = require('child_process'); +const { promisify } = require('util'); + +const server = http.createServer(async (req, res) => { + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // Handle exec endpoint + if (req.url === '/__exec' && req.method === 'POST') { + try { + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', async () => { + try { + const payload = JSON.parse(body); + const result = await executeCommand(payload); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } catch (error) { + console.error('Exec error:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + exitCode: 1, + stdout: '', + stderr: error.message, + duration: 0 + })); + } + }); + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + exitCode: 1, + stdout: '', + stderr: 'Failed to process exec request', + duration: 0 + })); + } + return; + } + + // Handle regular HTTP requests + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Container Exec Demo Server\\nUse the /__exec endpoint for command execution'); +}); + +async function executeCommand(payload) { + const { command, workingDirectory, env, timeout = 30000 } = payload; + const startTime = Date.now(); + + return new Promise((resolve) => { + const options = { + cwd: workingDirectory || '/app', + env: { ...process.env, ...(env || {}) }, + timeout: timeout, + killSignal: 'SIGKILL' + }; + + console.log('Executing command:', command, 'with options:', options); + + const child = spawn(command[0], command.slice(1), options); + + let stdout = ''; + let stderr = ''; + + child.stdout?.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr?.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code, signal) => { + const duration = Date.now() - startTime; + + // Handle timeout + if (signal === 'SIGKILL' && duration >= timeout - 100) { + resolve({ + exitCode: 124, + stdout: stdout, + stderr: stderr + '\\nCommand timed out', + duration: duration + }); + return; + } + + resolve({ + exitCode: code || 0, + stdout: stdout, + stderr: stderr, + duration: duration + }); + }); + + child.on('error', (error) => { + resolve({ + exitCode: 1, + stdout: stdout, + stderr: stderr + '\\nError: ' + error.message, + duration: Date.now() - startTime + }); + }); + + // Set up timeout + setTimeout(() => { + if (!child.killed) { + child.kill('SIGKILL'); + } + }, timeout); + }); +} + +const PORT = process.env.PORT || 8080; +server.listen(PORT, () => { + console.log(\`Container exec server running on port \${PORT}\`); + console.log('Ready to handle exec requests at /__exec'); +}); +EOF + +EXPOSE 8080 + +CMD ["node", "server.js"] diff --git a/container-exec/README.md b/container-exec/README.md new file mode 100644 index 0000000..88674aa --- /dev/null +++ b/container-exec/README.md @@ -0,0 +1,74 @@ +# Container Exec Demo + +This demo showcases the `container.exec()` functionality, allowing you to execute arbitrary commands inside running containers with a simple, clean API. + +## Features + +- **Simple API**: Just `await container.exec('your command')` - no manual container state checking required +- **Automatic Container Management**: The container is automatically started if not running +- **Rich Command Support**: Execute both string commands and command arrays +- **Comprehensive Results**: Get stdout, stderr, exit codes, and execution duration +- **Error Handling**: Graceful handling of timeouts, network errors, and command failures +- **Flexible Options**: Working directory, environment variables, and custom timeouts + +## Usage Examples + +### Basic Command Execution +```typescript +const result = await container.exec('echo "Hello from container!"'); +console.log(result.stdout); // "Hello from container!" +``` + +### Command with Options +```typescript +const result = await container.exec('echo $NODE_ENV', { + env: { NODE_ENV: 'production' }, + workingDirectory: '/app', + timeout: 5000 +}); +``` + +### Array Commands +```typescript +const result = await container.exec(['ls', '-la', '/app']); +``` + +## Demo Endpoints + +- `GET /` - Interactive web interface for testing exec commands +- `POST /exec` - API endpoint for executing commands +- `GET /examples` - Pre-built command examples +- `GET /status` - Container and execution status + +## Running the Demo + +```bash +npm install +npm run dev +``` + +Then visit http://localhost:8787 to interact with the container exec demo. + +## Container Setup + +The demo uses a simple Node.js container that: +- Listens on port 8080 for HTTP requests +- Implements the `/__exec` endpoint for command execution +- Provides a clean execution environment with common CLI tools + +## Command Examples to Try + +- **System Info**: `uname -a` +- **File System**: `ls -la /` +- **Environment**: `env | grep NODE` +- **Process List**: `ps aux` +- **Network**: `curl -s https://httpbin.org/json` +- **Custom Script**: `echo "console.log('Hello from Node!')" | node` + +## Error Handling + +The exec API gracefully handles various error scenarios: +- **Command not found**: Returns exit code 127 with appropriate stderr +- **Timeout**: Returns exit code 124 with "Command timed out" message +- **Network errors**: Returns exit code 1 with connection error details +- **Container failures**: Automatically attempts to restart and retry diff --git a/container-exec/package.json b/container-exec/package.json new file mode 100644 index 0000000..074f0da --- /dev/null +++ b/container-exec/package.json @@ -0,0 +1,16 @@ +{ + "name": "container-exec-demo", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "cf-typegen": "wrangler types" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250403.0", + "typescript": "^5.5.2", + "wrangler": "^4.25.0" + } +} diff --git a/container-exec/src/index.ts b/container-exec/src/index.ts new file mode 100644 index 0000000..f0df2fc --- /dev/null +++ b/container-exec/src/index.ts @@ -0,0 +1,386 @@ +import { Container } from '@cloudflare/containers'; + +interface Env { + // Define your environment variables here +} + +export class ExecContainer extends Container { + constructor(ctx: DurableObject['ctx'], env: Env) { + super(ctx, env, { + defaultPort: 8080, + sleepAfter: '10m' + }); + } + + /** + * Main request handler + */ + async fetch(request: Request): Promise { + const url = new URL(request.url); + const path = url.pathname; + + try { + switch (path) { + case '/': + return this.handleHome(); + case '/exec': + return this.handleExecAPI(request); + case '/examples': + return this.handleExamples(); + case '/status': + return this.handleStatus(); + default: + return new Response('Not Found', { status: 404 }); + } + } catch (error) { + console.error('Request error:', error); + return new Response('Internal Server Error', { status: 500 }); + } + } + + private handleHome(): Response { + const html = ` + + + + Container Exec Demo + + + +
+

🐳 Container Exec Demo

+

Execute commands in your container with a simple, powerful API

+ +
+

💻 Execute Command

+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+ +
+

📚 Quick Examples

+
+
+
Basic Echo
+
echo "Container exec is working!"
+
+
+
System Info
+
uname -a
+
+
+
List Root Directory
+
ls -la /
+
+
+
Process List
+
ps aux
+
+
+
Environment Variables
+
env | head -10
+
+
+
Date & User
+
date && whoami
+
+
+
+ +
+

📖 API Usage

+

You can also use the exec API programmatically:

+
+// Basic usage +const result = await container.exec('echo "Hello World"'); +console.log(result.stdout); // "Hello World" + +// With options +const result = await container.exec('npm test', { + workingDirectory: '/app', + env: { NODE_ENV: 'test' }, + timeout: 60000 +}); + +// Array command +const result = await container.exec(['ls', '-la', '/app']); +
+
+
+ + + +`; + + return new Response(html, { + headers: { 'Content-Type': 'text/html' } + }); + } + + private async handleExecAPI(request: Request): Promise { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + try { + const { command, options } = await request.json() as { + command: string | string[]; + options?: { + workingDirectory?: string; + env?: Record; + timeout?: number; + }; + }; + + if (!command) { + return new Response(JSON.stringify({ error: 'Command is required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + // Execute the command using the container's exec method + const result = await this.exec(command, options); + + return new Response(JSON.stringify(result), { + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return new Response(JSON.stringify({ + error: errorMessage, + exitCode: 1, + success: false + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + } + + private handleExamples(): Response { + const examples = [ + { + title: "System Information", + commands: [ + { desc: "OS and kernel info", cmd: "uname -a" }, + { desc: "System uptime", cmd: "uptime" }, + { desc: "Memory usage", cmd: "free -h" }, + { desc: "Disk usage", cmd: "df -h" } + ] + }, + { + title: "Process Management", + commands: [ + { desc: "List all processes", cmd: "ps aux" }, + { desc: "Current user", cmd: "whoami" }, + { desc: "Current directory", cmd: "pwd" }, + { desc: "Environment variables", cmd: "env | head -20" } + ] + }, + { + title: "File Operations", + commands: [ + { desc: "List root directory", cmd: "ls -la /" }, + { desc: "Find files", cmd: "find /usr -name '*.conf' | head -10" }, + { desc: "File permissions", cmd: "ls -la /etc/passwd" }, + { desc: "Disk usage by directory", cmd: "du -sh /* 2>/dev/null | head -10" } + ] + }, + { + title: "Network & Connectivity", + commands: [ + { desc: "Network interfaces", cmd: "ip addr show" }, + { desc: "Test HTTP request", cmd: "curl -s -o /dev/null -w '%{http_code}' https://httpbin.org/status/200" }, + { desc: "DNS resolution", cmd: "nslookup google.com" } + ] + } + ]; + + const html = ` + + + + Container Exec Examples + + + + ← Back to Exec Demo +

Container Exec Examples

+ + ${examples.map(section => ` +
+

${section.title}

+ ${section.commands.map(cmd => ` +
+
${cmd.desc}
+
${cmd.cmd}
+
+ `).join('')} +
+ `).join('')} + +`; + + return new Response(html, { + headers: { 'Content-Type': 'text/html' } + }); + } + + private async handleStatus(): Response { + const state = await this.getState(); + + const status = { + containerState: state, + currentTime: new Date().toISOString(), + sleepAfter: this.sleepAfter, + defaultPort: this.defaultPort, + lastChange: new Date(state.lastChange).toISOString(), + uptime: `${Math.round((Date.now() - state.lastChange) / 1000)}s` + }; + + return new Response(JSON.stringify(status, null, 2), { + headers: { 'Content-Type': 'application/json' } + }); + } +} + +// Export the Durable Object +export { ExecContainer as default }; diff --git a/container-exec/tsconfig.json b/container-exec/tsconfig.json new file mode 100644 index 0000000..96054ba --- /dev/null +++ b/container-exec/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "moduleResolution": "node", + "types": [ + "@cloudflare/workers-types/2023-07-01" + ], + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/container-exec/wrangler.jsonc b/container-exec/wrangler.jsonc new file mode 100644 index 0000000..271fa48 --- /dev/null +++ b/container-exec/wrangler.jsonc @@ -0,0 +1,30 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "container-exec-demo", + "main": "src/index.ts", + "compatibility_date": "2025-04-03", + "containers": [{ + "image": "./Dockerfile", + "class_name": "ExecContainer", + "max_instances": 1 + }], + "durable_objects": { + "bindings": [ + { + "class_name": "ExecContainer", + "name": "EXEC_CONTAINER" + } + ] + }, + "migrations": [ + { + "new_sqlite_classes": [ + "ExecContainer" + ], + "tag": "v1" + } + ], + "observability": { + "enabled": true + } +}